Promise和setTimeout执行顺序 面试题

看到过下面这样一道题:html

(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 ?html5

比较难回答,但咱们能够首先说一说能够从输出结果反推出的结论:web

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

相关规范摘录

在解答问题前,咱们必须先去了解相关的知识。(这部分至关枯燥,想看结论的同窗能够跳到最后便可。)windows

Promise/A+ 规范api

要想找到缘由,最天然的作法就是去看规范。咱们首先去看看 Promise的规范 。promise

摘录 promise.then 相关的部分以下:浏览器

promise.then(onFulfilled, onRejected)app

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

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 规范,看看 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 ) :

这里咱们看到, 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 callbacks 和 promise 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 。
相关文章
相关标签/搜索