看到过下面这样一道题:
(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
?
比较难回答,但我们可以首先说一说可以从输出结果反推出的结论:
Promise.then
是异步执行的,而创建Promise实例( executor
)是同步执行的。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
是异步的,但它的实现又是平台相关的。要继续解答我们的疑问,必须理解下面几个概念:
Event Loop
规范
HTML5 规范里有 Event loops 这一章节(读起来比较晦涩,只关注相关部分即可)。
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。
好了,现在可以让我们真正来深入task(macro-task)和micro-task。
认真说,规范并没有包括macro-task 和 micro-task这部分概念的描述,但阅读一些大神的博文以及从规范相关概念推测,以下所提到的在我看来,是合理的解释。但是请看文章的同学辩证和批判地看。
首先, micro-task在ES2015规范中称为Job。 其次,macro-task代指task。
哇,所以我们可以结合前面的规范,来讲一讲Event Loop(事件循环)是怎么来处理task和microtask的了。
mutation observer callbacks
和 promise callbacks
。定位到开头的题目,流程如下:
setTimeout
的callback被添加到tasks queue中;1
; promise resolved;输出 2
;promise.then
的callback被添加到microtasks queue中;3
;5
;4
。原文:https://www.cnblogs.com/yzhihao/p/9383822.html