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

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

Open
ccforward opened this issue Nov 21, 2016 · 30 comments
Open

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

ccforward opened this issue Nov 21, 2016 · 30 comments
Assignees

Comments

@ccforward
Copy link
Owner

关于 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。
@codezyc
Copy link

codezyc commented Feb 6, 2017

mark

@PLDaily
Copy link

PLDaily commented Mar 9, 2017

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

@ccforward
Copy link
Owner Author

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

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

@PLDaily
Copy link

PLDaily commented Mar 9, 2017

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?

@ccforward
Copy link
Owner Author

@PLDaily

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

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

@PLDaily
Copy link

PLDaily commented Mar 9, 2017

哦哦,感谢解答

@Thinking80s
Copy link

mark

@Thinking80s
Copy link

mark

@authhwang
Copy link

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

@ccforward
Copy link
Owner Author

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

@authhwang
Copy link

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

@ccforward
Copy link
Owner Author

@authhwang

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

@authhwang
Copy link

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

@Arweil
Copy link

Arweil commented Jul 10, 2017

@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

@authhwang
Copy link

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

@ccforward
Copy link
Owner Author

@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: 你截图里的代码配色不错啊 什么编辑器? 什么主题?

@Arweil
Copy link

Arweil commented Jul 10, 2017

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

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

@junfeisu
Copy link

junfeisu commented Aug 1, 2017


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

正常执行
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

@libin1991
Copy link

libin1991 commented Nov 2, 2017

从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

@helios741
Copy link

@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>

@Aaaaaaaty
Copy link

Aaaaaaaty commented Nov 8, 2017

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

@helios741
Copy link

@Aaaaaaaty 嗯嗯,理解了,thanks

@Aaaaaaaty
Copy link

@helios741 客气哈哈

@CommanderXL
Copy link

@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
插入

@xwenliang
Copy link

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

<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

@ccforward
Copy link
Owner Author

@xwenliang

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

会优先执行一个 macrotask

@xwenliang
Copy link

@ccforward 感谢回答

@zhuanyongxigua
Copy link

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

@HualiangLI
Copy link

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

@MuYunyun
Copy link

MuYunyun commented Oct 3, 2018

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

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