浏览器事件循环机制Event Loop

550 阅读3分钟

前言

学习概念之前让我们来看几行简单的代码:

console.log(1)
setTimeout(() => {
    console.log(2);
}, 0);
const promise = new Promise((resolve, reject) => {
  console.log(3);
  resolve();
});
promise.then(() => {
  console.log(4);
});
console.log(5);

输出结果如下:

1
3
5
4
2

和你预想的结果一样吗?
我经常在工作中遇到这种类似的问题,我明明想让它先执行,他就是后执行,导致页面渲染的各种问题。
想要弄清楚为什么输出结果是这样的,我们就需要了解浏览器的事件循环机制。

事件循环机制

JavaScript 事件循环机制分为: 浏览器事件循环机制和 Node 事件循环机制,两者的实现技术不一样,浏览器 Event Loop 是 HTML 中定义的规范,Node Event Loop 是由 libuv 库实现。 我们只讲浏览器事件循环机制。 event-loop.png 浏览器执行 js 代码大致可以分为三个步骤,而这三个步骤的循环构成了 js 的事件循环机制(如上图所示)。

  1. 主线程(js引擎线程)中执行宏任务(JS整体代码或回调函数),执行过程中会将对象存储到堆(heap)中,将函数的参数和局部变量加入到栈(stack)中,执行完毕后会释放堆或退出栈。执行完这个宏任务后,会判断微任务队列是否为空,如果不为空,则会将所有的微任务依次取出并执行。如果在这个过程中触发了任何 Web APIs 将进行第二步操作。

  2. 调用 Web API,并在合适的时候将回调函数加入到事件回调队列(event queue)中。比如执行了setTimeout(callback1, 1000),会创建一个计时器,并且在另一个线程(浏览器定时触发线程)里面监听计时器是否过期,等到计时器过期后,会将对应回调 callback1加入事件回调队列中。

  3. 等到第一步中的微任务执行完毕之后,会判断事件回调队列是否为空。如果不为空,则会取出并执行最先进入队列的回调函数,执行过程如同第一步。如果为空,则会视情况进行等待或挂起主线程。 一句话总结:先执行一个宏任务,再执行这个宏任务产生的对应微任务,执行完毕后,再执行后面的宏任务,以此往复。

宏任务、微任务

  • 宏任务macro-task
    • Script整体代码、setTimeout、setInterval、I/O操作、UI rendering
  • 微任务micro-task
    • new Promise().then中的内容、MutationObserve(前端的回溯) 了解完了宏任务与微任务的分类和js执行宏任务与微任务的顺序,我们再来看开头的那个例子
console.log(1)
setTimeout(() => {
    console.log(2);
}, 0);
const promise = new Promise((resolve, reject) => {
  console.log(3);
  resolve();
});
promise.then(() => {
  console.log(4);
});
console.log(5);

第一次宏任务(整体代码):输出1,遇到setTimeout加入到宏任务队列(等待执行),遇到promise正常输出3,遇到.then()加入到微任务队列,输出5,本次宏任务执行完毕,共输出1 3 5
第一次宏任务执行完毕,清空这个宏任务产生的微任务队列,输出4
检查宏任务队列,发现还有一个宏任务setTimeout,执行该任务,输出2
最终结果是1 3 5 4 2

趁热打铁

js 执行顺序:先执行一个宏任务,再执行这个宏任务产生的对应微任务,执行完毕后,再执行后面的宏任务,以此往复。

function func1() {
  console.log('func1');
  Promise.resolve().then(() => {
    console.log('microtask.promise1');
  });
}
function func2() {
  console.log('func2');
  Promise.resolve().then(() => {
    console.log('microtask.promise2');
  });
}
function main() {
  func1();
  func2();
  setTimeout(func1, 0);
  setTimeout(func2, 0);
}
main()

输出结果如下:

// 第一次宏任务(整体代码)执行
func1
func2
microtask.promise1
microtask.promise2
// 第二次宏任务(setTimeout(func1, 0))
func1
microtask.promise1
// 第三次宏任务(setTimeout(func2, 0))
func2
microtask.promise2

是不是已经完全懂了,你要相信你是最棒的。