涉及到的概念
- 事情循环 (
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()
})()
我专门画了一张图, 来简略描绘一下过程
- 首要,
main()
会进入履行栈
, 然后履行栈
会自上而下履行该函数中的各条句子 - 在履行句子的过程中, 假如遇到形如同步函数, 比方
test()
, 那么它会先等候其履行完毕, 也便是会将test()
压入栈, 然后持续履行test()
中的各条句子 - 在履行
f1
时,履行栈
又遇到了同步函数console.log
, 所以它持续先等候其履行完毕, 也便是会先等屏幕输出1
, 然后才持续开端下一条句子 - 接着,
履行栈
遇到了异步使命源
, 也便是setTimeout
, 所以他会将setTimeout
分配的异步使命
(包括 fn 和 delay) 送到某个区域, 这个区域咱们先称其为 异步模块 (在浏览器环境中叫 APIs), 然后履行栈
就会持续履行下一条句子了。留意此刻 fn 并没有履行。
APIs
的履行由浏览器独自担任, 他和 JS 的单线程没有关系。记住: JS 是单线程的, 但浏览器是多线程的 [from]
-
履行栈
接着履行, 然后会输出2
, 此刻主函数履行完,履行栈
中没有其他的使命需要履行了。 - 当
履行栈
为空时, 会让Event Loop
从一个区域中取出新的使命履行, 这个区域咱们称之为使命行列
,使命行列
中的使命都是等候程序处理的使命, 这些使命的来源便是咱们刚刚说到的 异步模块。由于此刻时刻还没有过去太久(缺乏500ms), 所以使命行列
为空,履行栈
也为空 -
异步模块 每过一段时刻就会查看一下它保护的那些
异步使命
, 经过大约 500ms(实践数值肯定大于500ms) 后, 异步模块 发现有一个异步使命
(fn) 可以履行了, 所以将这个 fn 发送给使命行列
, 此刻使命行列
不为空 -
履行栈
为空, 而且使命行列
有使命在等候履行, 所以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 -
微使命行列
, 由微使命组成的行列
- 首要, 浏览器自上而下的履行(履行的过程中
履行栈
中进行), 先履行l-1
, 发现是setTimeout
, 所以将它的参数(callback,delay)发送给APIs
- 然后持续识别
l-2
, 发现又是setTimeout
, 所以持续将它的参数发送给APIs
-
APIs
接收到setTimeout
的内容后, 会进行计时, 当经过 delay(也便是500ms) 后, 会将 callback(也便是l-1的回调函数) 发送给使命行列
中的宏使命行列
。这样的事情APIs
干了两次(由于有两个setTimeout
)。 - (继2) 当
履行栈
为空时,Event Loop
会将使命行列
中的使命发送到履行栈
中履行。不过此刻使命行列
为空, 故什么都不履行 - 经过 delay 时刻后, 两个计时器的回调函数将会被
APIs
发送给宏使命行列
- 此刻
履行栈
为空, 而且微使命行列
为空,宏使命行列
非空, 故可以将宏使命行列
中的第一个宏使命
送到履行栈
中履行 -
履行栈
履行l-1
的回调函数, 先履行l-3
, 此刻直接输出"宏使命: 计时使命1"
- 持续履行
l-4
, 发现是Promise
, 所以将 then 的回调函数送到APIs
(横竖是类似APIs
的异步处理模块, 但该模块不属于 JS 的单线程领域), 然后履行了 resolve 了, 所以 then 的回调函数被发送到了微使命行列
中 -
履行栈
履行完l-3
和l-4
后, 又为空了, 所以Event Loop
持续查看使命行列
- 此刻的
使命行列
中,微使命行列
有了新的微使命
, 故先履行微使命
, 也便是将l-5
送入履行栈
中履行, 此刻输出"微使命1"
- 履行完
l-5
后,履行栈
为空,微使命行列
为空, 所以Event Loop
再从宏使命行列
中取出一个宏使命
送往履行栈
- 后边的就和前面的重复(循环)了
- 先履行
l-6
, 输出"宏使命: 计时使命2"
- 在履行
l-7
, 履行完后会微使命行列
又增加了一个微使命
-
履行栈
又为空, 持续查看使命行列
, 取出微使命
送往履行栈
- 履行
l-8
, 输出"微使命2"
-
完毕,使命行列
又为空, 不作任何操作, 等候异步模块持续发送某些宏使命
或微使命
到使命行列
中, 比方用户突然点击了某个绑定了回调事情的按钮, 或许某个网络恳求恳求完毕, 或许是之前设置的某些定时使命到了触发的时刻了等等..
看完上面的过程, 咱们其实可以发现, 打开网页时, 除了网页文档中的 script
脚本是直接送入 履行栈
, 其他的 使命
, 其实都是从 使命行列
中取出的了。或许更加简略一点, 咱们可以直接以为, 文档中最开端的 script
其实便是在 使命行列
中的。或许是 宏使命
, 也或许是 微使命
。横竖记住一点, 最早开端履行的肯定是 script
脚本中的内容, 其他的内容, 就都是从 使命行列
中取出的了, 而 使命行列
中的内容, 是由异步模块(比方 APIs
, 其实我也就只知道一个 APIs
了)发送给咱们的。
对于 宏使命
和 微使命
, 不需要介意谁先履行谁后履行, 只需要记住一点就可以了: 当一个 宏使命
想要履行时, 有必要保证 微使命行列
为空。记住这一点后, 其他的都可以直接推理出来了, 比方 微使命行列
不为空时, 永久轮不到 宏使命
履行, 换句话说, 咱们要当心运用 微使命行列
, 否则会呈现死循环的状况, 这也是为什么官网不主张咱们用太多 queueMicrotask()
函数。
NodeJS11 之前的 Event Loop (不重要, 可忽略)
下面来谈一点 “过时” 的东西, 前面的剖析, 在现在这个时刻点(22.12.15)都是对的。而在之前, 也便是 NodeJS11版别之前(不包括11), node 和 浏览器的 Event Loop
机制是不一样的, 最大的区别就在于 宏使命
和 微使命
。前面现已说了,每一个 宏使命
履行时, 都要保证 微使命行列
为空, 这是新版别的规范。在此之前的版别有一点点不同,之前的版别所要求的的是 同一类宏使命行列 履行之前, 要保证 微使命行列为空。这个差异导致的结果便是,当存在两个 setTimeout
时, 会先履行完这两个宏使命, 然后再去履行微使命, 所以前面的代码, 用 node11 之前的版别运行时, 会是不一样的结果
有关 nodejs 的 Event Loop
详细的流程图, 可以看下面这张图 [1]
在这个过程中, 还发现了一个 让人困惑 有意思的现象, 那便是当咱们将两个 setTimeout
的 delay
设置为 0
秒时, 输出的状况是不确定的, 有时候会呈现 微使命1
在 宏使命2
之前输出, 如图所示
下面我想试着解说这么一种现象。
首要, 经过输出可以发现, 大多数状况下, 仍是先输出两个宏使命, 然后才输出微使命, 这个很好理解, 当 宏使命行列
存在两个 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)
- 首要,
l-1
和l-2
都会在履行栈
中等候履行 -
履行栈
先履行l-1
, 此刻会将l-3,4,5
送到 异步模块 中。为便利描绘, 记l-3,4,5
为s1
。 -
履行栈
持续履行l-2
, 此刻会将l-6,7,8
送到 异步模块 中。 记l-6,7,8
为s2
由于 异步模块 不属于 JS 单线程的领域, 所以 异步模块 的内容和
履行栈
中的内容是可以并发进行的, 这就导致了一种分歧: 当履行栈
为空时, 异步模块 或许还未将s1
发送给宏使命行列
, 也或许现已将s1
发送给宏使命行列
。 理由: 咱们设置的延迟时刻是 0, 理论上s1
被送往 异步模块 时, 异步模块 应该立刻将其发送给使命行列
, 但实践上, 异步模块 应该会每隔一段时刻, 才检查s1
的延迟时刻是否现已到期, 才决议是否将s1
送往宏使命行列
。 下面咱们考虑的是状况是, 当履行栈
为空时, 只要s1
现已被送往宏使命行列
。
- 当
履行栈
将s2
送往 异步模块 后,履行栈
为空, 此刻宏使命行列
只要s1
,微使命行列
为空, 所以Event Loop
将s1
送往履行栈
履行 -
关键来了!, 存在这么一个时刻节点,
履行栈
在履行s1
, 异步模块 在等候s2
的时刻到期,使命行列
为空。当履行栈
先履行完s1
时,l-4
(Promise callbacks) 会被送往了 异步模块, 而且 异步模块 还未将s2
送往宏使命行列
。 也便是说, 此刻的 异步模块 同时存在s2
和l-4
(Promise callbacks), 而且 异步模块 会先将微使命l-4
(Promise callbacks) 送往微使命行列
, 而s2
还停留在 异步模块 中。
尽管我不清楚 异步模块 详细实现的源代码, 甚至都不敢保证 node 中存在 异步模块 这个机制, 但由于 微使命 是优于 宏使命 的, 所以, 当同时存在 0 秒延迟的
setTimeout
和微使命(Promise callbacks)时, 即便setTimeout
是率先抵达 异步模块 的, 我也以为微使命是有时机先于setTimeout
被发送到使命行列
的。
-
异步模块 先将
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
是一种机制, 它指示了异步使命使命之间的运行规则。 - 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)