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

从HTML5与PromiseA+规范来看事件循环 #13

Open
Aaaaaaaty opened this issue Aug 10, 2017 · 6 comments
Open

从HTML5与PromiseA+规范来看事件循环 #13

Aaaaaaaty opened this issue Aug 10, 2017 · 6 comments

Comments

@Aaaaaaaty
Copy link
Owner

Aaaaaaaty commented Aug 10, 2017

写在最前

本次分享一下从HTML5PromiseA+规范来迅速理解一波事件循环中的microtask 与macrotask。

欢迎关注我的博客,不定期更新中——

JavaScript小众系列开始更新啦

——何时完结不确定,写多少看我会多少!这是已经更新的地址:

这个系列旨在对一些人们不常用遇到的知识点,以及可能常用到但不曾深入了解的部分做一个重新梳理,虽然可能有些部分看起来没有什么用,因为平时开发真的用不到!但个人认为糟粕也好精华也罢里面全部蕴藏着JS一些偏本质的东西或者说底层规范,如果能适当避开舒适区来看这些小细节,也许对自己也会有些帮助~文章更新在我的博客,欢迎不定期关注。

先来看段代码

setTimeout(function() {
  console.log('setTimeout1');
    Promise.resolve().then(function() {
      console.log('promise1');
    }).then(function() {
      console.log('promise2');
    })
}, 0);
setTimeout(function() {
  console.log('setTimeout2');
    Promise.resolve().then(function() {
      console.log('promise3');
    }).then(function() {
      console.log('promise4');
    })
}, 0);

从这段代码中我们发现里面有两个定时器setTimeout,每个定时器中还嵌套了Promise。我相信熟悉microtask 与macrotask任务队列的童鞋能很快的知晓答案,这个东西给我的感觉就是清者自清。

so 结果是什么?

/* 请在新版chrome中打印结果
    setTimeout1
    promise1
    promise2
    setTimeout2
    promise3
    promise4
*/

why?

不做解释直接看下规范中怎么说的:

There must be at least one browsing context event loop per user agent, and at most one per unit of related similar-origin browsing contexts. An event loop has one or more task queues.

一个浏览器环境下只能有一个事件循环,同时循环中是可以存在多个任务队列的。
同时我们接着看规范中对event-loop执行过程是如何规定的:

1.Let oldestTask be the oldest task on one of the event loop's task queues.

2.Set the event loop's currently running task to oldestTask.

3.Run oldestTask.

4.Set the event loop's currently running task back to null.

5.Remove oldestTask from its task queue.

6.Microtasks: Perform a microtask checkpoint.

7.Update the rendering

其中的task queues,就是之前提到的macrotask,中文可以翻译为宏任务。顾名思义也就是正常的一些回调执行,比如IO,setTimeout等。简单来说当事件循环开始后,会将task queues最先进栈的任务执行,之后移出,进行到第六步,做microtask的检测。发现有microtask的任务那么会依照如下方式执行:

While the event loop's microtask queue is not empty:

//当microtask队列中还有任务时,按照下面执行

1.Let oldestMicrotask be the oldest microtask on the event loop's microtask queue.

2.Set the event loop's currently running task to oldestMicrotask.

3.Run oldestMicrotask.

4.Set the event loop's currently running task back to null.

5.Remove oldestMicrotask from the microtask queue.

从这段规范可以看出,当执行了一个macrotask后会有一个循环来检查microtask队列中是否还存在任务,如果有就执行。这说明执行了一个macrotask(宏任务)之后,会执行所有注册了的microtask(微任务)。

一起看起来很正常对吧?

那么如果微任务“嵌套”了呢?就像一开始作者给出的那段代码一样,promise调用了很多次.then方法。这种情况文档中有做出规定么?有的。

If, while a compound microtask is running, the user agent is required to execute a compound microtask subtask to run a series of steps, the user agent must run the following steps:

1.Let parent be the event loop's currently running task (the currently running compound microtask).

2.Let subtask be a new task that consists of running the given series of steps. The task source of such a microtask is the microtask task source. This is a compound microtask subtask.

3.Set the event loop's currently running task to subtask.

4.Run subtask.

5.Set the event loop's currently running task back to parent.

简单来说如果有“嵌套”的情况,注册的任务都是microtask,那么就会一股脑得全部执行。

小结

通过上面对文档的解读我们可以知道以下几件事:

  1. 一个运行环境有一个事件循环。PS:有关web worker的概念作者也不太清楚,有兴趣的童鞋可以查查
  2. #重点# 一个事件循环有多个任务队列。目前来看是实现了两个队列
  3. 队列分为macrotask宏任务队列与microtask微任务队列
  4. 回调的任务会被分配到macrotask与microtask中,具体分配见下文。
  5. 执行一个宏任务,将已经注册的所有微任务,包括有“嵌套”的全部执行。
  6. 执行下一个宏任务,重复步骤5

那么还剩一件事情就是什么任务是macrotask,什么是microtask?
image.png
这张图来源一篇翻译PromisA+的文章,里面所提到的关于任务的分类。

但是!我对于setImmediate与process.nextTick的行为持怀疑态度。理由最后说!不过在浏览器运行环境中我们不需要关系上面那两种事件。

测试一下代码

在本文一开始就提出,这段代码要在新版chrome中运行才会得到正确结果。那么不在chrome中呢?

safari

举个例子,别的作者不一一测试了,这是safari中的结果。我们可以看到顺序被打乱了。so为什么我执行了一样的代码结果却不同?
个人认为若出现结果不同的情况是由于不同执行环境(chrome, safari, node .etc)将回调需要执行的任务所划分到的任务队列PromiseA+规范中所提到的任务队列中的任务划分准则执行不一致导致的。也就是Promise可能被划分到了macrotask中。有兴趣深入了解的童鞋可以看下这篇tasks-microtasks-queues-and-schedules.

抛一个作者也解释不清的问题

细心的童鞋可能发现我一直强调的js运行环境是浏览器下的事件循环情况。那么node中呢?

setTimeout(function() {
  console.log('setTimeout1');
    Promise.resolve().then(function() {
      console.log('promise1');
    }).then(function() {
      console.log('promise2');
    })
}, 0);
setTimeout(function() {
  console.log('setTimeout2');
    Promise.resolve().then(function() {
      console.log('promise3');
    }).then(function() {
      console.log('promise4');
    })
}, 0);

还是这段代码,打印出来会不会有区别?多打印几次结果一样么?为什么会这样?

我只能理解到node通过libuv实现事件循环的方式与规范没有关系,但具体为什么会打印出不同的效果。。求大神@我

最后

不定时更新中——
有问题欢迎在issues下交流。

@Aaaaaaaty Aaaaaaaty changed the title 从HTML与PromiseA+规范来看事件循环 从HTML5与PromiseA+规范来看事件循环 Aug 10, 2017
@ipengyo
Copy link

ipengyo commented Sep 29, 2017

make 这种问题很纠结人啊。

@Aaaaaaaty
Copy link
Owner Author

@ipengyo 主要是规范就一套,不同环境自己实现,最后的结果可能都不太一致=。=

@xhlwill
Copy link

xhlwill commented Apr 8, 2018

safari 11 测试了几次,和 chrome 一样
2018-04-08 2 48 46

@xhlwill
Copy link

xhlwill commented Apr 8, 2018

另:《从hello world看JavaScript隐藏的黑魔法制》链接好像错了

@Aaaaaaaty
Copy link
Owner Author

@xhlwill 已修正 谢谢指出~

@stickmy
Copy link

stickmy commented Aug 6, 2018

node 部分其实不难理解, 看 ryf 这篇 http://www.ruanyifeng.com/blog/2018/02/node-event-loop.html, timers 阶段会执行到期的 timers,两个 setTimeout 到期了会先执行,每执行一次 macroTask 会执行一轮 microTask,所以 Promise 会在之后执行

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

4 participants