cc icon indicating copy to clipboard operation
cc copied to clipboard

45.理解事件循环二(macrotask和microtask)

Open ccforward opened this issue 7 years ago • 30 comments

关于 macrotask 和 microtask

上一篇 理解事件循环一(浅析) 用例子简单理解了下 macrotask 和 microtask

这里再详细的总结下两者的区别和使用

简介

一个事件循环(EventLoop)中会有一个正在执行的任务(Task),而这个任务就是从 macrotask 队列中来的。在whatwg规范中有 queue 就是任务队列。当这个 macrotask 执行结束后所有可用的 microtask 将会在同一个事件循环中执行,当这些 microtask 执行结束后还能继续添加 microtask 一直到真个 microtask 队列执行结束。

怎么用

基本来说,当我们想以同步的方式来处理异步任务时候就用 microtask(比如我们需要直接在某段代码后就去执行某个任务,就像Promise一样)。

其他情况就直接用 macrotask。

两者的具体实现

  • macrotasks: setTimeout setInterval setImmediate I/O UI渲染
  • microtasks: Promise process.nextTick Object.observe MutationObserver

从规范中理解

whatwg规范:https://html.spec.whatwg.org/multipage/webappapis.html#task-queue

  • 一个事件循环(event loop)会有一个或多个任务队列(task queue) task queue 就是 macrotask queue
  • 每一个 event loop 都有一个 microtask queue
  • task queue == macrotask queue != microtask queue
  • 一个任务 task 可以放入 macrotask queue 也可以放入 microtask queue 中
  • 当一个 task 被放入队列 queue(macro或micro) 那这个 task 就可以被立即执行了

再来回顾下事件循环如何执行一个任务的流程

当执行栈(call stack)为空的时候,开始依次执行:

  1. 把最早的任务(task A)放入任务队列
  2. 如果 task A 为null (那任务队列就是空),直接跳到第6步
  3. 将 currently running task 设置为 task A
  4. 执行 task A (也就是执行回调函数)
  5. 将 currently running task 设置为 null 并移出 task A
  6. 执行 microtask 队列
    • a: 在 microtask 中选出最早的任务 task X
    • b: 如果 task X 为null (那 microtask 队列就是空),直接跳到 g
    • c: 将 currently running task 设置为 task X
    • d: 执行 task X
    • e: 将 currently running task 设置为 null 并移出 task X
    • f: 在 microtask 中选出最早的任务 , 跳到 b
    • g: 结束 microtask 队列
  7. 跳到第一步

上面就算是一个简单的 event-loop 执行模型

再简单点可以总结为:

  1. 在 macrotask 队列中执行最早的那个 task ,然后移出
  2. 执行 microtask 队列中所有可用的任务,然后移出
  3. 下一个循环,执行下一个 macrotask 中的任务 (再跳到第2步)

其他

  • 当一个task(在 macrotask 队列中)正处于执行状态,也可能会有新的事件被注册,那就会有新的 task 被创建。比如下面两个
    1. promiseA.then() 的回调就是一个 task
    • promiseA 是 resolved或rejected: 那这个 task 就会放入当前事件循环回合的 microtask queue
    • promiseA 是 pending: 这个 task 就会放入 事件循环的未来的某个(可能下一个)回合的 microtask queue 中
    1. setTimeout 的回调也是个 task ,它会被放入 macrotask queue 即使是 0ms 的情况
  • microtask queue 中的 task 会在事件循环的当前回合中执行,因此 macrotask queue 中的 task 就只能等到事件循环的下一个回合中执行了
  • click ajax setTimeout 的回调是都是 task, 同时,包裹在一个 script 标签中的js代码也是一个 task 确切说是 macrotask。

ccforward avatar Nov 21 '16 17:11 ccforward

mark

codezyc avatar Feb 06 '17 05:02 codezyc

包裹在一个 script 标签中的js代码也是一个 task,意思就是这些代码会进入事件循环,那为什么会产生阻塞呢?包裹在一个 script 标签中的js代码是指哪些代码?

PLDaily avatar Mar 09 '17 02:03 PLDaily

<script>
for(var i=0,l=10000;i<l;i++){
 ....
}
</script>

这就是在一个 script 标签中的代码了

ccforward avatar Mar 09 '17 03:03 ccforward

index.html中的代码

<script type="text/javascript">
for(var i = 0; i < 10000; i++) {
	if(i == 9999) {
		console.log('a');
	}
}
</script>
<script type="text/javascript" src="main.js"></script>

main.js中的代码

console.log('b');

script中的代码进入事件循环,那通过src引入的代码就会先执行,那为什么还是会先输出a,再输出b?

PLDaily avatar Mar 09 '17 03:03 PLDaily

@PLDaily

上面 script 标签中的代码不是异步执行的

确切的说应该是 script 中的异步代码会进入事件循环等待执行

ccforward avatar Mar 09 '17 03:03 ccforward

哦哦,感谢解答

PLDaily avatar Mar 09 '17 03:03 PLDaily

mark

Thinking80s avatar Mar 20 '17 05:03 Thinking80s

mark

Thinking80s avatar May 07 '17 02:05 Thinking80s

你好 我想问假如是http.get fs.readfile的回调算是macrotask吗?还是应该是microtask?(因为想了一下假如代码是这样的 fs.readfile('123.txt',function(){ console.log('1'); } settimeout(function(){ console.log('2'); ,2} 这样的话感觉fs.readfile的回调不可能是以macrotask的形式呀.. )

authhwang avatar May 10 '17 16:05 authhwang

@authhwang
fs.readfile 属于 IO 操作 所以是 macrotask

ccforward avatar May 11 '17 03:05 ccforward

@ccforward 那同类型的macrotask是怎么判断执行先后顺序的 例如fs.readfile与setTimeout之间的顺序先后 是不是除了代码的编写顺序以外还有别的判断?

authhwang avatar May 11 '17 04:05 authhwang

@authhwang

你的例子中 fs.readfilesetTimeout 是按顺序执行的(几乎是同时),但是回调函数谁先执行就不一定了

ccforward avatar May 11 '17 06:05 ccforward

@ccforward 今天研究了一下 我觉得假如是这个几乎是同时的例子是基本上不需要i/o操作的会都会快过需要i/o操作的 我想问 Object.observe MutationObserver 这两个跟事件循环的i/o观察者 定时器观察者有关系吗

authhwang avatar May 11 '17 10:05 authhwang

@ccforward 你好,有段代码: image

  1. 如果按照您说的 “script 中的异步代码会进入事件循环等待执行”,那么这个自执行函数应该属于同步操作吧,那么如果先执行macro-task之后执行micro-task是否是应该先输出4,后输出5呢?
  2. 而如果把script中的代码全部看做是macro-task,仿佛能解释先输出5,后输出4(macro-task执行完后执行micro-task也就是then),那么我应该如何理解js中的main thread呢?main thread和task是两回事吧。

不知道自己理解的错误点在哪里,希望指正

PS: 正确的输出结果 1, 2, 3, 5, 4

Arweil avatar Jul 10 '17 06:07 Arweil

@Arweil 建议你看这个 https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/ 看完就大概懂了 然后我在别的地方看到是promise队列的处理then会比事件队列的处理快些 优先级高 不过忘了在哪里看了...

authhwang avatar Jul 10 '17 07:07 authhwang

@Arweil 你的代码我重新输出下

(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);
})()

我的理解是应该把 script 标签内的代码块 作为一整个 macro-task ,先输出5后输出4。 这点就像 @authhwang 说的 Promise 的优先级要高一些。

整个自执行函数是同步操作的,但是你再去写一个自执行函数放入另一个 script 标签内,像这样

<script>
(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);
})()
</script>

<script>
(function test2() {
  setTimeout(function () {
    console.log(42)
  }, 0);
  new Promise(function executor (resolve) {
    console.log(12);
    for(var i = 0; i < 10000; i++) {
      i == 9999 && resolve();
    }
    console.log(22);
  }).then(function() {
    console.log(52);
  });
  console.log(32);
})()
</script>

执行结果如下:

1
2
3
5
12
22
32
52
4
42

两个 script 标签的整个执行过程是一个 main thread ,但并不意味着先执行第一个script标签后再执行第二个,因为两个script标签中的 setTimeout 进入的是同一个事件循环中等待,因此他俩在最后分别输出了了 4 和 42。

不知道这样解释你会不会明白,我也是看了好多资料后自己的理解。

PS: 你截图里的代码配色不错啊 什么编辑器? 什么主题?

ccforward avatar Jul 10 '17 08:07 ccforward

@ccforward @authhwang 感谢回答!编辑器用的sublime text 3 主题:https://github.com/wesbos/cobalt2 不过这里给你推荐更实用的VSCode,免费并且跨平台

PS:想直接给你传图片,死活传不上去。。。

Arweil avatar Jul 10 '17 08:07 Arweil

为什么这段代码执行的结果是

正常执行 nextTick延迟执行1 nextTick延迟执行2 setImmediate延迟执行1 setImmediate延迟执行2 强势插入

按照上面你的观点,应该是下面的结果: 正常执行 nextTick延迟执行1 nextTick延迟执行2 setImmediate延迟执行1 强势插入 setImmediate延迟执行2

请解惑一下,深入浅出Node.js里面给出的结果也是

正常执行 nextTick延迟执行1 nextTick延迟执行2 setImmediate延迟执行1 强势插入 setImmediate延迟执行2

junfeisu avatar Aug 01 '17 02:08 junfeisu

从Vue.js源码看nextTick机制

macrotasks: setTimeout ,setInterval, setImmediate,requestAnimationFrame,I/O ,UI渲染 microtasks: Promise, process.nextTick, Object.observe, MutationObserver

当一个程序有:setTimeout, setInterval ,setImmediate, I/O, UI渲染,Promise ,process.nextTick, Object.observe, MutationObserver的时候: 哈哈 1.先执行 macrotasks:I/O -》 UI渲染

2.再执行 microtasks :process.nextTick -》 Promise -》MutationObserver ->Object.observe

3.再把setTimeout setInterval setImmediate 塞入一个新的macrotasks,依次:

setTimeout ,setInterval --》setImmediate

setImmediate(function(){
    console.log(1);
},0);
setTimeout(function(){
    console.log(2);
},0);
new Promise(function(resolve){
    console.log(3);
    resolve();
    console.log(4);
}).then(function(){
    console.log(5);
});
console.log(6);
process.nextTick(function(){
    console.log(7);
});
console.log(8);
结果是:3 4 6 8 7 5 2 1

libin1991 avatar Nov 02 '17 15:11 libin1991

@ccforward 您好,就是下面这个简单的例子,为什么3不是第一个先输出的呢?Promise不是应该属于异步的过程么,下面的console.log(3)不应该是主线程么?

	<script>
  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);
	</script>

helios741 avatar Nov 08 '17 02:11 helios741

@helios741 promise里面构造executor的时候内部是同步执行的。异步的是resolve这种结果的回调 可以参考mdn的解释https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise

Aaaaaaaty avatar Nov 08 '17 02:11 Aaaaaaaty

@Aaaaaaaty 嗯嗯,理解了,thanks

helios741 avatar Nov 08 '17 02:11 helios741

@helios741 客气哈哈

Aaaaaaaty avatar Nov 08 '17 02:11 Aaaaaaaty

@junfeisu 这个地方主要是node.jsevent loop过程中分不同阶段去执行代码。具体的内容可参加官方的文档

具体到你的这个例子:

process.nextTick(() => {
  console.log('nextTick1')
})

process.nextTick(() => {
  console.log('nextTick2')
})


setImmediate(() => {
  console.log('setImmediate1')
  process.nextTick(() => {
    console.log('插入')
  })
})

setImmediate(() => {
  console.log('setImmediate2')
})

console.log('正常执行')

在执行第一个setImmediate的时候将cb放入了event loopcheck阶段的callback queue当中,然后就会执行第二个setImmediate,这个时候也会将cb放入event loopcheck阶段的callback queue当中。当主线程的任务执行完了,进入event loop,然后执行到check阶段,因为check阶段有2个callback,因此会依次执行,在执行第一个callback的时候,因为调用了一次process.nextTick,即注册了一个microTask,这个时候会当check阶段结束后,立马就执行microTask

因此最后执行输出的内容应该是:

正常执行
nextTick1
nextTick2
setImmediate1
setImmediate2
插入

CommanderXL avatar Mar 13 '18 02:03 CommanderXL

谁能解释下面的现象:
两个标签:

<script>
    new Promise((resolve, reject)=>{
        resolve();
        console.log(2);
    }).then(()=>{
        console.log(3);
    });
</script>

<script>
    new Promise((resolve, reject)=>{
        resolve();
        console.log(22);
    }).then(()=>{
        console.log(33);
    });
</script>
2, 3, 22, 33

同个标签:

<script>
    new Promise((resolve, reject)=>{
        resolve();
        console.log(2);
    }).then(()=>{
        console.log(3);
    });

    new Promise((resolve, reject)=>{
        resolve();
        console.log(22);
    }).then(()=>{
        console.log(33);
    });
</script>
2, 22, 3, 33

xwenliang avatar Jun 07 '18 21:06 xwenliang

@xwenliang

可以这样理解 包裹在一个 script 标签中的js代码也是一个 macrotask

会优先执行一个 macrotask

ccforward avatar Jun 13 '18 03:06 ccforward

@ccforward 感谢回答

xwenliang avatar Jun 13 '18 06:06 xwenliang

macrotasks: setTimeout ,setInterval, setImmediate,requestAnimationFrame,I/O ,UI渲染 microtasks: Promise, process.nextTick, Object.observe, MutationObserver 请问这个分类的方式是在标准的哪里写的?我在whatwg里面没有找到啊。

zhuanyongxigua avatar Aug 16 '18 01:08 zhuanyongxigua

关于这个,推荐读下国外大神的 Tasks, microtasks, queues and schedules

HualiangLI avatar Aug 30 '18 08:08 HualiangLI

@zhuanyongxigua 这个应该是作者自己归类的。不过我对 setInterval 的归类持有保留态度。之前看到一个前端 promise 库就是用 setInterval 来模拟实现的。

MuYunyun avatar Oct 03 '18 09:10 MuYunyun