从一道执行题,了解Node中JS执行机制

1,117 阅读5分钟

与浏览器环境有何不同

node环境和浏览器环境,表现出来的事件循环状态,大体表现一致 唯一不同的是:

  1. JS引擎存在 monitoring process 进程,会持续不断的检查主线程执行为空,一旦为空,就会去 callback queue 中检查是否有等待被调用的函数。(只有宏任务和微任务两个队列)
  2. node 中是依靠 libuv 引擎实现,我们书写的 js 代码有 V8 引擎分析后去调用对应的 nodeAPI ,这些 api 最后由 libuv 引擎驱动,在 libuv 引擎中有一套自己的模型,把不同的事件放在不同的队列中等待主线程执行。( 模型中有6种宏任务队列和1种微任务队列 )

Node事件循环的几个阶段

// libuv引擎中的事件模型,在每个模型后面都添加了一些说明

   ┌───────────────────────────────────────────────────────┐
┌─>│        timers       │ setTimeout/setInterval的回调
│  └──────────┬────────────────────────────────────────────┘
│             ↓
│  ┌──────────┴────────────────────────────────────────────┐
│  │   pending callbacks │ 处理网络、流、tcp的错误回调
│  └──────────┬────────────────────────────────────────────┘
│             ↓
│  ┌──────────┴────────────────────────────────────────────┐
│  │     idle, prepare   │ 只在node内部使用
│  └──────────┬────────────────────────────────────────────┘
│             ↓                                                      ┌───────────────┐
│  ┌──────────┴────────────────────────────────────────────┐         │   incoming:   │
│  │         poll        │ 执行poll中的i/o队列,检查定时器是否到时  <------│   connections,
│  └──────────┬────────────────────────────────────────────┘         │   data, etc.  │
│             ↓                                                      └───────────────┘
│  ┌──────────┴────────────────────────────────────────────┐
│  │        check        │ 存放setImmediate回调
│  └──────────┬────────────────────────────────────────────┘
│             ↓
│  ┌──────────┴────────────────────────────────────────────┐
└──┤    close callbacks  │ 关闭的回调(socket.on('close')...)
   └───────────────────────────────────────────────────────┘

Node事件循环中的几个阶段

官方的event-loop-timers-and-nexttick更详细的说明

  1. times:这个阶段执行定时器队列中的回调函数 ( setTimeoutsetInterval )
  2. pending callback:这个阶段执行几乎所有的回调( 网络、流、tcp错误... )。除了,close 回调、定时器回调、setImmediate 回调这3个规定好的阶段
  3. idle,prepare:这个阶段仅在内部使用( 可以暂不理会 )
  4. poll:等待新的I/O事件,node在特殊情况下会阻塞这里,检查定时器是否到时( 入口 )
  5. checksetImmediate() 的回调会在这个阶段执行
  6. close callbacks:例如 socket.on('close', ...)
  7. process.nextTick.then() 会在事件循环的阶段切换过程中执行

说了一堆概念,来一起看看下面这段代码

(function test() {
  setTimeout(function () { console.log(4) }, 0);
  new Promise(function (resolve, reject) {
    console.log(1);
    for (var i = 0; i < 10000; i++) {
      i == 9999 && resolve();
    }
    console.log(2);
  }).then(function () {
    console.log(5);
  });
  console.log(3);
})();
// 这段代码是不是很熟悉
// 最终结果1,2,3,5,4 和 浏览器中效果一致

来点稍微高难度的

和上篇博客 从一道执行题,了解浏览器中JS执行机制 中的代码一样 (⊙﹏⊙)b

console.log(1)

setTimeout(() => {
  console.log(2)
  new Promise(resolve => {
    console.log(4)
    resolve()
  }).then(() => {
    console.log(5)
  })
})

new Promise(resolve => {
  console.log(7)
  resolve()
}).then(() => {
  console.log(8)
})

setTimeout(() => {
  console.log(9)
  new Promise(resolve => {
    console.log(11)
    resolve()
  }).then(() => {
    console.log(12)
  })
})
// 浏览器中的结果:1、7、8、2、4  , 5、9、11、12
// Node 中的结果:1、7、8、2、4  , 9、11、5、12

解析如下:

  1. 在浏览器中 macro task 执行完成后,再次循环 宏任务 的回调队列之前,会优先处理micro中的任务。因此结果是: 1、7、8、2、4、5、9、11、12
  2. Node 中有6个宏任务队列,事件循环首先进入 poll 阶段。进入 poll 阶段后查看是否有设定的 timers ( 定时器 )时间到达,如果有一个或多个时间到达, Event Loop 将会跳过正常的循环流程,直接从 timers 阶段执行,并执行 timers 回调队列,此时只有把 timers 阶段的回调队列执行完毕后。才会走下一个阶段,这也就是为什么 setTimeout 中有 .then,而没有被立即执行的原因,当 timers 阶段的回调队列执行完毕后,切换到下一个阶段这个过程中去触发 微任务(process.nextTick.then) 。在阶段与阶段的切换之间。

再来一道基础题

setTimeout(function () {
  console.log('setTimeout')
});
setImmediate(function () {
  console.log('setImmediate')
});

执行结果:( setTimeout、setImmediate ) 或 ( setImmediate、setTimeout )

为什么? setTimeout 在标准中默认的最小时间是4ms,如果开启node和执行node代码的时间小于4ms,那么代码解析完成后传入 libuv 引擎,首先会进入 poll 阶段,此时查看设定的时间是否达到截止时间点,如果这个时间小于4ms( 没有达到 ),那么会走 check 阶段,会触发 setImmediate 再触发 setTimeout。如果开启node和执行node代码时间大于等于4ms,那么就会先执行 setTimeout 后执行 setImmediate

在基础上进化的经典题

setImmediate(() => {
  console.log('setImmediate1')
  setTimeout(() => {
    console.log('setTimeout1')
  }, 0);
})
setTimeout(()=>{
  process.nextTick(()=>console.log('nextTick'))
  console.log('setTimeout2')
  setImmediate(()=>{
    console.log('setImmediate2')
  })
},0);

两种情况 ( nextTick执行的位置:是在队列切换时执行 )

  1. 如果 setImmediate 先执行:setImmediate1、setTimeout2、setTimeout1、nextTick、setImmediate2
  2. 如果 setTimeout 先执行:setTimeout2、nextTick、setImmediate1、setImmediate2、setTimeout1

setImmediate和process.nextTick的直译

  1. Immediate立即执行的意思,其实际上是固定在 check 阶段才会被执行。这个直译的意义和 process.nextTick 才是最匹配的。
  2. node的开发者们也清楚这两个方法的命名上存在一定的混淆,他们表示不会把这两个方法的名字调换过来---因为有大量的node程序使用着这两个方法,调换命名所带来的好处与它的影响相比不值一提。

了解这些东西有什么用?

  1. 可以使我们对异步代码的执行顺序有清晰的认知( 重要的 )
  2. 推迟任务执行
  3. 面试

总结

这些概念远比想象中的要重要

  1. 为什么 new Promise 第一个参数是同步执行的 ?学习Promise && 简易实现Promise
  2. 浏览器 中的 JS 执行机制是什么样子的?从一道执行题,了解浏览器中JS执行机制

附:这篇博客 也许 想表达 概念远比想象中的要重要 (⊙﹏⊙)b