1. 并发与并行
  2. call stack
  3. event loop(事件循环)
  4. task
  5. macrotask(宏任务)
  6. microtask(微任务)
  7. task 是如何运行的
  8. 实例
  9. 参考

event loop

JavaScript 支持处理并发事件,其并发模型基于 event loop.
Untitled Diagram (2).jpg

并发与并行

并发和并行是两个不同的概念,很容易被弄混.
并发(concurrency)指的是同一时间有能力应对多件事,而并行(Parallel )指的是同一时间有能力做多件事.

举个例子:
妈妈一边打小明,一边和老王通电话.妈妈先打一下小明, 然后和老王聊一句.如此交替,这是并发.
小明的妈妈是个妖怪,长了两个脑袋.一个脑袋骂小明,同时另一个脑袋通电话.这即是并行,也是并发.
但如果妈妈的两个脑袋同时骂小明,那这就只能是并行了,不是并发.因为骂小明是同一个事件.

call stack

什么是 call stack?维基百科给出的解释是:

调用栈(英语:Call stack,港台称“呼叫堆叠”,英文直接简称为“栈”(the stack))别称有:执行栈(execution stack)、控制栈(control stack)、运行时栈(run-time stack)与机器栈(machine stack),是计算机科学中存储有关正在运行的子程序的消息的栈。有时仅称“栈”,但栈中不一定仅存储子程序消息。几乎所有计算机程序都依赖于调用栈,然而高级语言一般将调用栈的细节隐藏至后台。

调用栈最经常被用于存放子程序的返回地址。在调用任何子程序时,主程序都必须暂存子程序运行完毕后应该返回到的地址。因此,如果被调用的子程序还要调用其他的子程序,其自身的返回地址就必须存入调用栈,在其自身运行完毕后再行取回。在递归程序中,每一层次递归都必须在调用栈上增加一条地址,因此如果程序出现无限递归(或仅仅是过多的递归层次),调用栈就会产生栈溢出。

https://zh.wikipedia.org/wiki/呼叫堆疊

可以注意到, call stack 有很多别称, 例如 stack 或执行栈. 这很容易引发误解. 因为不同资料对 call stack 的不同叫法, 让读者误以为是不同的东西.

在 Js 中, call stack 和 execution context stack指的是同一个概念.
Js 引擎将 execution context(执行环境)储存在 execution context stack(执行环境栈)中, 利用 execution context 来储存/查找对应的变量/函数.

Is "Call stack" the same as "Execution context stack" in JavaScript? - Stack Overflow

event loop(事件循环)

什么是 event loop?

In computer science, the event loop, message dispatcher, message loop, message pump, or run loop is a programming construct that waits for and dispatches events or messages in a program. It works by making a request to some internal or external "event provider" (that generally blocks the request until an event has arrived), and then it calls the relevant event handler ("dispatches the event").

在计算机科学中,event loop(又名 message dispatcher, message loop, message pump, run loop)是一个程序结构, 其等待并分发程序中的事件和消息. 其工作原理是向某些内部或外部的"事件生成器"发出请求,然后调用相应的事件处理程序.

Event loop - Wikipedia

在浏览器中, event loop 是一个循环, 负责检查 task queues(任务队列)是否为空. 如果 task queue 中有 task(任务),event loop 就取一个出来运行.

值得注意的是, 浏览器的 event loop 规范由 HTML 标准提供, 而非 ECMAScript.

在 whatwg 的 HTML 文档中,关于 event loop 的描述如下:

To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. Each agent has an associated event loop.
用户代理必须使用本节描述的 event loop 来协调 事件/用户交互/脚本/渲染/网络 间的工作.
每个代理都有一个相关的 event loop.

coordinate 的意思是 to make various different things work effectively as a whole 让一大堆东西可以同时正常工作.

An event loop has one or more task queues. A task queue is an ordered list of tasks.
event loop 有一个或多个 task queue(任务队列).task queue 是有序的 task 队列.

When a user agent is to queue a task, it must add the given task to one of the task queues of the relevant event loop.
当用户代理要 queue a task(排列某个 task 时), 其必须把这个 task 添加到某个(与 event loop 相关的)task queue 中

Each task is defined as coming from a specific task source. All the tasks from one particular task source and destined to a particular event loop must always be added to the same task queue, but tasks from different task sources may be placed in different task queues.
每个 task 都由一个特定的 task source 生成,所有来自同一个 task source 的 task 都必须放到同一个 task queue 中,来自不同 task source 的 task 可以放在不同的 task queue 中

HTML Standard

task

task 分为两种, macrotask(宏任务)和 microtask(微任务).
同样, task queue 也分为两种, macrotask queue(宏任务队列)和 microtask queue(微任务队列).
可以同时存在多个 macrotask queue, 但只能有一个 microtask queue.

注: macro 和 micro 的发音有些相似,具体的区别可参考: Difference in pronunciation MICRO vs MACRO: Learn British English

当 execution content stack 为空时, event loop 首先会检查 microtask queue.
如果 microtask queue 队列中有任务, 则依序执行队列中的 microtask,然后把执行完的 microtask 从队列中删除,直到清空 microtask queue 为止.
如果在执行 microtask 的过程中生成了新的 microtask, 则把这个新的 microtask 添加到 microtask queue 队尾.这个新的 microtask 也会在本次循环中被执行.
当 microtask queue 被清空后, event loop 会依照权重, 从权重高的 macrotask queue 中取出一个 macrotask 执行, 然后删除这个 macrotask.
等到 execution content stack 再次为空后, 开始另一轮循环.

注: 浏览器可能会将类似的 macrotask 放入同一个的 macrotask queue.将不同的 macrotask 放入不同的 macrotask queue.
例如, 鼠标/键盘交互可能被分类到同一个 macrotask queue,而 setTimeout 和 setInterval 则会被分类到另一个 macrotask queue.
鼠标对实时性交互要求高, 所以鼠标事件所在的 macrotask queue 权重可能会高于 setTimeout 所在的 macrotask queue.

总结:

  • 只有 execution content stack 为空时, task 才会被执行
  • microtask 的优先级比 macrotask 高
  • microtask 一次性全部执行完,而 macrotask 按单个执行

macrotask(宏任务)

whatwg 文档中没有到 microtask 这个关键词.
这是因为在 microtask 出现后, 为了将文档原来有的 task 和 microtask 相区别, 称原有 task 称为 macrotask.

属于 macrotask 的任务有:

  • I/O
  • UI rendering UI 渲染
  • Event 事件
  • HTML parser HTML 文档解析
  • DOM manipulation DOM 操作
  • requestAnimationFrame
  • setTimeout/setInterval
  • setImmediate(IE10+)
  • MessageChannel
  • window.postMessage
  • XMLHttpRquest.onload

其实大部分事件都是 macrotask

microtask(微任务)

属于 micro task 的任务有:

  • process.nextTick(Node)
  • Promise.then
  • await
  • MutationObserver.observe
  • Object.observe(废弃)

在 chromium 中,microtask queue 由 V8(js 引擎)提供. 但是 ECMAScript 文档里并没有 microtask, 只能查到一个类似的 job.

ECMAScript 文档中,有关 job 的描述:

Execution of a Job can be initiated only when there is no running execution context and the execution context stack is empty.
当没有正在运行的执行环境且执行环境栈为空时, 才能运行 Job.

Once execution of a Job is initiated, the Job always executes to completion. No other Job may be initiated until the currently running Job completes.
一旦开始运行某个 Job, 这个 Job 就一定会完成运行(不会被中断).只有在当前的 Job 运行完毕后, 才能运行其他的 Job.

ECMAScript® 2018 Language Specification

task 是如何运行的

在 whatwg 文档中,没有明确说明如何运行 task.只有一句 run task:

1
2
3
4
// macrotask
Run oldestTask.
// microtask
Run oldestMicrotask.

HTML Standard

不过 ECMAScript 文档详细描述了 job(microtask)的运行过程:

Assert: The execution context stack is now empty.
Let nextQueue be a non-empty Job Queue chosen in an implementation-defined manner. If all Job Queues are empty, the result is implementation-defined.
Let nextPending be the PendingJob record at the front of nextQueue. Remove that record from nextQueue.
令 nextPending 为 PendingJob recode, 从 nextQueue 中删除该 recode.
Let newContext be a new execution context.
令 newContext 为新建的 execution context
Set newContext's Function to null.
Set newContext's Realm to nextPending.[[Realm]].
令 newContext 的 Realm 为 nextPending
Set newContext's ScriptOrModule to nextPending.[[ScriptOrModule]].
Push newContext onto the execution context stack; newContext is now the running execution context.
将 newContext 推入 execution context stack,成为 running execution context
Perform any implementation or host environment defined job initialization using nextPending.
Let result be the result of performing the abstract operation named by nextPending.[[Job]] using the elements of nextPending.[[Arguments]] as its arguments.
令 result 为以 nextPending.[[Arguments]]为参数执行 nextPending.[[Job]]的结果
If result is an abrupt completion, perform HostReportErrors(« result.[[Value]] »).

ECMAScript® 2018 Language Specification

js 引擎会会根据 job 生成一个 execution context, 将其推入 execution context stack,然后执行其中的代码.
执行完毕后, 将 execution context 从栈中弹出, 然后执行下一个 job.

实例

一些关于 event loop 的例子:

1

1
2
3
4
5
6
setTimeout(_ => {
console.log("setTimeout");
}, 0);
Promise.resolve().then(_ => {
console.log("Promise.then");
});

运行结果:

1
2
Promise.then
setTimeout

setTimeout 是 macrotask, Promise.then 是 microtask.
microtask 先执行,所以先打印 Promise.then.

2

1
2
3
4
5
6
7
8
9
setTimeout(_ => {
console.log("setTimeout");
}, 0);
Promise.resolve().then(_ => {
console.log("Promise.then1");
Promise.resolve().then(_ => {
console.log("Promise.then2");
});
});

运行结果:

1
2
3
Promise.then1
Promise.then2
setTimeout

新生成的 microtask 会被添加到 microtask queue 队列的尾部,且 microtask queue 中的任务会被全部执行.
因此先执行了所有的 Promise.then, 然后再执行 setTimeout.

3

1
2
3
4
5
6
7
8
9
10
11
12
async function bar() {
console.log("bar");
}
async function foo() {
console.log("foo1");
await bar();
console.log("foo2");
}
Promise.resolve().then(_ => {
console.log("Promise.then");
});
foo();

执行结果:

1
2
3
4
foo1
bar
Promise.then
foo2

当调用 async 函数时, 如果返回值不是 Promise 对象, 则会将其包裹在 Promise.resolve 中返回.
(疑问: 是 new Promise(resolve => resolve())还是 Promise.resolve() ?)
当遇到 await 时, 先执行 await 右侧的代码, 然后将右侧代码的执行结果包裹在 Promise.resolve 中返回.
而 await 关键字后面的代码(从下一行开始),则被包裹在 Promise.resolve().then()中.

因此, 实际的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function bar() {
return new Promise(resolve => {
console.log("bar");
resolve();
});
}
function foo() {
return new Promise(resolve => {
console.log("foo1");
resolve(Promise.resolve(bar()).then(_ => console.log("foo2")));
});
}
Promise.resolve().then(_ => {
console.log("Promise.then");
});
foo();

关于 await: 在 chrome72 及以下版本中, 可能得到和 chrome73 不同的结果,这里以高版本浏览器为准.
具体原因可以参考: javascript - async await 和 promise 微任务执行顺序问题 - SegmentFault 思否

参考

What the heck is the event loop anyway? | Philip Roberts | JSConf EU
Jake Archibald: In The Loop - JSConf.Asia 2018
Tasks, microtasks, queues and schedules - JakeArchibald.com
任务、微任务、队列和时间表 - 众成翻译(上文的翻译)
从 event loop 规范探究 javaScript 异步及浏览器更新渲染时机 · Issue #5 · aooy/blog · GitHub
JavaScript 异步、栈、事件循环、任务队列 - JS 精读 - SegmentFault 思否
从 Chrome 源码看事件循环 - 知乎
关于 javascript 的 event loop 如何理解 event queue 的优先级? - 边城的回答 - SegmentFault 思否
JavaScript 中的 task queues - blog
8 张图帮你一步步看清 async/await 和 promise 的执行顺序 - 前端进阶 - SegmentFault 思否
javascript - async await 和 promise 微任务执行顺序问题 - SegmentFault 思否
手把手教你写一个 Javascript 框架:时间调度 - 众成翻译