事件循环

89 阅读3分钟

浏览器的Event Loop

Javascript有一个main thread主线程和call-stack调用栈(执行栈),所有的任务都会被放到调用栈中等待主线程执行。

image.png

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。同步代码执行完之后,就会去查看是否有微任务,有PromiseMutationObserver两个微任务,去执行他们。

  • 因为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执行