浏览器的Event Loop
Javascript有一个main thread主线程和call-stack调用栈(执行栈),所有的任务都会被放到调用栈中等待主线程执行。
JS调用栈(执行栈)
js调用栈是先进后出,当函数执行后,会从栈顶移出,直到栈内被清空。
同步任务和异步任务
- js单线程将任务分为同步任务和异步任务,同步任务会在调用栈中按照顺序等待主线程依次执行;异步任务有结果之后,将注册的回调函数放到任务队列中等待主线程空闲的时候执行(调用栈被清空时)。
任务队列Task Queue,是一种先进先出的数据结构。
-
在执行栈中执行完
同步任务之后,查看执行栈是否为空,为空就去检查微任务队列是否为空,不为空就一次性执行完所有的微任务,为空就去执行宏任务。 -
每次执行宏任务之前,必须保证微任务队列被清空。不为空,就执行完所有微任务之后,设置微任务队列为
null,然后执行宏任务,如此循环。这个过程就是Event Loop。
(Event Loop只是负责告诉你该执哪行些任务,或者说哪些回调被触发了,真正的逻辑还是在进程中执行的。)
- 进入更新渲染阶段,判断是否需要渲染,这里有
rendering opportunity,就是说不一定每一次event loop都会对应一次浏览器渲染,要根据屏幕刷新率、页面性能、页面是否在后台运行来共同决定。
Event loop在浏览器中的表现
<div id="outer">
<div id="inner"></div>
</div>
const $inner = document.querySelector('#inner')
const $outer = document.querySelector('#outer')
function handler () { console.log('click') // 直接输出
Promise.resolve().then(_ => console.log('promise')) // 注册微任务
setTimeout(_ => console.log('timeout')) // 注册宏任务
requestAnimationFrame(_ => console.log('animationFrame')) // 注册宏任务
$outer.setAttribute('data-random', Math.random()) // DOM属性修改,触发微任务
}
new MutationObserver(_ => {
console.log('observer')
}).observe($outer, {
attributes: true
})
$inner.addEventListener('click',handler)
$outer.addEventListener('click', handler)
如果点击#inner,执行顺序是:click -> promise -> observer -> click -> promise -> observer -> animationFrame -> animationFrame -> timeout -> timeout。
-
因为一次
I/O创建了一个宏任务,就会触发handler。同步代码执行完之后,就会去查看是否有微任务,有Promise和MutationObserver两个微任务,去执行他们。 -
因为
click事件会冒泡,所以对应的I/O会触发两次handler函数(inner、outer),冒泡事件会早于其他的宏任务。 -
执行完同步代码和微任务,继续查找宏任务,因为我们触发了
setAttribute,实际上修改了DOM的属性,这会导致页面重绘,而这个set的操作是同步的,所以就是requestAnmationFrame(宏任务)的回调会早于setTimeout。(requestAnmationFrame会在渲染之前被调用,因为合理来说就是在渲染之前更改DOM) -
MutationObserver的监听不会同时触发多次,而是多次修改只会有一次回调被触发
Node的Event Loop和浏览器的有什么区别
Node新增了两个方法:微任务的process.nextTick以及宏任务的setImmediate。
setImmediate和setTimeout的区别:
在官方文档中的定义,setImmediate为一次Event Loop执行完毕后调用。
setTimeout则是通过计算一个延迟时间后进行执行。
process.nextTick
这个函数是独立于Event Loop之外,他有一个自己的队列,如果存在nextTick队列,会先清空这个队列的所有回调函数,而且是早于其他microtask执行