您当前的位置:首页 > 计算机 > 编程开发 > JavaScript

从 Promise 来看 JavaScript 中的 Event Loop、Tasks 和 Microtasks

时间:12-14来源:作者:点击数:

看到过下面这样一道题:

(function test() {
    setTimeout(function() {console.log(4)}, 0);
    new Promise(function executor(resolve) {
        console.log(1);
        for( var i=0 ; i<10000 ; i++ ) {
            i == 9999 && resolve();
        }
        console.log(2);
    }).then(function() {
        console.log(5);
    });
    console.log(3);
})()

为什么输出结果是 1,2,3,5,4 而非 1,2,3,4,5

比较难回答,但我们可以首先说一说可以从输出结果反推出的结论:

  1. Promise.then 是异步执行的,而创建 Promise 实例(executor)是同步执行的。
  2. setTimeout 的异步和Promise.then 的异步看起来 不太一样 ——至少是不在同一个队列中。

相关规范摘录

在解答问题前,我们必须先去了解相关的知识。(这部分相当枯燥,想看结论的同学可以跳到最后即可。)

Promise/A+ 规范

要想找到原因,最自然的做法就是去看规范。我们首先去看看Promise的规范

摘录promise.then相关的部分如下:

promise.then(onFulfilled, onRejected)

2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].

Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.

规范要求,onFulfilled必须在 执行上下文栈(execution context stack) 只包含 平台代码(platform code) 后才能执行。平台代码指 引擎,环境,Promise实现代码。实践上来说,这个要求保证了onFulfilled的异步执行(以全新的栈),在then被调用的这个事件循环之后。

规范的实现可以通过 macro-task 机制,比如setTimeout和 setImmediate,或者 micro-task 机制,比如MutationObserver或者process.nextTick。因为promise的实现被认为是平台代码,所以可以自己包涵一个task-scheduling队列或者trampoline

通过对规范的翻译和解读,我们可以确定的是promise.then是异步的,但它的实现又是平台相关的。要继续解答我们的疑问,必须理解下面几个概念:

  1. Event Loop,应该算是一个前置的概念,理解它才能理解浏览器的异步工作流程。
  2. macro-task 机制和 micro-task 机制,这组概念很新,之前根本没听过,但却是解决问题的核心。

Event Loop 规范

HTML5 规范里有 Event loops 这一章节(读起来比较晦涩,只关注相关部分即可)。

  1. 每个浏览器环境,至多有一个 event loop。
  2. 一个 event loop 可以有1个或多个 task queue。
  3. 一个 task queue 是一列有序的task,用来做以下工作:Events task,Parsing task, Callbacks task, Using a resource task, Reacting to DOM manipulation task等。

每个 task 都有自己相关的 document,比如一个task在某个element的上下文中进入队列,那么它的 document 就是这个 element 的 document。

每个task定义时都有一个 task source,从同一个 task source 来的task必须放到同一个 task queue,从不同源来的则被添加到不同队列。

每个(task source对应的)task queue 都保证自己队列的先进先出的执行顺序,但 event loop 的每个 turn,是由浏览器决定从哪个task source 挑选 task。这允许浏览器为不同的 task source 设置不同的优先级,比如为用户交互设置更高优先级来使用户感觉流畅。

Jobs and Job Queues 规范

本来应该接着上面 Event Loop 的话题继续深入,讲 macro-task 和 micro-task,但先不急,我们跳到 ES2015 规范( ecma-international 组织网/ecma-262/6.0/index.html),看看 Jobs and Job Queues 这一新增的概念,它有点类似于上面提到的 task queue

一个 Job Queue 是一个先进先出的队列。一个 ECMAScript 实现必须至少包含以下两个 Job Queue

Name Purpose
ScriptJobs Jobs that validate and evaluate ECMAScript Script and Module source text. See clauses 10 and 15.
PromiseJobs Jobs that are responses to the settlement of a Promise (see 25.4).

单个 Job Queue 中的PendingJob总是按序(先进先出)执行,但多个 Job Queue 可能会交错执行。

跟随 PromiseJobs 到 25.4 章节,可以看到 PerformPromiseThen ( promise, onFulfilled, onRejected, resultCapability )( ecma-international 组织网/ecma-262/6.0/index.html#sec-performpromisethen):

这里我们看到,promise.then的执行其实是向PromiseJobs添加Job。

event loop 怎么处理 tasks 和 microtasks?

好了,现在可以让我们真正来深入 task(macro-task)和 micro-task。认真说,规范并没有包括 macro-task 和 micro-task 这部分概念的描述,但阅读一些大神的博文以及从规范相关概念推测,以下所提到的在我看来,是合理的解释。但是请看文章的同学辩证和批判地看。

首先,micro-task 在 ES2015 规范中称为 Job。 其次,macro-task 代指 task。

哇,所以我们可以结合前面的规范,来讲一讲 Event Loop(事件循环)是怎么来处理task和microtask的了。

  1. 每个线程有自己的事件循环,所以每个 web worker 有自己的,所以它才可以独立执行。然而,所有同属一个 origin 的 windows 共享一个事件循环,所以它们可以同步交流。
  2. 事件循环不间断在跑,执行任何进入队列的 task。
  3. 一个事件循环可以有多个 task source,每个 task source 保证自己的任务列表的执行顺序,但由浏览器在(事件循环的)每轮中挑选某个 task source 的 task。
  4. tasks are scheduled,所以浏览器可以从内部到 JS/DOM,保证动作按序发生。在tasks之间,浏览器可能会 render updates。从鼠标点击到事件回调需要 schedule task,解析 html,setTimeout 这些都需要。
  5. microtasks are scheduled,经常是为需要直接在当前脚本执行完后立即发生的事,比如 async 某些动作但不必承担新开task的弊端。microtask queue在回调之后执行,只要没有其它JS在执行中,并且在每个task的结尾。microtask 中添加的 microtask 也被添加到 microtask queue 的末尾并处理。microtask包括mutation observer callbackspromise callbacks

结论

定位到开头的题目,流程如下:

  1. 当前 task 运行,执行代码。首先 setTimeout 的 callback 被添加到 tasks queue 中;
  2. 实例化 promise,输出 1; promise resolved;输出 2;
  3. promise.then 的 callback 被添加到microtasks queue中;
  4. 输出 3;
  5. 已到当前 task 的 end,执行 microtasks,输出 5;
  6. 执行下一个 task,输出4
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门
本栏推荐