我是这样理解EventLoop的荐

我是这样理解EventLoop的

一、前言

  众所周知,在使用javascript时,经常需要考虑程序中存在异步的情况,如果对异步考虑不周,很容易在开发中出现技术错误和业务错误。作为一名合格的jI p q [ l X W 2avascript使用者,了解异步的存在和运行机制十分重要且有必要;那么,异步究竟是何方神圣呢?我们不得不提Event Loop:: s 6 Y叫做事件循环,是指浏览器或Node环境的一种解决javaScrip7 ` t单线程运行时不会阻塞的一种机制,也就是实现异步的原理W x v r M Y。作为一种单线程语言,javascript本身是没有异步这一说法的,是由其宿主w t x d l t环境提供的(EventLoop优秀文章网上有很多,这篇文章3 9 y是自己的整合和理解)。
注意:Event Loop 并不是在 ECMAScript 标准中定义的,而是在 HO 1 -TML 标准中定义的;

二、Event Loop知识铺垫

  javascript代码运行时,任务被分为两种,宏任务(MacroTask/Task)微任务(MircoTask)Event Loop在执行和协调各种任务时也将任务队列分为Task QueueMircoTN D o z Y , g L {ak Queue分别对应管理宏任务(MacroTask/Task)微任务(MircoTask);作为队列: 3 nTask QueueMircoTak Queue也具备队列特性:先进先出(FIFO—first in first o@ O X 6 yut)

1、微任务(MircoTask)

~ 3 m w | m R 在 HTML 标准中,并没有明确规定 Microtask,但是实际开发中包含以下四种:

  • Promise E , ^ g k d i ne中的then、catch、finally(原理参考:【js进阶】手撕PrQ * e N j 6omise,一码一解析 包懂)
  • MutationObserver(监视 DOM 变动的API,详情参考MDN)
  • Object.observe(废弃:监听标准= . B对象的变化)
  • Process.nextTick(Node环境,通常也被认为是微任务)

    2v ~ ~ g =、宏任务(MacJ ] hroTask/Task)

      基本上,我们将javascript中非微任务(MircoTask)的所有任务都归为宏任务,比如:& u e z / ^ J T [

  • script中全部代码
  • DOM操作
  • 用户交互操作
  • 所有的网路请求
  • 定时器相关的 setTimeout、setInterval 等
  • 3、javascript runtime

      javascr* - | 5ipt runtime:为 JavaScript 提供一些对象或机制,使它能够与外界交互,是javascript的执行环境。j! I b G 3 i T ` ]avascript执行时会创建一个main thread主线程call-stack 调用栈(执行栈,遵循后进先出的规E 5 ] } J则)所有的任务都会被放到调用栈/执行栈等待主线程执行。其运行机制如下:
    我是这样理解EventLoop的荐

  • 1)主线程自上而下依次执行所有代码;
  • 2)同步任务直接进入到主线程被执行;
  • 3)异步k S t M m S 5 g _任务进入到Event Table,当异步任务有结果后,将相对应的回调函数进行注册,放入Event Queue
  • 4)主线程任务执行完空闲下来后,从Event Queue(FIFO)中读取任务,放入主线程执行;
  • 5)u | m q@ q ?入主线程的Event Queue任务继续从第一步开始,如此循环执行;
    上述步骤执行过程就是我们所说的事件循环(Event Loop),上图展示了事件循环中的一个完整循环过程。

    三、浏览器环境的Event LT % V S , Zoop

      不同的执行环境中,Event Loop的执行机制是不同的;例如Chrome 和 Node.js 都使用了 V8 Engine:V8 实现并提供了 ECMAScript 标准中的所有数据类型j Z h 4 @ j D ^ h、操作符、对象和方法(注意并没有 DOM)。但它们的 Runtime 并不一样:Chrome 提供了 window、DOM,j 9 P W e ^ g q而 Node.js 则是 require、process 等等。我们在了解浏览器中Event Loop的具体表现前需要先整理同步、异步、微任务、宏任务之间的关0 8 ; z 6系!

    1、同步、异步 和 宏任务、微任务

      看到这里,可能会有很多疑惑:同步异步很好理解,宏任务微^ ! $任务上面也进行了分类,但是当他们四个在一起后就感觉很混乱了,冥冥之中觉得同步异步和宏任务微任务有内在联系,但是他们之间有联系吗?又是什么联系呢?网上有的文章说N j p X E宏任务就是同步的,微任务就是异步的 这种说法明显是错的!
      其实我更愿意如此描述:宏任务和微任务是相对而言的,根据代码执时循环的先后,将代码执行分层理解,在每一层(一次)的事件; : 0 $ j / 6 c ^循环中,首先整体代码块看作一个宏任务,宏任B | + p j q 5务中的( } % o M H Q X Promi, L gse(then、catch、finally)、MutationObserL s @ ! Fver、Process.nextTick就是该宏任务层的微任- c m e q X务;宏任务中的同步代码进入主线程中立即执行的,宏任务中的非微任务异步执行代码将作为下一次i z ` j x ^ $ p T循环的宏任务时进入调用栈等待执行的;此时,调用栈中等( h j y ` n待执Q E Q行的队列分为两种,优先级较高先执行的本层循环微任务队列(MicroTask Queue),和优先级低的下层循环执行的宏任务队列(MacroTask Queue)!
    注意:每一次/层循环,都是首先从宏任务开始,微任务结束;
    我是这样理解EventLoop的荐

    2、简单实例分析

    上面的描叙相对拗口,结合代码和图片分2 3 @析理解:

我是这样理解EventLoop的荐

  答案暂时不给出,我们先进e 1 n E w J行代码分析:这是一个简单而典型的双层循环事件循环执行案例,在这个循环中可h U j以按照以下步骤进行分析:

  • 1、首先区分出该层宏任务的范围(整个代码);
  • 2` : c、区分宏任务同步代码异步代码
    同步代码:console.log('script start');console.log('enter promise');console.log('script end');
    异步代码块:setTimeoutPromise的then注意Promise中只有then、catch、fiN znally的执行需要等到结果,Promise传入的回调函数属于同] V _ : . x M步执行代码);
  • 3、在异步中找出同层的微任务(代码中的PromiseR @ 7 e _ l . U o的then)和下层事件循环的宏任务(代码中的setTimeout
  • 4N ? * m / !宏任a o [ J t Q 4 G同步代码优先进入主线程,按照自上而下顺序执行完毕;
    输出顺序为:
    //同步代码执行输) S V D V出
    script start
    entr O % = ! *er p: g s % d Y J 3 &romise
    script8 E b 7 end
  • 5、当主线程空闲时,执行该层的K a U E J N微任务
    //同层微任务队列代码执行输出
    promise then 1
    promiA 5 F se then 2
  • 6、首层事件循环结束,进入第二层事件循环(seg & n U f _ ,tTimeout包含的执行代码,只有一个同步代码)
    //第二层宏任务队列代码执行输出
    setTimeout

    综合分析最终得出数据结果为:

    //首层宏任务代码执行输出
    script start
    enter promise
    script end
    //首层微任务队列代码执行输出
    promise then 1
    promise then 2
    //第二层宏任务队列代码执行输出
    setTimeout

    3、复杂案例分( n 1 h

      那么,你是否已经了解1 & q上述执行D r K P过程了呢?如果完全理解上述实例,说明= 8 s q ~ G g 3你已经大概知道浏览器中EK G D t g r 3vent Loop的执行机制,但是,要想知道自己是不是完全明白,不妨对于下列多循环的事件循环进行分析检验,给出你的结果:

    console.log('1');
    setTimeout(function() {
    cons* g D ) r q  Nole.log('2');
    new Promise(fuI ] S { m * Knction(resolve) {
    console.a X 6log('3');
    resolve();
    }).then(function() {
    console.log('4')
    })
    setTimeout(function() {
    console.log('5');
    new Promise(functiV C k 3 n ; G $ )on(resolve) {
    coS o p r - l a  insole.log('6');
    resolve()1 H e  v : [ c;
    }).thenW 6 d {(function() {
    coW * U H & % `nsole.log('7')
    })
    })
    conso& { 2 D c [ UlA / h e m B me.log('14');
    })
    new Promise(function(resolve) {
    console.log('8');
    resolve();
    }).then(function() {
    console.log('9')
    })
    setTimeout(function() {
    console.log('10');
    new Promise(function(resolve) {
    console.log('1P 8 h ) = # B1');
    resolve();
    }).then(function() {
    console.logo . L('12')
    })
    })
    cons, j % x !ole.log('L T  o13')

    分析:如下图草& t # D 2 稿所示,左上u K 1 } _ z角标a为宏任务队列,/ k J 9 } &左上角标i为F ! Z o } t E Z g微任务队列,同一层循环中,本层宏任务先执行,再执行微任务;本层宏任务中的非微任务异i V * | @ r步代码块作为下层循环的宏任务进入下次循环,如此循环执行;
    我是这样理解EventLoop的荐

如果你的与下面的结果一致,恭喜你浏览器环境的Event Loop你已经完全掌握,那么请开始下面的学习:

1->8->13->9->2->3->14->4-&) ( ;gt;1B b ] 9 50->11->12->5->6->7

四、Node 环境下的 Event Loop

  在Node环境下,浏览器的EventLoop机制并不适用,切记不能混为一谈。这里借用网上很H 5 v 9多博客上的一句总结(其实我也是真不太懂):Node中的Event Loop是基于libuv实现的:# { O E i # } q PlibuvNode 的新P ! T G ^ q & q跨平台抽象层,libuv使用异步,事件驱动的编程方} ? [ D { U V #式,核心是提供i/o的事件循环和异步回调。libuvAPI包含有时间,非阻塞的网络,异步文件操作,子进程等等。

1、Event Loop的6阶段

我是这样理解EventLoop的荐

  Node的Event loop6 f 8 q一共分为6个阶段,每个细节具体. h ; m b P Q如下:

  • timers: 执行setTimeout和setInterval中到期的callback。
  • pending callback: 上一轮循环中少数的callback会放在这一阶段执行。
  • idle, prepare:仅在内部使用。
  • polc I 0 x ~ Ml:最重要的阶段,执行pends i eing callback,在适当的情况下回阻塞z F 5 C X在这个阶t % R j 4 q 6段。
  • check:执行setImmediatey | 2 $ 6 ~ S j 6的callback。
  • close callbacks: 执行close事件的callback,例如socket.on('close'[,fn])或者http.server.on('close, fn)。
    注意:上面六个阶段都不包括 process.nextTick()
    我是这样理解EventLoop的荐

重点:如上图所,在Node.js中,一次宏任务可以认为是包含上述6个阶段、微任务microtask会* ~ 2 o K 4在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。

2、process.nextTick()

  在第二节中就了解到,process.nextTick()属于微任K - v务,但是这里需要重点提及下:

  • process.nextTick()虽然它; | x是异步D b *API的一部分,但未在图中显示。因为process.nextTick()从技术上讲n t ?,它不是事件循环的一部分;
  • 当每个阶段完成后,如果存在 nextTick,就会1 b s F y x ^ A清空队列中的所有回调函数,并且优先于其: h : V $他 mics & 7 ( 4 ,rot0 Z cask 执行(可以理解为微任务中优先级最高的

    3、实例分析

      老规矩,线上代码:

    console.log('1');
    setTimeoJ ; 1 X h dut(function() {
    console.log('2');
    process.nextTick(function() {
    console.log('3');
    })
    ne6 A ) 1w Pe Z !roB h hmise(functik 6 bon(resolve) {
    console.log('4');
    resolve();
    }).then(functionv @ n() {
    console.log('5')
    })
    })
    proces6 X v G R U !s.nextTick(ft ] u h :unction() {
    console.log('6');
    })
    new Promise(function(resolve) {
    console.log('7');
    rp } 5esolve();
    }L 0 ) M R A c W).then(function()w J , 6 a {
    console.log('8')
    })
    setTimeout(function() {
    consolR N I # ) Ae.log('9');
    process.nextTick(function() {
    console.log('10');
    })
    new Promise(function(resolve) {
    cZ f G B v q * 0onsole.log('11');
    resolve();
    }).then(function() {
    console.log('12')
    })
    })
    consolz x $ ] ) K Ce.log('13')

    将代m o ^ d N / Q码的执行分区进行解释
    我是这样理解EventLoop的荐
    分析:^ + R # : D如下图草稿所示,左上角标a为宏任务队列,左上角标i为微任务队列左上角标t为timers阶段队列左上角标p为nextTick队列同一层循环中,本层宏任务先执行,再执行微任务;本层宏任务中的非微H q 8 k任务异步代码块作为下层循环的宏任X y + O K + U E 1务进入下P r H Y i次循环B F 0 E t S 0 P x,如此循环执行:我是这样理解EventLoop的荐

  • 1、整体代码可以看做宏任务,同步& o 5 N 6 d M代码直接进入主线程执行,输出1- D ! X l _,7,130 ` J a ! $ R,接着执行同层微任务且nex( ! * htTick优先# w V I 4执行输出6,8
  • 2、二层中宏任务中只存在setTimeout,两个setTimeout代码块依次进入$ Q i 4 # F + 0 {6阶段中的timer阶段t1、t2进入队列;代码等价于:
    setTimm D Y 4 8 peout(function() {
    console.log('2');
    processU E + 5 }.neQ O axtTick(function= g H ~() {
    console.log('3');
    })
    new Promise(funU | e t Oction(resolve) {
    console.log($ - # o 4 5 V z'4');
    resolve();
    }).then(function() {
    console.log('5')
    })
    })
    setU M V j / G | 2Timeout(functionK E x u ( X l() {
    console.log('9');
    process.nextTick(. ) @ / V rfunction() {
    console.log('10');) : 7 c 8 *
    })
    new Promise- 4 - 1 % h W(funct1 : - sion(resolve) {
    console.log('11');
    resolve();
    }).then(function() {
    console.log('12')
    })
    })
  • 3、setTimeout中的同步代码立即执行输z j i T V2,4,9,11nextTickPormise.then. S ! 2 7进入微任务执行输出3,10,5,12
  • 4、二层中不存在6* 9 B y J 2 6阶段中的其他阶段,循环完毕,最终输出结果为:1->7->13->6->8->2->4-&g# + ~ _ {t;9->11->3-&p r y + Q ( hgt;10->5->12

    4、当e ; , l堂小考

    console.log('1');
    setTimeout(function() {
    console.log('2');
    proces+ X  C Q ?s.nextTick(functio~ : ` X 6 R V w =n() {
    console.log('3');
    })
    new Promise(fU 1 $ j B K G } Punction(resolve) {
    cok w T i }nsole.log('4');
    resolve()z # 7 L b % ) 3;
    }).then(function() {
    console.log('5')
    sep x $ 7 6 H ltTimeout(function() {& V P 3 7
    console ! De.log('6');
    process.nextTick(function() {
    console.log('7');
    })
    new Promise(function(resolve) {
    ch 8 Console.log(m , , b'8');
    resolve();
    }).then(function()# Z ) [ { * Y h L {
    console.log('9')
    })
    })
    })
    })
    process.nextTick(funcH L r f h / y Ktion() {
    console.log('10');
    })
    new Promis) s b 3 . s V Y me(function(resolve) {
    console.log('1b C V ] # w1');
    resT ; G Q Wolve();
    }).then(function() {
    console.log('12')
    setTimeout(function() {
    console.log('13');
    process.nextTick(function() {
    console.log('14');
    })
    n| A F b oew Promise(funcX V = m W # vtion(resolve) {
    console.log('15');6 2 % P # O & / 8
    resolve();
    }).then(function() {
    console.log('16')
    })j 6 X 9 `
    })
    })
    setTimeout(function() {
    console.log? A b w = / ^ c('17');
    process.nextTick(function() {
    cM b # ( ` gonsole.log('18');
    })
    new Pl e H h y 9 ; $ 3romise(function(resolve) {
    console.log('19');
    resolve/ $ -();
    }).then(function() {
    console.log('20')
    })
    })
    console.log('21')

    五、总结

      浏览器NP w ~ E ! sode环境下,mi: ! 5crotask 任务队列的执行时机不同:Node 端,micr_ ^ % P n 3 Z z cotask 在事件循环的各个阶段之间执行;浏览器端,microtask 在事件循% ^ c ,环的 macrotask 执行完c n ` L _ b g _之后U @ X r b t D执行;

参考借鉴

  • 深入理解 JavaScript Evr / o - ! { 5 $ Vent Loop
  • 这一次,彻底弄懂 JavaScript 执行机制
  • 【THE LAST TIME】彻底吃透 JavaScript 执行机制