理解js的事件循环
参考资料
[1] JS浏览器事件循环机制
PS: 本文除了对微任务和宏任务有自己的理解外,其他的都是复制粘贴。
浏览器内核
浏览器内核中有种线程在工作:
- GUI渲染线程:负责渲染页面,解析HTML,CSS构成DOM树等,当页面重绘或者由于某种操作引起回流都会调起该线程。和JS引擎线程是互斥的,当JS引擎线程在工作的时候,GUI渲染线程会被挂起,GUI更新被放入在JS任务队列中,等待JS引擎线程空闲的时候继续执行。
- JS引擎线程:单线程工作,负责解析运行JavaScript脚本。和GUI渲染线程互斥,JS运行耗时过长就会导致页面阻塞。
- 事件触发线程:当事件符合触发条件被触发时,该线程会把对应的事件回调函数添加到任务队列的队尾,等待JS引擎处理。
- 定时器触发线程:浏览器定时计数器并不是由JS引擎计数的,阻塞会导致计时不准确。开启定时器触发线程来计时并触发计时,计时完成后会被添加到任务队列中,等待JS引擎处理。
- http请求线程:http请求的时候会开启一条请求线程。请求完成有结果了之后,将请求的回调函数添加到任务队列中,等待JS引擎处理。
事件循环机制
JavaScript中有一个main thread(主线程)和call stack(调用栈,或执行栈),所有任务都会被放到call stack等待主线程执行。
同步任务和异步任务
JavaScript 单线程中的任务分为同步任务和异步任务。同步任务会在调用栈中按照顺序排队等待主线程执行,异步任务则会在异步有了结果后将注册的回调函数添加到任务队列(消息队列)中等待主线程空闲的时候,也就是栈内被清空的时候,被读取到栈中等待主线程执行。任务队列是先进先出的数据结构。
宏任务和微任务
为什么要分宏任务和微任务呢?我认为是这样的:如果只有宏任务,对于一个异步任务task来说,最快只能通过如setTimeout(task, 0)这样的方式将其添加到任务队列的末尾,如果此时任务队列里的任务很多的话,轮到task执行的时间就会很晚。现在有了微任务,而微任务比宏任务先执行,因此异步任务执行的时间可以提前到微任务执行的阶段。
那为什么要增加一个微任务队列呢,不能直接将异步任务放到任务队列的头部吗?原因是凡事讲究个先来后到,像js这样单线程执行的语言更加如此。如果采用将想先要执行的异步任务放到任务队列的头部这种方式的话,当我们想让一个异步任务task在js执行栈清空后先执行的话,得把将task加入任务队列头部的代码放到最后面出现,这显然违背我们的编程习惯——我们总希望先出现的先执行。
除了广义的同步任务和异步任务,JavaScript 单线程中的任务可以细分为宏任务(macro-task)和微任务(micro-task)。
macro-task:script(整体代码)、setTimeout、setInterval、setImmediate、I/O(网络、文件)相关API、UI rendering
micro-task:requestAnimationFrame、process.nextTick、Promise.prototype.then/catch/finally、Object.observe、MutationObserver、queueMicrotask
第一次事件循环中,JavaScript 引擎会把整个 script 代码当成一个宏任务执行,执行完成之后,再检测本次循环中是否寻在微任务,存在的话就依次从微任务的任务队列中读取执行完所有的微任务,再读取宏任务的任务队列中的任务执行,再执行所有的微任务,如此循环。JS 的执行顺序就是每次事件循环中的宏任务-微任务。
console.log(1); // 在第一个宏任务内执行
setTimeout(function() {
console.log(2); // setTimeout的任务被放到宏任务队列中
})
var promise = new Promise(function(resolve, reject) {
console.log(3); // Promise的函数是即刻执行的,因此在第一个宏任务内执行
resolve();
})
promise.then(function() {
console.log(4); // promise.then的任务被放到微任务队列中
})
console.log(5); // 在第一个宏任务内执行
上面这串代码,输出是13542。