一文读懂 Event Loop

Posted 园子Talk

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文读懂 Event Loop相关的知识,希望对你有一定的参考价值。



前言


作为开发者,了解程序运行机制是很重要的,这样可以帮助输出优质的代码。技术不断更新迭代,掌握底层的原理可以应对新的技术。本篇将探讨 Event Loop。


什么是 Event Loop


来自 Wikipedia 的定义:


Event Loop 是一个程序结构,用于等待和发送消息和事件。(a programming construct that waits for and dispatches events or messages in a program.)


简单说,就是在程序中设置两个线程,一个负责本身的运行,称为主线程;另一个负责主线程与其他进程(主要是各种 I/O 操作)的通信,成为 Event Loop 线程。


进程与线程


进程:CPU 资源分配的最小单位。


线程:CPU 调度的最小单位。



JavaScript 单线程


众所周知,javascript 是一个单线程机制的语言,也就是,同一个时间只能做一件事。


JavaScript 为何不能有多个线程?


JavaScript 的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript 主要用途与用户互动,以及操作 DOM。这决定了它只能是单线程。


假设 JavaScript 同时有两个线程,一个线程在某个 DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应以哪个线程为准?


所以,为了避免复杂性,从一诞生,JavaScript 就是单线程。


浏览器端的 Event Loop

一文读懂 Event Loop

上图是 JS 的运行机制图,JS运行时大致会分为几个部分:


  • Call Stack:执行栈,所有同步任务在主线程上执行,形成一个执行栈。因为 JS 单线程,所以执行栈中每一次只能执行一个任务,当遇到的同步任务执行完之后,由任务队列提供任务给执行栈执行。


  • Task Queue:任务队列,存放异步任务,当异步任务可以执行的时候,任务队列会通知主线程,然后该任务会进入主线程执行。任务队列中都是已经完成的异步操作,而不是注册一个异步任务就会被放在这个任务队列中。


总结以上,Event Loop 也可以理解为:不断地从任务队列中取出任务执行的一个过程。


同步任务与异步任务


同步任务必须等到结果来了之后才能做其他的事


异步任务不需要等到结果来了才能继续往下走,等结果期间可以做其他的事

从概念上可以看出,异步任务从一定程度上更高效一些,核心是提高用户体验。


Event Loop 调度运行机制


Event Loop 很好的调度了任务的运行,现在看看它的调度运行机制。


执行 JS 代码时,主线程会从上到下一步步的执行代码,同步任务会被依次加入执行栈中执行;


异步任务会在拿到结果的时候将注册的回调函数放入任务队列,当执行栈中没有任务在执行时,引擎会从任务队列中读取任务压入执行栈中执行。


宏任务与微任务


任务队列是一个消息队列,先进先出。就是说,后来的事都是被加在队尾等到前面的事执行完之后才会被执行。


如果在执行的过程中突然有重要的数据需要获取,或者说有事件突然需要处理,按照队列的先进先出顺序是无法得到及时处理。这时就催生了宏任务和微任务,微任务使得异步任务得到及时的处理。


所以上面提到的异步任务又分为宏任务和微任务,JS 运行时任务队列会被分为宏任务队列和微任务队列,分别对应宏任务和微任务。


浏览器端的宏任务和微任务大致有:


  • 宏任务        


        1. script 整体代码


        2. setTimeout


        3. setInterval


        4. I/O 操作


  • 微任务


        1. Promise.then

      

         2. MutationObserver


事件执行顺序


    1. 执行同步任务,同步任务不需要做特殊处理,直接执行(下面步骤遇到同步任务都是一样处理),第一轮从 script 开始。


    2. 从宏任务队列中取出队头任务执行。


    3. 如果产生宏任务,将宏任务放入队列,下次轮循时执行。


    4. 如果产生微任务,将微任务放入微任务队列。


    5. 执行完当前宏任务之后,取出微任务队列中的所有任务依次执行。


    6. 如果微任务执行过程中产生新的微任务,则继续执行微任务,直到微任务的队列为空。


    7. 轮循以上 2 - 6。


举栗


console.log('script start')new Promise((resolve, reject) => {  console.log('promise1-1');  resolve();}).then(() => {  console.log('then1-1');  new Promise((resolve, reject) => {   console.log('promise2-1');   resolve();  })  .then(() => {   console.log('then2-1')  })  .then(() => {    console.log('then2-2')  })}).then(() => { console.log('then1-2')})setTimeout(() => { console.log('setTimeout')})console.log('script end')


根据事件执行顺序分析:


  • 遇到 console.log,输出 script start


  • 遇到 setTimeout 产生宏任务,注册到宏任务队列,下一轮 Event Loop 执行


  • 遇到 new Promise,输出 promise1-1,然后 resolve


  • 遇到最后一个 console.log,输出 script end,当前执行栈清空


  • 从微任务队列中取出队头任务 then1-1 进行执行


  • 往下遇到 new Promise,输出 promise2-1,然后 resolve


  • resolve 匹配到 promise2-1 第一个then,把这个 then 注册到微任务队列 [then2-1],当前 then1-1 可执行部分结束。然后产生 promise2-1 的第二个 then,把这个 then 注册到微任务 [then2-1, then1-2]


  • 拿出微任务队头任务 then1-2,触发 promise2-1 第二个 then,注册到微任务队列 [then1-2, then2-2]


  • 拿出微任务队头任务 then1-2,输出 then1-2


  • 拿出微任务队头任务then2-2,输出then2-2


  • 微任务执行完,执行宏任务中的 setTimeout,输出 setTimeout


最后输出的结果:


script start -> promise1-1 -> script end -> then1-1 -> promise2-1 -> then2-1 -> then1-2 -> then2-2 -> setTimeout


async / await


  • async 隐式返回 Promise 作为结果。


  • 执行完 await 后直接跳出 async 函数,让出执行权。


  • 当前任务的其他代码执行完之后再次获得执行权进行执行。


  • 立即 resolve 的 Promise 对象是在本轮事件循环的结束时执行,不是在下一轮事件循环的开始。


举栗


console.log('script start') async function async1() {   await async2();   console.log('async1 end') } async function async2() { console.log('async2 end') } setTimeout(() => {   console.log('setTimeout') }) new Promise((resolve, reject) => {  console.log('promise')  resolve(); }) .then(() => {   console.log('then1') }) .then(() => {   console.log('then2') }) async1() console.log('script end')


按照之前的分析得出结果:


script start -> promise -> async2 end -> script end -> then1 -> async1 end -> then2 -> setTimeout


Node 中的 Event Loop


与浏览器中的事件循环类似, Node 中也有宏任务与微任务。它们的不同点是:Node 有多个宏任务队列,浏览器只有一个宏任务队列。


Node 的架构底层是基于 libuv 实现的,它是 Node 新跨平台抽象层,libuv 使用异步,事件驱动的编程方式,核心是提供 I/O 的事件循环和异步回调。


通过它可以去调用一些底层操作,Node 中的 Event Loop 功能就是在 libuv 中封装实现的。


宏任务与微任务


  • 宏任务

        

        1. setImmediate


  • 微任务


        1. process.nextTick


事件循环机制六个阶段

一文读懂 Event Loop

  • timers:执行 setTimeout 和 setInterval 中到期的 callback。


  • pending callback:上一轮循环中少数 callback 会放在这一阶段执行。


  • idle,prepare:仅在内部使用。


  • poll:最重要的阶段,执行 pending callback,在适当的情况下会阻塞在这个阶段。


  • check:执行 setImmediate 的 callback。其中 setImmediate 是将事件插入到事件队尾,主线程和事件队列的函数执行完成后立即执行 setImmediate 指定的回调函数。


  • close callback:执行 close 事件的 callback,如:socket.on('close', [, fn]) 或 http.server.on('close', fn)。


轮循顺序


执行的轮循顺序,每阶段都要等到对应的宏任务队列执行完毕才会进入到下一阶段的宏任务。


  • timers


  • I/O callbacks


  • poll


  • setImmediate


  • close events


每两个阶段之间执行微任务队列。


Event Loop 过程


  • 执行全局 script 同步代码。


  • 执行微任务队列,先执行所有 nextTick 队列中的所有任务,再执行其他的微任务队列中的所有任务。


  • 开始执行宏任务,共六个阶段,从第一个阶段开始执行自己宏任务队列中的所有任务。(浏览器是从宏任务队列中取第一个执行)


  • 每个阶段的宏任务执行完毕后,开始执行微任务。


  • TimersQueue > 步骤 2 > I/O Queue > 步骤 2  > Check Queue > 步骤 2 > Close Callback Queue > 步骤 2 > TimersQueue ...


setImmediate 与 setTimeout 区别


setImmediate 与 setTimeout 很相似,但根据它们被调用的时间以不同的方式表现。


  • setImmediate 用于在当前 poll 阶段完成后 check 阶段执行脚本。


  • setTimeout 在经过最小 (ms) 后运行的脚本,在 timers 阶段执行。


举栗


setTimeout(() => {  console.log('setTimeout')}, 0)setImmediate(() => {  console.log('setImmediate')})


执行定时器的顺序将根据调用它们的上下文有所不用。如果主模块中调用两者,那么时间将受到进程性能的限制。其结果也不一样。


如果在 I/O 周期内移动两个调用,则始终首先执行立即回调。


const fs = require('fs');fs.readFile(__filename, () => { setTimeout(() => {    console.log('setTimeout'); }, 0); setImmediate(() => {    console.log('setImmediate'); });});


结果是:setImmediate > setTimeout。


原因是在 I/O 阶段读取文件后,事件循环会进入 poll 阶段,发现有 setImmediate 需要执行,会立即进入 check 阶段执行 setImmediate。


接着再进入 timers 阶段,执行 setTimeout,输出 setTimeout。


Process.nextTick


虽然它是异步 API 的一部分,但不是事件循环的一部分。


process.nextTick 将 callback 添加到 next tick 队列,一旦当前事件轮循队列的任务全部完成,在 next tick 队列中的所有 callback 会被依次调用。


举栗


let bar;
setTimeout(() => { console.log('setTimeout');}, 0)
setImmediate(() => { console.log('setImmediate');})function someAsyncApiCall(callback) { process.nextTick(callback);}
someAsyncApiCall(() => { console.log('bar', bar); // 1});
bar = 1;


在 Node v10 中上述代码执行可能有两种答案。


一种为:bar 1 > setTimeout > setImmediate。


一种为:bar 1 > setImmediate > setTimeout。


无论哪一种,始终都是先执行 process.nextTick,输出 bar 1。


总结


从以上探讨得出:浏览器中的 Event Loop 和 Node 中的 Event Loop 是不同的,实现机制也不同。它们的宏任务与微任务包括和事件循环也略有差异。



———— / END / ———



参考资料



「1」 JS 浏览器事件循环机制

「2」什么是浏览器的事件循环(Event Loop) 

「3」一次弄懂 Event Loop

「4」浏览器与 Node 的事件循环有何区别

「5」你不知道的 Event Loop

「6」JavaScript 运行机制详解:再谈 Event Loop


一文读懂 Event Loop






喜欢本文,点个“在看”告诉我

以上是关于一文读懂 Event Loop的主要内容,如果未能解决你的问题,请参考以下文章

一文读懂Guava EventBus(订阅发布事件)

浏览器和Node不同的事件循环(Event Loop)

一文读懂PyTorch张量基础(附代码)

一文读懂TensorFlow(附代码学习资料)

一文读懂层次聚类(Python代码)

一文读懂Java注解