美文网首页
Event Loop 原来是这么回事

Event Loop 原来是这么回事

作者: lentoo | 来源:发表于2019-08-19 10:20 被阅读0次

    前沿

    从前有座山,山里有座庙,庙里有个小和尚在讲故事、讲什么呢?讲的是:

    从前有座山,山里有座庙,庙里有个小和尚在讲故事、讲什么呢?讲的是:

    从前有座山,山里有座庙,庙里有个小和尚在讲故事、讲什么呢?讲的是:

    ...

    请看一个小故事

    image

    以前有一个餐厅,这个餐厅有一个老板和一个厨师,自己创业的,刚开始起步阶段,没有资金请员工,所以自己来当老板兼服务员。

    来这家餐厅的顾客有这四种类型

    1. 超级VIP客户
    2. VIP客户
    3. 普通的客户
    4. 赖账的客户

    作为VIP顾客,肯定得有VIP特权。

    • 优先上菜
    • 同等VIP,先点菜的先上菜

    所以这个店的上菜顺序跟身份和点菜的顺序有关,

    超级VIP客户 > VIP客户 > 普通客户 > 赖账的客户

    一天、老板开始营业后,陆陆续续的来了一些人进来点餐吃饭。

    • 第一个进来的是普通的客户,点了一道回锅肉

    • 第二个进来的是充了钱的VIP客户,点了一道小龙虾

    • 第三个进来的是赖账的客户,点了很多菜

    • 第四个进来的是一个超级VIP客户,点了一道酸菜鱼

    由于这个店只有一个人,所以老板招待好他们点完餐之后就去炒菜了。根据上面提到的顺序,所以会先炒超级VIP客户点的菜、然后到VIP客户点的菜、然后到普通客户点的菜、最后到赖账的客户点的菜

    让我们用伪代码看看如何实现这个逻辑

    image

    我们定义了四个function

    • superVipOrder(name, dish) 用来表示超级VIP用户下单点菜
    • vipOrder(name, dish) 用来表示VIP用户下单点菜
    • order(name, dish) 用来表示普通用户下单点菜
    • groupOrder(name, dish) 用来表示赖账的客户下单点菜
    image

    根据上面提到的上菜规则,

    超级VIP客户 > VIP客户 > 普通客户 > 赖账的客户

    实际的上菜顺序我们可以知道是

    image

    那么问题来了,那些function都是什么呢?

    其实很简单,这些function都对应着JavaScript中的一些异步函数

    // 超级VIP客户
    // 微任务,将回调加入到  执行队列,优先执行
    function superVipOrder(name, dish, order) {
      process.nextTick(() => {
        console.log(red(`superVip顾客 ${name} 点了 ${dish} 已经上了`));
      });
    }
    
    // VIP客户
    // 微任务,将回调加入到  执行队列,优先执行,优先级比process.nextTick低
    function vipOrder(name, dish, order) {
      new Promise((resolve, reject) => {
        resolve(true);
      }).then(() => {
        console.log(blue(`vip顾客 ${name} 点了 ${dish} 已经上了`));
      });
    }
    
    // 普通客户下单
    // 宏任务,将回调加入到 宏任务的执行队列中
    function order(name, dish, order) {
      setTimeout(() => {
        console.log(yellow(`普通顾客 ${name} 点了 ${dish} 已经上了`));
      }, 0);
    }
    
    // 赖账的客户下单
    function groupOrder(name, dish, order) {
      setImmediate(() => {
        console.log(green(`赖账的顾客 ${name} 点了 ${dish} 已经上了`));
      });
    }
    

    我们可以暂且先把 process.nextTick 认为是超级vip用户,优先级最高、

    原生Promise认为是vip用户,执行优先级高

    setTimeout 认为是普通用户,执行优先级一般

    setImmediate 认为是赖账的顾客,执行优先级低

    还原伪代码

    我们将伪代码还原成这些异步函数,这会让我们看的更加直观、亲切一些


    image

    根据上面故事提到的优先级规则,我们知道输出的结果是这样的

    image

    为什么会是这样的结果呢?下面就来讲讲JavaScript中的Event Loop

    Event Loop

    1. JavaScript的事件循环

    image

    我们知道 JavaScript是单线程的,就像上面故事的老板,他得去服务员去招待客人点菜,并将菜单给厨师,厨师炒好后再给到他去上菜。如果老板不请个厨师,自己来炒菜的话,那么在炒菜时就没办法接待客人,客人就会等待点菜。等着等着就会暴露出服务态度不行的问题。所以说,得有厨师专门处理炒菜的任务

    所以在js中,任务分为同步任务和异步任务,

    • 同步任务 -> 服务员去接待客人点菜
    • 异步任务 -> 厨师炒菜、异步回调函数相当于 服务员去上菜
    image

    JS的事件循环如图所示,

    1. 在执行主线程的任务时,如果有异步任务,会进入到Event Table并注册回调函数,当指定的事情完成后,会将这个回调函数放到 callback queue
    2. 在主线程执行完毕之后,会去读取 callback queue中的回调函数,进入主线程执行
    3. 不断的重复这个过程,也就是常说的Event Loop(事件循环)了

    2. 异步任务

    异步任务又分为宏任务跟微任务、他们之间的区别主要是执行顺序的不同。

    在js中,微任务有

    • 原生的Promise -> 其实就是我们上面提到的VIP用户

    • process.nextTick -> 其实就是我们上面提到的超级VIP用户

    process.nextTick的执行优先级高于Promise

    宏任务

    • 整体代码 script
    • setTimeout -> 其实就是我们上面提到的普通用户
    • setImmediate -> 其实就是我们上面提到的群体用户

    setTimeout的执行优先级高于 setImmediate

    宏任务与微任务的执行过程

    在一次事件循环中,JS会首先执行 整体代码 script,执行完后会去判断微任务队列中是否有微任务,如果有,将它们逐一执行完后在一次执行宏任务。如此流程

    image

    测试

    下面我们来看一段代码是否了解了这个流程

    <script>
        setTimeout(() => {
          console.log('a');
          new Promise( res => {
            res()
          }).then( () => {
            console.log('c');
          })
          process.nextTick(() => {
            console.log('h');
          })
        }, 0)
        console.log('b');
        
        process.nextTick( () => {
          console.log('d');
          process.nextTick(() => {
            console.log('e');
            process.nextTick(() => {
              console.log('f');
            })
          })
        })
        
        setImmediate( () => {
          console.log('g');
        })
    </script>
    

    执行结果为:b d e f a h c g

    让我们来分析一下这段代码的执行流程

    1. 首页执行第一个宏任务 整段script标签代码,遇到第一个 setTimeout,将其回调函数加入到任务队列中,

    2. 输出 console.log('b')

    3. 遇到process.nextTick,将其回调函数加入到任务

    4. 遇到setImmediate 将其回调函数加入到任务队列中

    宏任务Event Queue 微任务Event Queue
    setTimeout process.nextTick
    setImmediate
    1. 当第一个宏任务执行完后,就会去判断是否还有任务,刚好有一个 任务,执行process.nextTick的回调,输出 console.log('d'),然后又遇到了一个process.nextTick,又将其放入到任务队列
    2. 继续将任务队列中的回调函数取出,继续执行,输出 console.log('e'),然后又遇到了一个process.nextTick,又将其放入到任务队列
    3. 继续将任务队列中的回调函数取出,继续执行,输出 console.log('f'),然后又遇到了一个process.nextTick,又将其放入到任务队列
    宏任务Event Queue 微任务Event Queue
    setTimeout
    setImmediate
    1. 当微任务队列为空后,开始新的宏任务,取出第一个宏任务队列的函数,setTimeout,执行 console.log('a'),然后遇到Promiseprocess.nextTick 将其回调加入到任务队列。执行完后
    宏任务Event Queue 微任务Event Queue
    setImmediate promise.then
    - process.nextTick
    1. 继续判断任务队列是否有回调函数可执行,由于process.nextTick的执行优先级大于promise,所以会先执行process.nextTick的回调,输出 console.log('h');、如果有多个process.nextTick的回调,会将process.nextTick的所有回调执行完成后才会去执行其它任务的回调。
      当nextTick所有的回调执行完后,执行promise的回调,输出console.log('c');,直到promise的回调队列执行完后,又会去判断是否还有任务。
    宏任务Event Queue 微任务Event Queue
    setImmediate
    1. 微任务执行完后,开始执行新的宏任务,执行setImmediate的回调,输出 console.log('g');

    3. setImmediate

    这里为什么要把 setImmediate 单独拿出来说呢,因为它属于宏任务的范畴,但又有点不一样的地方。

    先看一段代码

    image

    按照我们上面的分析逻辑,我们会认为这段代码的输出结果应该是a b c d
    如果我们把使用Node 0.10.x的版本去执行这段代码,结果确实是输出a b c d

    然而,在Node 大于 4.x 的版本后,在执行setImmediate的,会使用while循环,把所有的immediate回调取出来依次进行处理。

    ps:在 node 11版本以上,已经把setImmediate按照宏任务的执行顺序进行处理了,不会循环依次进行调用了

    image

    最后看一段代码看看自己是否真的掌握了

    如果还没有掌握,欢迎评论区吐槽

    image
    <script>
       console.log("start");
       process.nextTick(() => {
         console.log("a");
         setImmediate(() => {
           console.log("d");
         });
         new Promise(res => res()).then(() => {
           console.log("e");
           process.nextTick(() => {
             console.log("f");
           });
           new Promise(r => {
             r()
           })
           .then(() => {
             console.log("g");
           });
           setTimeout(() => {
             console.log("h");
           });
         });
       });
       
       setImmediate(() => {
         console.log("b");
         process.nextTick(() => {
           console.log("c");
         });
         new Promise(res => res()).then(() => {
           console.log("i");
         });
       });
       console.log("end");
    </script>
    

    输出的结果为: start end a e g f h b d c i

    简单分析一下代码:

    1. 第一轮事件循环开始,执行script代码,输出 start end,将process.nextTick 的回调加入微任务队列中,将setImmediate的回调加入到宏任务的队列中
    2. 执行微任务队列中的process.nextTick的回调,输出 a 、将setImmediate的回调加入到任务的队列中,遇到promise、将回调加入到任务队列中。
    宏任务 微任务
    setImmediate promise.then
    setImmediate -
    1. 继续执行微任务队列中的回调,取出promise.then并执行,输出e,将process.nextTick的回调放入到微任务中,遇到promise、将回调加入到任务队列中。
    2. 判断当前promise的回调队列是否还有回调函数没执行,如果有,将继续执行,取出刚刚放入的promise的回调,输出 g,当Promise回调队列执行完后,继续判断当前是否还有微任务。
    3. 取出process.nextTick的回调并执行,输出f
    宏任务 微任务
    setImmediate -
    setImmediate -
    setTimeout -
    1. 当前微任务队列为空后,开始执行宏任务,因为setTimeout的优先级大于setImmediate,所以先取出setTimeout的回调并执行,输出h
    2. 当前微任务队列还是为空,开始执行宏任务,取出所有setImmediate的回调函数,并执行,输出b d,将 process.nextpromise的回调放入到微任务队列中。
    3. 取出微任务队列中的回调函数,并执行,输出 c i

    总结

    Event Loop 作为面试的高频题,静下心来认真的分析一下,其实不难理解。

    原文链接:https://ccode.live/lentoo/list/3

    欢迎关注

    欢迎关注公众号“码上开发”,每天分享最新技术资讯

    image

    相关文章

      网友评论

          本文标题:Event Loop 原来是这么回事

          本文链接:https://www.haomeiwen.com/subject/cszjsctx.html