写这篇文章的原因有两个:其一,团队小伙伴之前分享过《macrotask microtask介绍》这个话题,当时留下了一些疑问,至今仍模棱两可;其二,看到了「奇舞周刊」转发了一篇《从 薛定谔的猫 聊到 Event loop》的文章,内容精炼,但是有一些原则性的问题和规范有偏差。 特整理一下相关内容,以免误导大家,也对自己的掌握做一个总结(大部分内容均来自官方文档,文档结尾处有相关链接)。
JavaScript 引擎不是单独运行的 — 它运行在一个宿主环境中,对于大多数开发者来说就是典型的浏览器和 Node.js(如今,JavaScript 被应用到了从机器人到灯泡的各种设备上)。每个设备都代表了一种不同类型的 JS 引擎的宿主环境。但,所有的环境都有一个共同点,就是都拥有一个 事件循环 Event Loop 的内置机制,它随着时间的推移每次都去调用 JavaScript 引擎去处理程序中多个块的执行。
这意味着 JavaScript 引擎只是 JavaScript 代码按需执行的环境。是它周围的环境来调度 JavaScript 代码执行。
事件循环(Event Loop)的任务很简单: 监控调用栈和回调队列。如果调用栈是空的,它就会取出队列中的第一个事件,然后将它压入到调用栈中,然后运行它。
Event Loop 是在 HTML Standard 中定义的: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.
之所以称之为事件循环,是因为它经常按照类似如下的方式来被实现:
while (queue.waitForTask()) {
queue.processNextTask();
}
如果当前没有任何任务,queue.waitForTask()
会同步地等待任务到达。
每个”线程“都有自己的 Event Loop。所以,每个 web worker 拥有独立的 Event Loop,它们都可以独立运行;同源的 windows 共享一个 Event Loop,它们之间可以互相通信。
A window event loop is the event loop used by similar-origin window agents. User agents may share an event loop across similar-origin window agents. A worker event loop is the event loop used by dedicated worker agents, shared worker agents, and service worker agents. There must be one worker event loop per such agent.
每一个任务完整的执行后,其它任务才会被执行。这为程序的分析提供了优秀的特性:一个函数执行时,它永远不会被抢占,并且在其他代码运行之前完全运行;与此同时带来的是,当一个任务需要太长时间才能处理完毕时,Web 应用就无法处理用户的交互,例如点击或滚动。
在 Event Loop
期间的某个时刻,运行时从最先进入队列的消息开始处理队列中的任务。为此,这个消息会被移出队列,并作为输入参数调用与之关联的函数。调用一个函数总是会为其创造一个新的栈帧 — 见下述「执行栈」描述。
函数的处理会一直进行到执行栈再次为空为止;然后事件循环将会处理队列中的下一个任务(如果还有的话)。
在规范的 Processing model 定义了 event loop
的循环过程。 概括来说:
Event Loop
会不断循环的去取 tasks
队列的中”最老“(最先进入队列)的一个任务(这里的任务就是 macrotask
)推入栈中执行;并在当次循环里依次执行并清空 microtask
队列里的任务microtask
队列里的任务,有可能会渲染更新(浏览器很聪明,在一帧以内的多次Dom变动浏览器不会立即响应,而是会积攒变动以最高60HZ的频率更新视图)事件循环模型的一个非常有趣的特性是,永不阻塞。 处理 I/O 通常通过事件和回调来执行。
所以,比如当你的 JavaScript 程序发出了一个 Ajax 请求(异步)去服务器获取数据,在回调函数中写了相关 response 的处理代码。 JavaScript 引擎就会告诉宿主环境: “嘿,我现在要暂停执行了,但是当你完成了这个网络请求,并且获取到数据的时候,请回来调用这个函数。“然后宿主环境(浏览器)设置对网络响应的监听,当返回时,它将会把回调函数插入到事件循环队列里然后执行。
说道 Event Loop,不得不提及执行栈(JavaScript execution context stack),相关官方描述 — Here。
JavaScript 是单线程,只有一个执行栈,每一个函数执行的时候,都会生成新的执行上下文(execution context),执行上下文会包含一些当前函数的参数、局部变量之类的信息,它会被推入栈中,正在执行的上下文(running execution context)始终处于栈的顶部。当函数执行完后,它的执行上下文会从栈弹出。
function bar() {
console.log('bar')
}
function foo() {
console.log('foo')
bar()
}
foo()
注意: 正在运行的执行上下文(running execution context)始终是此堆栈的顶层元素。每当控制从与当前运行的执行上下文相关联的可执行代码转移到与该执行上下文无关的可执行代码时,就创建新的执行上下文。新创建的执行上下文被压入堆栈并成为正在运行的执行上下文。正在执行的只有一个!!!
====================================== 华丽的分割线 ====================================== 至此,我们已经很清晰的知道:Event Loop 从任务队列获取任务,然后将任务添加到执行栈中( 动态,根据函数调用),JavaScript 引擎获取执行栈最顶层元素(即正在运行的执行上下文)进行运行!
那么,Event Loop 执行过程中,提及到的 macrotask 与 microtask 又有啥区别?
An event loop has one or more task queues. A task queue is a set of tasks.
Event Loop 有一个或多个任务队列,这里的任务就是文章中提及的 宏任务 — macrotask。
Note:The microtask queue is not a task queue
以下,属于宏任务(macrotask/task):— Here
所以,当 执行栈 为空时,会从任务队列里获取任务,加入到执行栈中,这里的任务就是 宏任务。
setTimeout(…)
不会自动的把回调放到事件循环队列中。它设置了一个定时器,当定时器过期了,宿主环境会将回调放到事件循环队列中,以便在以后的循环中取走执行它。
setTimeout(myCallback, 1000)
这并不意味着 myCallback
将会在 1000ms 之后执行,而是,在 1000ms 之后将被添加到事件队列。然而,这个队列中可能会拥有一些早一点添加进来的事件 — 回调将会等待被执行。这就是我们常说的 setTimeout 不准时的根本原因!
Each event loop has a microtask queue, which is a queue of microtasks, initially empty. A microtask is a colloquial way of referring to a task that was created via the queue a microtask algorithm. — HTML standard microtask
每个 Event Loop 有一个微任务队列,同时有一个 microtask checkpoint ,关于 perform-a-microtask-checkpoint 参考 这里。
Event Loop Processing model 中的第 8 步,Microtasks: Perform a microtask checkpoint. 即每执行完成一个宏任务后,就会 check 微任务。
以下,属于微任务(microtask/jobs):
所以,当某个 宏任务 执行完成后,会先执行 微任务 队列,执行完成后,再次获取新的 宏任务。这里微任务相当于插队操作!!
HTML standard 中是这样描述微任务执行时机的:
If the stack of script settings objects is now empty, perform a microtask checkpoint.
而,ES6 中对于 job 的执行是这样定义的:
Execution of a Job can be initiated only when there is no running execution context and the execution context stack is empty… — ECMAScript: Jobs and Job Queues
所以,从描述上看,job 和 microtask 很相似。
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
正确的执行结果: script start
, script end
, promise1
, promise2
, setTimeout