乐趣区

关于node.js:Pride深入解析-NodeJS-的微任务运行规则

前言

本文探讨的外围基本概念是 Event Loop,即事件循环,是 JavaScript 的执行模型,这是实现异步编程的外围。在不同的平台有不同的实现,浏览器基于 HTML5 的标准各自实现,而 NodeJS 基于 libuv 外围。他们尽管都是实现了异步告诉的成果,但运行规定还是有些差异。
网络上对于“事件循环”的文章有很多,本文就不再多赘述,请浏览本文 前准备好这些背景常识,重点是 NodeJS 运行的 6 个阶段以及宏工作队列(Macrotask Queue)和微工作队列(Microtask Queue)的基本概念。
自己能力无限,不足之处还请批评指教。

本文要解决的问题

  1. 在 NodeJS 中运行齐全局同步代码后是先运行微工作,还是先运行宏工作?
  2. 不同版本的 NodeJS 运行微工作的机会有差别吗?
  3. Next Tick Queue 和 Other Micro Queue 都是 NodeJS 中的微工作队列,他们的运行机会有什么差异?
  4. 微队列是一次清空,还是每一轮循环运行一个工作?

举个例子

console.log("start")

setTimeout(() => {console.log(1)
  Promise.resolve().then(() => {console.log(2)
  })
  setTimeout(() => {console.log(3)
  })
  setImmediate(() => {console.log(4)
  })
  process.nextTick(() => {console.log(5)
  })
})

setTimeout(() => {console.log(6)
  Promise.resolve().then(() => {console.log(7)
  })
  setTimeout(() => {console.log(8)
  })
  setImmediate(() => {console.log(9)
  })
  process.nextTick(() => {console.log(10)
  })
})

process.nextTick(() => {console.log(11)
})

console.log("end")

阐明

下面的例子根本能够解决上述的疑难,每一个回调函数都只会打印出一个数字,这个数字也相当于该函数的编号;全局的“start”和“end”表明了全局同步代码的运行开始与完结。

例子中属于宏工作的 API:

  • setTimeout()
  • setImmediate()

例子中属于微工作的 API:

  • process.nextTick() 会进入 Next Tick Queue
  • Promise 会进入 Other Micro Queue

不同 NodeJS 版本的运行后果

只选取稳固版本进行测试;为了节约篇幅,把本来竖的打印后果打横展现。
setTimeout 和 setImmediate 执行程序在同模块中是不肯定,因而“4 9”和“3 8”屡次运行后果可能会互调,因为这是属于宏工作的内容,这里不展开讨论。

v6.17.1

start end 11 1 6 5 10 2 7 4 9 3 8

v8.17.0

start end 11 1 6 5 10 2 7 4 9 3 8

v10.23.0

start end 11 1 6 5 10 2 7 4 9 3 8

v12.20.0

start end 11 1 5 2 6 10 7 4 9 3 8

v14.15.3

start end 11 1 5 2 6 10 7 4 9 3 8

后果剖析

由下面的后果,解答后面提出的问题。

  1. 因为 11 是紧跟在全局代码执行的,因而能够得悉,全局的同步代码运行完即会先开始微工作的运行。
  2. 不同版本的 NodeJS 运行是有差别的,以 v10.23.0 版本的后果为分界线,会有很显著的两个后果(通过进一步验证,这个变动从 v11.0.0 就开始)。至于产生这个不同的起因,将在上面详细分析。
  3. 以 v10.23.0 的后果来看,“5 10”是在“2 7”之前执行,因而可知 Next Tick Queue 是优先于 Other Micro Queue 的。
  4. 以 v10.23.0 的后果来看,“5 10”是在“2 7”是间断呈现的,因而可知微队列是一次清空。由同样间断呈现的“4 9”和“3 8”,也可知宏队列也有一次清空的性质,但他们不同 API 分属的队列不同。

剖析新旧版本的差别

以 v11.0.0 版本为分界线,高于或等于 v11.0.0 称为新版本;低于 v11.0.0 称为旧版本。
这里宏工作队列不是探讨重点进行了简化解决。

旧版本运行过程

  1. 运行全局同步代码:

    1. 打印“start”;
    2. 1 入宏工作队列;
    3. 6 入宏工作队列;
    4. 11 入微工作队列的 Next Tick Queue;
    5. 打印“end”。

    宏工作队列:[1, 6]
    微工作队列的 Next Tick Queue:[11]
    微工作队列的 Other Micro Queue:[]

  2. 运行微工作队列,输入 11。

    宏工作队列:[1, 6]
    微工作队列的 Next Tick Queue:[]
    微工作队列的 Other Micro Queue:[]

  3. 从宏工作队列取出 1 运行:

    1. 打印“1”;
    2. 2 入微工作队列的 Other Micro Queue;
    3. 3 入宏工作队列;
    4. 4 入宏工作队列;
    5. 5 入微工作队列的 Next Tick Queue。

    宏工作队列:[6, 3, 4]
    微工作队列的 Next Tick Queue:[5]
    微工作队列的 Other Micro Queue:[2]

  4. 从宏工作队列取出 6 运行:

    1. 打印“6”;
    2. 7 入微工作队列的 Other Micro Queue;
    3. 8 入宏工作队列;
    4. 9 入宏工作队列;
    5. 10 入微工作队列的 Next Tick Queue。

    宏工作队列:[3, 4, 8, 9]
    微工作队列的 Next Tick Queue:[5, 10]
    微工作队列的 Other Micro Queue:[2,7]

  5. 运行微工作队列:

    1. 清空 Next Tick Queue,打印“5”,“10”;
    2. 清空 Other Micro Queue,打印“2”,“7”。

    宏工作队列:[3, 4, 8, 9]
    微工作队列的 Next Tick Queue:[]
    微工作队列的 Other Micro Queue:[]

  6. 后续运行宏队列,在这里省略。

新版本运行过程

  1. 运行全局同步代码:

    1. 打印“start”;
    2. 1 入宏工作队列;
    3. 6 入宏工作队列;
    4. 11 入微工作队列的 Next Tick Queue;
    5. 打印“end”。

    宏工作队列:[1, 6]
    微工作队列的 Next Tick Queue:[11]
    微工作队列的 Other Micro Queue:[]

  2. 运行微工作队列,输入 11。

    宏工作队列:[1, 6]
    微工作队列的 Next Tick Queue:[]
    微工作队列的 Other Micro Queue:[]

  3. 从宏工作队列取出 1 运行:

    1. 打印“1”;
    2. 2 入微工作队列的 Other Micro Queue;
    3. 3 入宏工作队列;
    4. 4 入宏工作队列;
    5. 5 入微工作队列的 Next Tick Queue。

    宏工作队列:[6, 3, 4]
    微工作队列的 Next Tick Queue:[5]
    微工作队列的 Other Micro Queue:[2]

  4. 运行微队列:

    1. 清空 Next Tick Queue,打印“5”;
    2. 清空 Other Micro Queue,打印“2”。

    宏工作队列:[6, 3, 4]
    微工作队列的 Next Tick Queue:[]
    微工作队列的 Other Micro Queue:[]

  5. 从宏工作队列取出 6 运行:

    1. 打印“6”;
    2. 7 入微工作队列的 Other Micro Queue;
    3. 8 入宏工作队列;
    4. 9 入宏工作队列;
    5. 10 入微工作队列的 Next Tick Queue。

    宏工作队列:[3, 4, 8, 9]
    微工作队列的 Next Tick Queue:[10]
    微工作队列的 Other Micro Queue:[7]

  6. 运行微队列:

    1. 清空 Next Tick Queue,打印“10”;
    2. 清空 Other Micro Queue,打印“7”。

    宏工作队列:[3, 4, 8, 9]
    微工作队列的 Next Tick Queue:[]
    微工作队列的 Other Micro Queue:[]

  7. 后续运行宏队列,在这里省略。

差别总结

差别呈现在第四步,旧版本是从宏队列取出工作执行,而新版本是解决微工作。
于是咱们能够失去论断:旧版本会清空宏工作队列,再运行微工作;而新版本是每运行完一个宏工作,就去清空微工作队列。

额定补充

网上有流传微工作队列有深度限度的传说,如同说限度是 1000,在此验证一下。应用上面这个例子(一次性塞入 10000 个微工作):

console.log('start')

let a = 0
while (a < 10000) {process.nextTick(() => {console.log(111)
  })

  a += 1
}

setTimeout(()=>{console.log(123)
})

console.log('end')

在不同的版本(v6 到 v12)都能顺利运行,并失去雷同后果:

start
end
111 x 10000
123

论断:微队列并不存在深度限度,但过多的微工作会导致系统始终在运行微工作而无奈去运行其余工作,比方例子里处于宏工作的 123 将在 10000 个微工作运行完再运行,体验上有很显著的提早,因而为了性能考量,不应泛滥地应用微工作。

退出移动版