这个事件机制我看了好多的文章,有不少被指出有理解错误或问题,我一直对这个东西处于迷迷糊糊的状态,今天看到一道题,决定好好研究一下。本文只是我自己的的学习总结,不免有问题,希望看到问题的朋友批评指正。
工具:Loupe我们可以通过使用 Loupe(Loupe是一种可视化工具,可以帮助您了解JavaScript的调用堆栈/事件循环/回调队列如何相互影响)工具来了解上面代码的执行情况。
1. JavaScript是单线程
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
2.任务队列
所有任务分为两种,一种同步任务,一种是异步任务,同步任务会在主线程排队执行,遇到异步事件不会等待它返回结果,而是将这个事件挂起,继续执行执行栈中的其他任务。当异步事件返回结果,将它放到事件队列中,被放入事件队列不会立刻执行起回调,而是等待当前执行栈中所有任务都执行完毕,主线程空闲状态,主线程会去查找事件队列中是否有任务,如果有,则取出排在第一位的事件,并把这个事件对应的回调放到执行栈中,然后执行其中的同步代码。
3.宏任务和微任务
页面渲染事件,各种IO的完成事件等随时被添加到任务队列中,一直会保持先进先出的原则执行,我们不能准确地控制这些事件被添加到任务队列中的位置。但是这个时候突然有高优先级的任务需要尽快执行,那么一种类型的任务就不合适了,所以引入了微任务队列。
宏任务:macrotask
script(整体代码)
setTimeout()
setInterval()
postMessage(window.postMessage() 方法可以安全地实现跨源通信,比如H5和app通信)
I/O
UI交互事件
微任务: microtask
new Promise().then(回调)
MutationObserver(html5 新特性)
1. 异步任务的返回结果会被放到一个任务队列中,根据异步事件的类型,这个事件实际上会被放到对应的宏任务和微任务队列中去。
在当前执行栈为空时,主线程会查看微任务队列是否有事件存在。
2. 当前执行栈执行完毕后时会立刻处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。
3. 关于macrotask和microtask的理解,光这样看会有些晦涩难懂,结合事件循坏的机制理解清晰很多,下面这张图可以说是介绍得非常清楚了。
总结起来,一次事件循环的步骤包括:
- 检查macrotask队列是否为空,非空则到2,为空则到3
- 执行macrotask中的一个任务
- 继续检查microtask队列是否为空,若有则到4,否则到5
- 取出microtask中的任务执行,执行完成返回到步骤3
- 执行视图更新
上代码:
console.log('start')
setTimeout(function() {
console.log('setTimeout')
}, 0)
Promise.resolve().then(function() {
console.log('promise1')
}).then(function() {
console.log('promise2')
})
console.log('end')
1.全局代码压入执行栈执行,输出 start
2.setTimeout压入 macrotask队列,promise.then 回调放入 microtask队列,最后执行 console.log(‘end’),输出 end
3.调用栈中的代码执行完成(全局代码属于宏任务),接下来开始执行微任务队列中的代码,执行promise回调,输出 promise1, promise回调函数默认返回 undefined, promise状态变成 fulfilled ,触发接下来的 then回调,继续压入 microtask队列,此时产生了新的微任务,会接着把当前的微任务队列执行完,此时执行第二个 promise.then回调,输出 promise2
4.此时,microtask队列 已清空,接下来会会执行 UI渲染工作(如果有的话),然后开始下一轮 event loop, 执行 setTimeout的回调,输出setTimeout
最后的执行结果如下
start
end
promise1
promise2
setTimeout
4.总结
事件循环是js实现异步的核心
每轮事件循环分为3个步骤:
a) 执行macrotask队列的一个任务
b) 执行完当前microtask队列的所有任务
c) UI render
浏览器只保证requestAnimationFrame的回调在重绘之前执行,没有确定的时间,何时重绘由浏览器决定
【参考资料】
1.JavaScript中的Event Loop(事件循环)机制
2.HTML系列:macrotask和microtask
3.深入理解js事件循环机制(浏览器篇)