[每日一题] 简述浏览器事件循环运行机制

763 阅读3分钟

我们先看下面一段代码:

async function async_func1() {
  console.log('async_func1 start');
  await async_func2();
  console.log('async_func1 end');
}

async function async_func2() {
  console.log('async_func2 start');
}

console.log('script start');

setTimeout(function(){
  console.log('setTimeout');
}, 0);

async_func1();

new Promise((resolve, reject) => {
  console.log('promise1');
  resolve();
  console.log('promise1 resolve');
}).then(() => {
  console.log('promise2');
});

输出结果为:script start -> async_func1 start -> async_func2 start -> promise1 -> promise1 resolve -> async_func1 end -> promise2 -> setTimeout

答对没有?错了也没关系,看完下面的分析就明白了。

JS 执行分为同步任务和异步任务,同步任务都在主线程上执行,形成一个执行栈。JS 引擎线程之外,事件触发线程管理着一个任务队列,当事件(如 onclick、onmousemove等)被触发时,事件响应 callback 函数被放入任务队列中。一旦执行栈中所有同步任务执行完毕,JS 引擎就会读取任务队列,将异步任务添加到执行栈中执行。

当调用 setTimeout 或 setInterval 时,定时器线程负责计时,计时完成后将 callback 函数推入事件队列中。

JS 中分为两种任务类型:宏任务(macrotask)和微任务(microtask)。宏任务可以理解为每次执行栈执行的代码(包括每次从事件队列中获取一个事件回调并放到执行栈执行),一个宏任务执行结束下一个宏任务开始前允许页面渲染(如果页面有变化的话)。

微任务放置在微任务队列中,当前宏任务执行结束后立即执行微任务队列中所有微任务,它在页面渲染前执行。

宏任务包括:主代码块、setTimeout 和 setInterval 的 callback、各事件响应程序等。

微任务包括:Promise.then、process.nextTick、async 函数里 await 后的内容等。

总结事件循环运行机制如下:

  1. 执行一个宏任务(执行栈中没有就从事件队列中获取)
  2. 执行过程中遇到微任务,就把它添加到微任务队列中
  3. 当前宏任务执行完毕,立即执行当前微任务队列中所有微任务
  4. 所有微任务执行完毕,检查渲染,如有变化,GUI 线程接管进行渲染
  5. 渲染完毕,JS 引擎线程继续接管,开始下一个宏任务

注意:Promise.then(callback) 中的 callback 属于微任务,但new Promise(executor)时的 executor 是同步任务,不是微任务,因为它会在 new 之后立即执行。

返回最开始的题目:

先打印 "script start" -> 然后 function(){ console.log('setTimeout'); } 被加入到事件队列中 -> 执行 async_func1 函数并打印 "async_func1 start" -> 执行 async_func2 函数并打印 "async_func2 start" -> console.log('async_func1 end'); 被放入微任务队列中 -> new Promise 并打印 "promise1" 和 "promise1 resolve" -> () => {console.log('promise2');} 被放入微任务队列中 -> 当前宏任务执行完毕,执行微任务队列中所有的微任务,依次打印 "async_func1 end" 和 "promise2" -> 从事件队列中读取下一个宏任务,即打印"setTimeout"

————————

文章已同步至公众号 《前端做题家》,关注回复“资料”领取前端面试题和学习资料。

文章均为个人理解,仅供参考,欢迎留言讨论交流。