Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
creeperyang opened this issue May 5, 2016 · 59 comments
Open

Comments

@creeperyang
Copy link
Owner

creeperyang commented May 5, 2016

看到过下面这样一道题:

(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 机制,比如setTimeoutsetImmediate,或者 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 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
@heartAndRain
Copy link

heartAndRain commented Oct 31, 2016

有一个问题就是,I/O, UI render 究竟是不是当做一个task来执行,看有的文章说是,有的说是在每个task之间进行的

@creeperyang
Copy link
Owner Author

@cendylee 就我看到的资料来说,好像是在task之间进行的。如果有更详细可靠的资料进行补充或更正当然更好。

@hyj1991
Copy link

hyj1991 commented Feb 8, 2017

Promise的then原型方法注册的回调确实是在microtask中注册执行的,但是我很好奇,node实现的process.nextTick,看源码似乎并不是由microtask驱动的,为啥网上到处都说process.nextTick也是属于microtask的一部分呢?

@creeperyang
Copy link
Owner Author

creeperyang commented Feb 13, 2017

@hyj1991 从源码来看,process.nextTick 属于 microtask。

https://github.com/nodejs/node/blob/v7.x/src/node.cc#L4381

inline int Start(Isolate* isolate, IsolateData* isolate_data,
                 int argc, const char* const* argv,
                 int exec_argc, const char* const* exec_argv) {

...
  {
    Environment::AsyncCallbackScope callback_scope(&env);
    LoadEnvironment(&env);
  }

https://github.com/nodejs/node/blob/v7.x/src/node.cc#L3406

void LoadEnvironment(Environment* env) {
...
  Local<String> script_name = FIXED_ONE_BYTE_STRING(env->isolate(),
                                                    "bootstrap_node.js");
  Local<Value> f_value = ExecuteString(env, MainSource(env), script_name);

https://github.com/nodejs/node/blob/v7.x/lib/internal/bootstrap_node.js#L12

function startup() {
...
NativeModule.require('internal/process/next_tick').setup();

https://github.com/nodejs/node/blob/v7.x/lib/internal/process/next_tick.js#L49

  function scheduleMicrotasks() {
    if (microtasksScheduled)
      return;

    nextTickQueue.push({
      callback: runMicrotasksCallback,
      domain: null
    });

    tickInfo[kLength]++;
    microtasksScheduled = true;
  }

  ...

  function nextTick(callback) {
    if (typeof callback !== 'function')
      throw new TypeError('callback is not a function');
    // on the way out, don't bother. it won't get fired anyway.
    if (process._exiting)
      return;

    var args;
    if (arguments.length > 1) {
      args = new Array(arguments.length - 1);
      for (var i = 1; i < arguments.length; i++)
        args[i - 1] = arguments[i];
    }

    nextTickQueue.push({
      callback,
      domain: process.domain || null,
      args
    });
    tickInfo[kLength]++;
  }

可以看到,安排microtask就是向nextTickQueue队列压入callback,而nextTick同样是向nextTickQueue队列压入callback,可见,process.nextTick 属于 microtask

20170629 更新代码路径

@Ma63d
Copy link

Ma63d commented Mar 3, 2017

很清晰的文章!
我也是有同样的疑惑:到底UI render是在哪执行的?
博主您给出的HTML规范链接里面的这部分内容我反复读了,但是Processing model的第5点Update the rendering之后说的 run the resize steps run the scroll steps这些步骤是什么意思呢?不可能每次microtask执行完就resize和scroll吧,所以请问博主你看懂这块了吗?

@creeperyang
Copy link
Owner Author

@Ma63d 你给的链接里描述很清晰,microtask跟你说的这些无关。

链接描述,一个event loop的典型步骤:

  1. Select the oldest task...
  2. Set the event loop's currently running task to the task selected in the previous step.
  3. Run: Run the selected task.
  4. Set the event loop's currently running task back to null.
  5. Remove the task that was run in the run step above from its task queue.
  6. Microtasks: Perform a microtask checkpoint.
  7. Update the rendering
  8. If this is a worker event loop...
  9. Return to the first step of the event loop.

Update the rendering是event loop的一个步骤,Microtasks也是event loop的一个步骤,在执行完Microtasks,浏览器开始Update the rendering,至于run the resize steps等是Update the rendering里面的内容,跟Microtasks无关。

@Ma63d
Copy link

Ma63d commented Mar 5, 2017

@creeperyang 嗯,我当时的表示有误,我问的意思是执行完所有的microtask之后执行的Update the rendering,不是说单独执行一个之后立马执行run the resize steps/run the scroll steps。我当时描述不准确,不好意思把你引入到了莫名的关注点上面去了。

我当时一开始没看懂,为什么Update the rendering竟然去执行resize窗口和scroll窗口。不可能每次task执行完,然后清空完microtask队列之后就让屏幕resize/scroll一次吧。所以评论了一开始的内容。

最近仔细看了一下run the scroll steps不是scoll窗口,每次我们scoll的时候视口或者dom就已经立即scroll了,并把document或者dom加入到 pending scroll event targets中,而run the scroll steps具体做的则是遍历这些target,在target上触发scroll事件。
run the resize steps也是相似的,这个步骤是触发resize事件。

至于后续的media query, run CSS animations and send events等等也是相似的,都是触发事件,第10步和第11步则是执行我们熟悉的requestAnimationFrame回调和IntersectionObserver回调(第十步还是挺关键的)。

第十二步才是对UI执行render,这里应该就是重排、重绘和把更改后的样式真正render改到dom上面去。

@zyg-github
Copy link

SetTimeout 内的回调属于 macrotask, 会在下一个 Event Loop 中执行

@keenwon
Copy link

keenwon commented Mar 11, 2017

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

  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。

关于第2,3步,不是promise.resolve()的时候,promise.then的callback已经被添加到microtask queue了吗?然后输出2,然后跳到第四步输出3.

@creeperyang
Copy link
Owner Author

@keenwon

为了让第2,3步更清晰一点,可以写成下面这样。

// 1
setTimeout();
// 2
var promise = new Promise(executor);
// 3
promise.then(callback)
// 4
console.log(3)

其中,得到Promise的实例promise的时候,exectuor作为参数传给Promise的构造函数同步执行。所以输出了数字12

构造函数执行完后,我们得到了promise(它是resolved)。

调用promise.thencallback被添加到microtasks的队列中。

console.log(3)执行完后,当前执行栈为空,则开始执行microtasks。

@keenwon
Copy link

keenwon commented Apr 27, 2017

@mqliutie
你这例子不对吧?你的 setTimeoutnew Promise 内部呀,大兄弟;setTimeout 不执行 then 也永远不执行,这是有明显的先后顺序的

@keenwon
Copy link

keenwon commented Apr 27, 2017

image

image

@mqliutie
Copy link

@keenwon 通过这个查看调用栈和任务队列

调用栈为什么还没有清空,就执行了任务队列的定时任务?

@jawil
Copy link

jawil commented May 12, 2017

学习学习

@qinyang912
Copy link

应该是每个浏览器至少有一个 event loop 吧

@creeperyang
Copy link
Owner Author

creeperyang commented Jun 3, 2017

补充关于 同步/异步/阻塞/非阻塞 的理解。搬迁自已废弃的#15,因为觉得和 event loop 强相关,放到一起便于参照。

Synchronize / Asynchronize / Block / Non-block 一个从分布式系统角度的理解

这一段主要来自 知乎 怎样理解阻塞非阻塞与同步异步的区别? 严肃的答案 ,并参照了 stackoverflow 的相关问题。

同步与异步,阻塞与非阻塞是两组概念,但容易混淆,比如同步不代表阻塞,同步也可以是非阻塞的。

同步与异步

  1. 同步和异步关注的是 消息通信机制 (synchronous communication/ asynchronous communication)

  2. 所谓同步,就是在发出调用时,

    1. 在没有得到结果之前,调用不返回。
    2. 一旦调用返回,即得到返回值。

    换句话说,就是由 调用者主动等待 这个调用的结果。

  3. 异步则是相反,调用在发出之后,调用就直接返回,但没有返回结果。

    换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

阻塞与非阻塞

  1. 阻塞和非阻塞关注的是 程序在等待调用结果(消息,返回值)时的状态

  2. 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。

  3. 非阻塞调用指在不能立刻得到结果之前,该调用 不会阻塞当前线程

举例

以你打电话让书店老板查找某本书为例来讲:

  • 同步通信机制:老板说,“你稍等,我查下”,然后查好后(可能会5秒或者1天)告诉你结果(返回结果)。
  • 异步通信机制:老板说,“我查好后打电话你”,然后直接挂电话(无返回结果)。查好后打电话你(“回电”这种方式回调/通知你)。
  • 阻塞式调用:打电话给老板时,你会一直把自己“挂起”,直到结果返回。
  • 非阻塞式调用:打电话给老板时,不管老板有没有告诉你,你自己先一边玩了,但可能会每过几分钟跟老板check一下有没有返回结果。

总结:阻塞与非阻塞 与 是否同步异步无关(跟老板通过什么方式回答你无关)。 所以,也可以说, 同步/异步 针对的是 通信机制(被调用方怎么通知调用方),阻塞/非阻塞 针对的是 调用方在等待结果时的状态。

此外,同步/异步 和 阻塞/非阻塞 可以相互组合 (from 吴昌明 的评论):

  • 同步阻塞:你打电话问老板有没有某书,老板去查,在老板给你结果之前,你一直拿着电话等待老板给你结果,你此时什么也干不了。
  • 同步非阻塞:你打电话过去后,在老板给你结果之前,你拿着电话等待老板给你结果,但是你拿着电话等的时候可以干一些其他事,比如嗑瓜子。
  • 异步阻塞:你打电话过去后,老板去查,你挂掉电话,等待老板给你打电话通知你,这是异步,你挂了电话后还是啥也干不了,只能一直等着老板给你打电话告诉你结果,这是阻塞。
  • 异步非阻塞:你打电话过去后,你就挂了电话,然后你就想干嘛干嘛去。只用时不时去看看老板给你打电话没。

这个回答我觉得是很清晰,易于理解的解释,不过下面还是会列出一些其它角度的解释,方便对照吧。

Asynchronous vs synchronous execution, what does it really mean?

这是关于同步/异步执行的理解,几个高票答案总结下可知:

  1. 同步执行是每个task必须结束后才能继续下个task,前后task是顺序的,有依赖的。
  2. 异步执行是多个task可以并行,相互不依赖。

这也是一个角度,可参照。

@szouc
Copy link

szouc commented Jun 3, 2017

是否可以这样认为,同步异步是两者的通信方式,CPU 同 I/O 或数据库。阻塞非阻塞是个体的工作方式,进程的单线程、多线程。单线程工作方式的进程就是阻塞的,多线程工作方式的进程就是非阻塞的。

NodeJS 应该是利用了事件循环既实现了两者间的异步通信,又解决了单线程的阻塞难题。(对比时分复用,将单线程按事件划分成虚拟的多线程,但是对CPU密集程序来说它还是阻塞的)

@creeperyang
Copy link
Owner Author

creeperyang commented Jun 4, 2017

@szouc 阻塞/非阻塞和单/多线程没有对应关系,同步异步和线程也没有关系。

在Node.js中,JavaScript是单线程的,所以必然不能使用阻塞IO(阻塞就没法实现高并发)。

Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient.

通过libuv,Node.js 在多平台支持了非阻塞IO:

  1. Network IO,依赖Linux上的epoll,OSX和BSD类OS上的kqueue,SunOS上的event ports以及Windows上的IOCP等内核事件通知机制。
  2. File IO,使用thread pool (线程池,多线程)。

@sponia-joker
Copy link

请教博主一个问题,烦请指教!当我反复查看html规范时候

An event loop has one or more task queues. A task queue is an ordered list of tasks, which are algorithms that are responsible for such work as:

  1. Events
    Dispatching an Event object at a particular EventTarget object is often done by a dedicated task.
    2.Parsing
    The HTML parser tokenizing one or more bytes, and then processing any resulting tokens, is typically a task.
    ....

规范里面说到event loop有多个task queues.一个task queue就是任务的列表,它们负责以下工作。
看里很久有两个疑惑,恳请解答
1.dispatching an event 不是浏览器有专门的线程监听各种事件吗?然后相应的event handler 会进入task queues.为什么规范里说:是任务队列Dispatching an Event。因为任务队列里的任务都会被JavaScript engines执行,也就是说是JavaScript engines在触发事件?
2.html解析不是通过渲染引擎来解析的吗,怎么这里又说是JavaScript engines解析?在一次event loop中,如果页面需要更新。这时候会停止JavaScript引擎,启动渲染引擎来更新页面吗?
谢谢了
@creeperyang

@googya
Copy link

googya commented Feb 22, 2018

new Promise(resolve => {
    resolve(1);
    console.log(4)
    // new Promise(r => r(2)).then(e => console.log(e))
    Promise.resolve(2).then(e => console.log(e))
    Promise.resolve(5).then(e => console.log(e))
}).then(v => console.log(v))

console.log(3)

在 Promise 中, 如果还有 promise, 那么先得把内部的 promise 顺序执行完,然后执行外面的 promise? 我是根据得到的结果推理出来的, 暂时没看到对应的文档说明, issue 主怎么看?

@creeperyang
Copy link
Owner Author

@googya

// 执行第 1 步
const p = new Promise(resolve => {
    resolve(1);
    // 第 1.1 步,首先输出 4 
    console.log(4)
    // 第 1.2 步,压入microtask 队列
    Promise.resolve(2).then(e => console.log(e))
    // 第 1.3 步,压入microtask 队列
    Promise.resolve(5).then(e => console.log(e))
})

// 执行第 2 步,压入microtask 队列
p.then(v => console.log(v))

// 执行第 3 步
console.log(3)

所以输出结果:

4  --> 3,然后microtask队列支持输出 2 --> 5 --> 1

@googya
Copy link

googya commented Feb 22, 2018

@creeperyang 步骤分解之后是更清楚一点。 我之前的疑问是, 为什么 1.2, 1.3 先于步骤2,只是因为 1.2 1.3 在构造器中, 先执行的吧

@creeperyang
Copy link
Owner Author

creeperyang commented Apr 12, 2018

标题:从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理
作者:Lichun Dai

一篇从浏览器进程/线程角度来解释浏览器环境中JS运行机制的文章,通俗易懂,对理解浏览器环境的 event loop 很有帮助。

核心知识点:

  1. 现代浏览器(如Chrome)每个tab页对应一个独立进程,有独立的渲染引擎(浏览器内核)实例,内核一般包括以下线程:

    +-------------------------------+
    |+-----------------------------+|
    ||         GUI渲染线程          ||
    ||                             ||
    |+-----------------------------+|
    |                               |
    |+-----------------------------+|
    ||         JS引擎线程           ||
    ||                             ||
    |+-----------------------------+|
    |                               |
    |+-----------------------------+|
    ||         事件触发线程          ||
    ||                             ||
    |+-----------------------------+|
    |                               |
    |+-----------------------------+|
    ||        定时器触发线程         ||
    ||                             ||
    |+-----------------------------+|
    |                               |
    |+-----------------------------+|
    ||       异步http请求线程        ||
    ||                             ||
    |+-----------------------------+|
    |                               |
    +-------------------------------+
    
  2. JS 线程用于解析和执行 JavaScript。图中显见:一个渲染引擎进程只有一个 JS 线程——JS 是单线程的。那么JS的异步能力是谁赋予的?JS引擎本身只能不断地去检索任务队列,取出开头的任务去执行(即所谓的event loop),那么 setTimeout/事件回调/ajax请求回调 等异步能力仅仅靠 JS 线程本身是做不到的,上面的图正好可以让初学者们恍然大悟:

    setTimeout 等本质上是 JS 线程调用了其它线程,其它线程在条件达成时把任务塞入队列。

  3. UI 线程和 JS 线程是互相阻塞的,所以不要让 JS 线程长时间运行,从而让 UI 线程被长时间挂起,使 UI 渲染掉帧甚至不响应。

@peze
Copy link

peze commented May 19, 2018

我最近正好看到这个地方,颇有疑惑之处,首先,node的makecallback 有调用 env()->tick_callback_function()->Call;所以我刚开始感觉nextTick是在事件循环的回调中调用。但是实际上比如用setTimeout(fn,0)这个是标准的libuv timer然后回调执行fn,那应该是在执行完fn以后再执行nexttick,但实际不是,所以我比较同意是Microtasks来执行的,而且应该不是在同一个线程上,因为node启动platform的时候用了4个线程做线程池,然后再node::start里uv_run前面有个PumpMessageLoop更加重了我的猜测,就是这个 是microtasks来做的nexttick。而且如果我通过执行自己的embed函数 在函数中直接运行uv_run,不通过node::start里面的uv_run执行,nextTick是不会被执行的。

@xianshenglu
Copy link

xianshenglu commented May 26, 2018

@watsonnnnn
Copy link

楼主能说下 tick和loop是啥区别吗?

@wroy1005
Copy link

<script type="text/javascript">
	setTimeout(function(){
		console.log(1);
	})
	Promise.resolve().then(function(){
		console.log(2);
	})
	console.log(3);
	document.write('<script type="text/javascript">console.log(4)<\/script>');
</script>
<script type="text/javascript">
	document.write('<script type="text/javascript">console.log(5)<\/script>');
	console.log(6)
</script>

结果是什么?

342561

@AdvancedCat
Copy link

楼主好,根据最近一次的新闻,WHATWG全权接管了HTML和DOM的标准制定权(链接)。
您在博文中HTML5标准的引用就有点过时了。而且根据最新标准制定Task Queue不是一个队列,它是一个任务集合(A task queue is a set of tasks)。
与时俱进一下。

@NotCoderJack
Copy link

NotCoderJack commented Oct 30, 2020

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

终于解答了最近被一小段代码的困扰:

var c1 = (r) => {
    console.log(r)
}
var c2 = () => {
    console.log(3)
}

console.log('promise1')
var p3
var p1 = Promise.resolve()
.then(function() {
    console.log(0)
    p3 = Promise.resolve(6)
    return p3
})
.then(c1)


console.log('promise2')
var p2 = Promise.resolve()
.then(function() {
    console.log(1)
})
.then(function() {
    console.log(2)
})
.then(function(){
    console.log('before 3')
    c2()
    console.log('after 3')
}).then(function() {
    console.log(5)
})
/**
log:
promise1
promise2
0
1
2
6
before 3
3
after 3
5
*/

在低版本Node中,输出结果如上,而高版本我用的V12,则6会在after 3后面输出。

但是规范里并没有说,多个Job Queue如何调度(可能有交叉调度),这也导致没法正确推导代码输出结果。哪位有了解多个Job Queue的调度方式,欢迎告知或提供学习资料,感谢!

@creeperyang
Copy link
Owner Author

creeperyang commented Mar 16, 2021

浏览器 event loop 简易归纳:

关于 taskQueue

  • 首先浏览器可以拥有不止一个 taskQueue。易于理解的一个解释是,不同 taskQueue 可以放不同类型的 task,比如处理鼠标/键盘事件的可以放一个 taskQueue,便于优先响应用户操作。(但不能饿死其它 taskQueue)。
  • taskQueue 不是 queue,而是 set。每次取最先可以运行的 task,而不是队列头部的 task。

关于运行流程(已简化)

  1. 取一个 taskQueue,并且 taskQueue 有 runnable 的 task(没有则跳到 microtask 那一步);
  2. 从 taskQueue 取出当前要运行的 task 并执行;
  3. 执行完检查 microtask queue 是否不为空,不为空则 dequeue microtask 执行,直到 microtask queue 为空;
  4. 执行 UI 更新(Update the rendering):
    • 理解"Rendering opportunities",比如 60HZ 刷新率下,那么1秒内最多有 60次 Rendering opportunities;如果浏览器没法满足60Hz,那么可能就会降级到30Hz(30次opportunities)而不是降几次;如果浏览器tab页不可见,那么甚至会降到4Hz。
    • rendering 的顺序:处理autofocus-->resize-->scroll-->media query-->update animation-->fullscreen-->animation frame callback --> intersection observation --> mark painting timing
  5. 更新完UI后,看看是不是有机会触发 Idle Callback。

一些重要的执行顺序

Promise --> requestAnimationFrame --> Paint --> requestIdleCallback

一个有意思的实例

Vue.nextTick 为什么能保证在DOM更新后执行? 具体情况见异步更新队列

  1. watcher 代码来看,vue 的数据绑定机制可以监听 vm 数据的更新,但不会同步更新DOM,而是内部调用 queueWatcher,把数据对应的 watcher 塞入队列,并调用 nextTick(仅一次,保证队列后面会被处理,即更新DOM,而后面vm再变动,只是塞入队列);
  2. 所以vm的更新也是调用 Vue.nextTick ,都是通过 Promise 或其它API 生成 microtask;
  3. 因为microtask 队列的先后关系,vm 的更新导致的 DOM 更新被先处理,所以可以通过 Vue.nextTick 保证访问到最新的 DOM 状态;
  4. 最后明确:DOM 更新,和屏幕上的UI更新不是一个概念。DOM 更新可以理解为 JS 的执行,真正的 Paint 时机仍然是浏览器按规则执行——最终符合上一节的执行顺序。

@fashen007
Copy link

image
image
这两个执行结果bing'bu'y并不一样,大佬能解释一下原理吗

@creeperyang
Copy link
Owner Author

creeperyang commented Mar 25, 2021

@spademan 我实际测了下,直接btn.click() 和 页面点击按钮输出不一样。

image

可以看到,btn.click() 时,两个 handler 显然合并在同一个 event loop 执行了;而在页面里点击按钮则不一样。这个是浏览器自身实现细节,我没有查到相关文档,估计要去看相关代码才能知道原因了。

我的猜测是:用户点击,是属于用户交互,浏览器认为在UI上需要立即反馈,于是每处理完一个handler,立即更新页面UI。(看看就行,别当真 😸 有官方解释可告知我)

@zlv2s
Copy link

zlv2s commented Mar 27, 2021

@spademan 用户点击,事件是异步触发,点击之后

  1. Promise.resolve(), 然后往微任务队列里面放一个任务(log('M1'))
  2. 打印 'L1'
  3. 这时候 call stack 是空闲的,会执行微任务队列里的任务,打印 'M1'
  4. 第二个事件处理程序也是同样如此

用程序触发 click, 事件是同步触发

  1. btn.click() 进入 call stack
  2. Promise.resolve(), 然后往微任务队列里面放一个任务
  3. 打印 'L1'
  4. 此时,call stack 并没有空闲,click 函数还在里面,所以不会执行微任务队列里的任务
  5. 执行第二个事件处理程序,Promise.resolve(),然后往微任务队列里面放一个任务(log('M2'))
  6. 打印 'L2'
  7. 事件处理程序结束,click 函数移出 call stack
  8. 开始执行微任务队列里的任务,打印 'M1', 'M2'

不晓得对不对。。。。

@fashen007
Copy link

image
image
这两个执行结果bing'bu'y并不一样,大佬能解释一下原理吗
@creeperyang @zlv2s 找到一个相对深入一点的解释
https://stackoverflow.com/questions/55709512/why-is-there-a-difference-in-the-task-microtask-execution-order-when-a-button-is

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests