Event Loop

238 阅读4分钟

Event Loop 是计算机系统的一种运行机制。(是一个程序结构,用于等待发送消息和事件)。 在不同的地方有不同的实现。;浏览器和 NodeJs 基于不同的技术实现了各自的 Evevt Loop.

JS 语言就是采用这种机制,来解决单线程运行带来的一些问题。

堆、栈、队列 (补充点)

堆(Heap)

是一种数据结构,是利用完全二叉树维护的一组数据,堆分为两种,一种为最大堆,一种为最小堆,将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。 堆是线性数据结构,相当于一维数组,有唯一后继。

栈(Stack)

在计算机科学中是限定仅在表尾进行插入或删除操作的线性表。 栈是一种数据结构,它按照后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据。 先进后出

队列(Queue)

先进先出

Event Loop

JavaScript 中,任务被分为两种,一种宏任务(MacroTask)也叫 Task,一种叫微任务(MicroTask)。

MacroTask(宏任务)

一些异步任务的回调会依次进入 MacroTask queue,等待后续被调用,这些异步任务包括:

  • script 全部代码
  • setTimeout
  • setInterval
  • setImmediate (浏览器暂时不支持,只有IE10支持,具体可见MDN)
  • I/O
  • UI Rendering

MicroTask(微任务)

  • Process.nextTick(Node独有)
  • Promise
  • Object.observe(废弃)
  • MutationObserver 具体查看

浏览器中的 Event Loop

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

JS 代码的具体执行流程

通过一张图帮助理解

  1. 执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如setTimeout等);
  2. 全局Script代码执行完毕后,调用栈Stack会清空;
  3. 从微队列microtask queue中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1;
  4. 继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到直到把microtask queue中的所有任务都执行完毕。注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行
  5. microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空;
  6. 取出宏队列macrotask queue中位于队首的任务,放入Stack中执行;
  7. 执行完毕后,调用栈Stack为空;
  8. 重复第3-7个步骤;
  9. 重复第3-7个步骤;
  10. ......

可以看到,这就是浏览器的事件循环Event Loop

这里归纳3个重点:

  1. 宏队列 macrotask 一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务;
  2. 微任务队列中所有的任务都会被依次取出来执行,直到 microtask queue 为空;
  3. 图中没有画UI rendering的节点,因为这个是由浏览器自行判断决定的,但是只要执行 UI rendering,它的节点是在执行完所有的 microtask 之后,下一个 macrotask 之前,紧跟着执行UI render。

案例1

console.log(1);

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3)
  });
});

new Promise((resolve, reject) => {
  console.log(4)
  resolve(5)
}).then((data) => {
  console.log(data);
})

setTimeout(() => {
  console.log(6);
})

console.log(7);

// 1, 4, 7, 5, 2, 3, 6

分析:

  1. 主线程: 1,4,7 (new Promise() 立即执行)
  2. 微任务:5.
  3. 宏任务:2.
  4. 微任务:3.
  5. 宏任务:6.

案例2:

console.log('script start')

async function async1() {
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2 end') 
}
async1()

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

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

console.log('script end')

// script start, async2 end, Promise, script end, async1 end, promise1, promise2, setTimeout

async/await 在底层转换成了 promisethen 回调函数。其实就是 Promise 的语法糖。

每次我们使用 await, 解释器都创建一个 promise 对象,然后把剩下的 async 函数中的操作放到 then 回调函数中。

NodeJS的Event Loop 参考

process.nextTick 解析参考