涉及到的概念

  • 事情循环 (Event Loop)
  • 宏使命 (macrotask)
  • 宏使命行列 (macrotask queue)。在 WHATWG specification 中被简略地称为 task queue。[from]
  • 微使命 (microtask)
  • 微使命行列 (microtask queue)
  • 履行栈, 或调用栈 (Call Stack)
  • 使命行列 (Task Queue, Event Queue), 我这儿将 宏使命行列微使命行列 统称为 使命行列
  • 异步编程 (asynchronous programming)
  • 单线程 (single-thread)

经过一段代码, 来简略知道 Event Loop

我这儿先做界说一个 异步使命源, 使命源可以分配使命, 异步使命源 便是专门分配 异步使命 的。这个主意来自 WHATWG 的 task source。我这儿不对界说做过多阐明,直接看我的比方, 你应该就明白我想表达什么了:

比方 setTimeout(callback) 中的 setTimeout 是一个 异步使命源, 它的参数 callback 便是一个 异步使命, setTimeout 本身的履行是同步的, 只不过在它履行的过程中, 它创建一个 异步使命, 这个 异步使命 不会立马履行。

function test () {
  console.log('1') // f1
  setTimeout(fn, delay/* 假设是500ms */) // f2
  console.log('2') // f3
}
;(function main () {
  test()
})()

我专门画了一张图, 来简略描绘一下过程

简单认识一下 Event Loop、宏任务和微任务

  1. 首要, main() 会进入 履行栈, 然后 履行栈 会自上而下履行该函数中的各条句子
  2. 在履行句子的过程中, 假如遇到形如同步函数, 比方 test(), 那么它会先等候其履行完毕, 也便是会将 test() 压入栈, 然后持续履行 test() 中的各条句子
  3. 在履行 f1 时, 履行栈 又遇到了同步函数 console.log, 所以它持续先等候其履行完毕, 也便是会先等屏幕输出 1, 然后才持续开端下一条句子
  4. 接着, 履行栈 遇到了 异步使命源, 也便是 setTimeout, 所以他会将 setTimeout 分配的 异步使命(包括 fn 和 delay) 送到某个区域, 这个区域咱们先称其为 异步模块 (在浏览器环境中叫 APIs), 然后 履行栈 就会持续履行下一条句子了。留意此刻 fn 并没有履行。

APIs 的履行由浏览器独自担任, 他和 JS 的单线程没有关系。记住: JS 是单线程的, 但浏览器是多线程的 [from]

  1. 履行栈 接着履行, 然后会输出 2, 此刻主函数履行完, 履行栈 中没有其他的使命需要履行了。
  2. 履行栈 为空时, 会让 Event Loop 从一个区域中取出新的使命履行, 这个区域咱们称之为 使命行列, 使命行列 中的使命都是等候程序处理的使命, 这些使命的来源便是咱们刚刚说到的 异步模块。由于此刻时刻还没有过去太久(缺乏500ms), 所以 使命行列 为空, 履行栈 也为空
  3. 异步模块 每过一段时刻就会查看一下它保护的那些 异步使命, 经过大约 500ms(实践数值肯定大于500ms) 后, 异步模块 发现有一个 异步使命(fn) 可以履行了, 所以将这个 fn 发送给 使命行列, 此刻 使命行列 不为空
  4. 履行栈 为空, 而且 使命行列 有使命在等候履行, 所以 Event Loop 从中取出使命(fn), 并发送到 履行栈 中履行, 所以 履行栈 持续履行, 并输出了 3

从上面的过程可以看到, 一个重要的时刻点便是 履行栈 为空, 而且这个时刻点后, 相关操作都是由 Event Loop 处理的, Event Loop 担任调控整个流程。 当然 异步模块 中的内容是别的的一部分, 正因如此, 当 履行栈 中履行使命时, 异步模块 可以对异步使命进行计时。

总的来说, 履行栈 履行使命, 履行完后, 就会让 Event Loop使命行列 中取出新的使命送到 履行栈 中履行。留意, 当 履行栈 不为空时, 使命行列 中的使命是无法进入 履行栈 中履行的。

现在, 让咱们了解两个新名词, 宏使命微使命, 前面的 异步使命 并不是常见的名词, 而是我自己在此处界说的名词, 异步使命 其实便是 宏使命微使命 的总称。

宏使命和微使命

宏使命微使命 是两类不同的 异步 使命。[from]

宏使命行列 的数据结构 不是行列, 而是 集合。[from]。 记住这个概念很重要, 由于行列是有序的, 而集合是无序的, 所以在 宏使命行列 中, 先抵达的使命 不一定 会先履行。

微使命行列 的数据结构是 行列, 所以 微使命行列 中使命的履行一定是有序的。微使命行列 还有这么一个特色, 当 微使命行列 中的 微使命 开端履行时, 它可以持续添加新的 微使命微使命行列 中, 而且 微使命行列 一旦开端履行, 就会履行到 微使命行列 为空。换句话说, 假如不断的有新的 微使命 加入到 微使命行列 中来, 那么 宏使命 将不断的被堵塞, 无法履行。这种状况导致的最常见的结果便是页面无法响应你的鼠标或许滚轮, 由于与用户的交互是属于 宏使命为了处理无限递归的 微使命, 听说以前的 Nodejs 中, 会供给一个机制来约束最大的递归数量, 但我没有在官方文档中找到详细的内容。[from], [from]

微使命行列 中的使命会一次性履行完, 带来的优点是它保证了每一个 微使命 之间的应用程序环境基本相同(没有鼠标坐标更改,没有新的网络数据等)。[from]

假如要给 微使命宏使命 定一个优先级, 那么你可以以为 微使命 的优先级更高。但我以为, 与其记住谁的优先级更高, 不如记住这么一句话: 每一个宏使命履行之前, 有必要保证微使命行列为空。[from]

下面给出已知的 宏使命微使命

  • 宏使命
    • setTimeout
    • setInterval
    • setImmediate (Node 独有)
    • requestAnimationFrame (浏览器独有)
    • I/O
    • UI rendering (浏览器独有)
  • 微使命
    • process.nextTick (Node 独有)
    • Promises (准确的说是 Promise.then() 中 then 的回调函数, 而不是 new promise(callback) 携带的回调函数)
    • Object.observe
    • MutationObserver
    • queueMicrotask

经过一段代码来理解宏使命和微使命

setTimeout(() => { // l-1
  console.log("宏使命: 计时使命1") // l-3
  Promise.resolve().then(() => { // l-4
    console.log("微使命1") // l-5
  })
}, 500);
setTimeout(() => { // l-2
  console.log("宏使命: 计时使命2") // l-6
  Promise.resolve().then(() => { // l-7
    console.log("微使命2") // l-8
  })
}, 500);

先讨论真正有用的, 也便是Node11之后版别和浏览器的版别, 下面以浏览器内核进行解说:

  • l-数字 代表某行代码
  • APIs 是浏览器中的一个机制, 详细的结果不清楚, 只知道一些异步API的处理, 都是由它进行处理的, 当异步函数履行完毕时, 也是由它担任发送给 使命行列。[from]
  • 宏使命行列, 由宏使命组成的行列, 宏使命行列分为 计时器行列 (Expired Timer Callbacks, 即到期的setTimeout/setInterval)、IO事情行列(I/O Events)、即时行列 (Immediate Queue, 即 setImmediate)、关闭事情处理程序行列 (close Handlers)。from, from
  • 微使命行列, 由微使命组成的行列
  1. 首要, 浏览器自上而下的履行(履行的过程中 履行栈 中进行), 先履行 l-1, 发现是 setTimeout, 所以将它的参数(callback,delay)发送给 APIs
  2. 然后持续识别 l-2, 发现又是 setTimeout, 所以持续将它的参数发送给 APIs
  3. APIs 接收到 setTimeout 的内容后, 会进行计时, 当经过 delay(也便是500ms) 后, 会将 callback(也便是l-1的回调函数) 发送给 使命行列 中的 宏使命行列。这样的事情 APIs 干了两次(由于有两个 setTimeout)。
  4. (继2) 当 履行栈 为空时, Event Loop 会将 使命行列 中的使命发送到 履行栈 中履行。不过此刻 使命行列 为空, 故什么都不履行
  5. 经过 delay 时刻后, 两个计时器的回调函数将会被 APIs 发送给 宏使命行列
  6. 此刻 履行栈 为空, 而且 微使命行列 为空, 宏使命行列 非空, 故可以将 宏使命行列 中的第一个 宏使命 送到 履行栈 中履行
  7. 履行栈 履行 l-1 的回调函数, 先履行 l-3, 此刻直接输出 "宏使命: 计时使命1"
  8. 持续履行 l-4, 发现是 Promise, 所以将 then 的回调函数送到 APIs (横竖是类似 APIs 的异步处理模块, 但该模块不属于 JS 的单线程领域), 然后履行了 resolve 了, 所以 then 的回调函数被发送到了 微使命行列
  9. 履行栈 履行完 l-3l-4 后, 又为空了, 所以 Event Loop 持续查看 使命行列
  10. 此刻的 使命行列 中, 微使命行列 有了新的 微使命, 故先履行 微使命, 也便是将 l-5 送入 履行栈 中履行, 此刻输出 "微使命1"
  11. 履行完 l-5 后, 履行栈 为空, 微使命行列 为空, 所以 Event Loop 再从 宏使命行列 中取出一个 宏使命 送往 履行栈
  12. 后边的就和前面的重复(循环)了
  13. 先履行 l-6, 输出 "宏使命: 计时使命2"
  14. 在履行 l-7, 履行完后会 微使命行列 又增加了一个 微使命
  15. 履行栈 又为空, 持续查看 使命行列, 取出 微使命 送往 履行栈
  16. 履行 l-8, 输出 "微使命2"
  17. 完毕, 使命行列 又为空, 不作任何操作, 等候异步模块持续发送某些 宏使命微使命使命行列 中, 比方用户突然点击了某个绑定了回调事情的按钮, 或许某个网络恳求恳求完毕, 或许是之前设置的某些定时使命到了触发的时刻了等等..

看完上面的过程, 咱们其实可以发现, 打开网页时, 除了网页文档中的 script 脚本是直接送入 履行栈, 其他的 使命, 其实都是从 使命行列 中取出的了。或许更加简略一点, 咱们可以直接以为, 文档中最开端的 script 其实便是在 使命行列 中的。或许是 宏使命, 也或许是 微使命。横竖记住一点, 最早开端履行的肯定是 script 脚本中的内容, 其他的内容, 就都是从 使命行列 中取出的了, 而 使命行列 中的内容, 是由异步模块(比方 APIs, 其实我也就只知道一个 APIs 了)发送给咱们的。

对于 宏使命微使命, 不需要介意谁先履行谁后履行, 只需要记住一点就可以了: 当一个 宏使命 想要履行时, 有必要保证 微使命行列 为空。记住这一点后, 其他的都可以直接推理出来了, 比方 微使命行列 不为空时, 永久轮不到 宏使命 履行, 换句话说, 咱们要当心运用 微使命行列, 否则会呈现死循环的状况, 这也是为什么官网不主张咱们用太多 queueMicrotask() 函数。

NodeJS11 之前的 Event Loop (不重要, 可忽略)

下面来谈一点 “过时” 的东西, 前面的剖析, 在现在这个时刻点(22.12.15)都是对的。而在之前, 也便是 NodeJS11版别之前(不包括11), node 和 浏览器的 Event Loop 机制是不一样的, 最大的区别就在于 宏使命微使命。前面现已说了,每一个 宏使命 履行时, 都要保证 微使命行列 为空, 这是新版别的规范。在此之前的版别有一点点不同,之前的版别所要求的的是 同一类宏使命行列 履行之前, 要保证 微使命行列为空。这个差异导致的结果便是,当存在两个 setTimeout 时, 会先履行完这两个宏使命, 然后再去履行微使命, 所以前面的代码, 用 node11 之前的版别运行时, 会是不一样的结果

简单认识一下 Event Loop、宏任务和微任务

有关 nodejs 的 Event Loop 详细的流程图, 可以看下面这张图 [1]

在这个过程中, 还发现了一个 让人困惑 有意思的现象, 那便是当咱们将两个 setTimeoutdelay 设置为 0 秒时, 输出的状况是不确定的, 有时候会呈现 微使命1宏使命2 之前输出, 如图所示

简单认识一下 Event Loop、宏任务和微任务

下面我想试着解说这么一种现象。

首要, 经过输出可以发现, 大多数状况下, 仍是先输出两个宏使命, 然后才输出微使命, 这个很好理解, 当 宏使命行列 存在两个 setTimeout 时, 肯定会先履行完两个 setTimeout 后再去查看 微使命行列 (留意这是 NodeJS11 版别之前, 新版别不是这样的)。

那么什么状况下, 会呈现先输出 宏使命1微使命1 呢?, 我以为关键就在于程序详细履行的细节中。上一段话中, 咱们说了 宏使命行列 存在两个 setTimeout 时, 肯定会先履行完两个 setTimeout, 但实践运行时, 宏使命行列 中一定会存在两个 setTimeout 吗? 或许应该这么问, 当第一个 setTimeout 运行完后, 别的一个 setTimeout 真的存在 宏使命行列 中吗? 答案应该是不一定的, 让我逐帧来剖析一下:

在开端剖析之前, 容许我再重复一下上面的代码:

setTimeout(() => { // l-1
  console.log("宏使命: 计时使命1") // l-3
  Promise.resolve().then(() => { // l-4
    console.log("微使命1") // l-5
  })
}, 0)
setTimeout(() => { // l-2
  console.log("宏使命: 计时使命2") // l-6
  Promise.resolve().then(() => { // l-7
    console.log("微使命2") // l-8
  })
}, 0)
  1. 首要, l-1l-2 都会在 履行栈 中等候履行
  2. 履行栈 先履行 l-1, 此刻会将 l-3,4,5 送到 异步模块 中。为便利描绘, 记 l-3,4,5s1
  3. 履行栈 持续履行 l-2, 此刻会将 l-6,7,8 送到 异步模块 中。 记 l-6,7,8s2

由于 异步模块 不属于 JS 单线程的领域, 所以 异步模块 的内容和 履行栈 中的内容是可以并发进行的, 这就导致了一种分歧: 当 履行栈 为空时, 异步模块 或许还未将 s1 发送给 宏使命行列, 也或许现已将 s1 发送给 宏使命行列。 理由: 咱们设置的延迟时刻是 0, 理论上 s1 被送往 异步模块 时, 异步模块 应该立刻将其发送给 使命行列, 但实践上, 异步模块 应该会每隔一段时刻, 才检查 s1 的延迟时刻是否现已到期, 才决议是否将 s1 送往 宏使命行列。 下面咱们考虑的是状况是, 当 履行栈 为空时, 只要 s1 现已被送往 宏使命行列

  1. 履行栈s2 送往 异步模块 后, 履行栈 为空, 此刻 宏使命行列 只要 s1, 微使命行列 为空, 所以 Event Loops1 送往 履行栈 履行
  2. 关键来了!, 存在这么一个时刻节点, 履行栈 在履行 s1, 异步模块 在等候 s2 的时刻到期, 使命行列 为空。 履行栈 先履行完 s1 时, l-4 (Promise callbacks) 会被送往了 异步模块, 而且 异步模块 还未将 s2 送往 宏使命行列。 也便是说, 此刻的 异步模块 同时存在 s2l-4 (Promise callbacks), 而且 异步模块 会先将微使命 l-4 (Promise callbacks) 送往 微使命行列, 而 s2 还停留在 异步模块 中。

尽管我不清楚 异步模块 详细实现的源代码, 甚至都不敢保证 node 中存在 异步模块 这个机制, 但由于 微使命 是优于 宏使命 的, 所以, 当同时存在 0 秒延迟的 setTimeout 和微使命(Promise callbacks)时, 即便 setTimeout 是率先抵达 异步模块 的, 我也以为微使命是有时机先于 setTimeout 被发送到 使命行列 的。

  1. 异步模块 先将 l-4 发送到 微使命行列, 此刻, 履行栈 为空, s2 还未送往 宏使命行列, 微使命行列 中存在 l-4, 所以 Event Loop 就先将 l-4 送往 履行栈 中履行了, 然后导致了 微使命1 先于 宏使命2 输出。后边的剖析就没有什么需要做笔记的了。

总的来说, 我的解说便是: 存在这么一个时刻节点, 异步模块 中同时存在 宏使命2微使命1。而且, 尽管 宏使命2 先于 微使命1 进入 异步模块, 但 异步模块 仍是或许先将 微使命1 发送到 使命行列, 然后导致了 微使命1 先于 宏使命2 履行。

其实, 我不确定上面的解说是否是正确的, 由于我不清楚 nodejs 的源代码是怎么编写的, 更不确定 nodejs 是否真的存在一个 异步模块, 但由于浏览器存在一个 APIs, 所以我感觉 nodejs 或许也有一个 异步模块

下面我再用别人给出的 nodejs11版别之前的 Event Loop 图, 来解说一下:

简单认识一下 Event Loop、宏任务和微任务

简单认识一下 Event Loop、宏任务和微任务
好了, 这一部分, 仅仅只是感觉有意思的, 所以想着纪录一下, 初次学习(说的便是我)不要 只想不做, 否则简单误入歧途, 最好休息一下, 放空放空大脑, 或许去看看大佬的文章, 再回头来思考一下。

总结

  • Event Loop 是一种机制, 它指示了异步使命使命之间的运行规则。
  • JS 的单线程, 体现在 履行栈 只要一个, 而且只要 履行栈 为空时, 才有时机将新的使命送入 履行栈 中履行。
  • 每一个宏使命履行之前, 有必要保证 微使命行列 为空。两个 setTimeout 的回调函数, 属于两个宏使命。

参考资料

  • JS 的异步机制一探 – ByteEE

  • NodeJS Event Loop: JavaScript Event Loop vs Node JS Event Loop – Deepal Jayasekara(很优异的系列文章, 这儿有翻译版, 不过最新的一篇没有翻译 —— 有关Nodejs和浏览器的比照)

  • Difference between microtask and macrotask within an event loop context – stack overflow

  • Event Loop – WHATWG(想要更深入时必读)

  • what is an event loop in javascript, (不长, 可简略的知道什么是 Event Loop)

  • JavaScript Event Loop, (不是很长, 介绍的是 Event Loop, 图表更丰富一些)

  • Understanding the Event Loop, Callbacks, Promises, and Async/Await in JavaScript

  • event-loop

  • 带你了解事情循环机制(Event Loop)