cc
cc copied to clipboard
45.理解事件循环二(macrotask和microtask)
关于 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)为空的时候,开始依次执行:
- 把最早的任务(task A)放入任务队列
- 如果 task A 为null (那任务队列就是空),直接跳到第6步
- 将 currently running task 设置为 task A
- 执行 task A (也就是执行回调函数)
- 将 currently running task 设置为 null 并移出 task A
- 执行 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 队列
- 跳到第一步
上面就算是一个简单的 event-loop 执行模型
再简单点可以总结为:
- 在 macrotask 队列中执行最早的那个 task ,然后移出
- 执行 microtask 队列中所有可用的任务,然后移出
- 下一个循环,执行下一个 macrotask 中的任务 (再跳到第2步)
其他
- 当一个task(在 macrotask 队列中)正处于执行状态,也可能会有新的事件被注册,那就会有新的 task 被创建。比如下面两个
- promiseA.then() 的回调就是一个 task
- promiseA 是 resolved或rejected: 那这个 task 就会放入当前事件循环回合的 microtask queue
- promiseA 是 pending: 这个 task 就会放入 事件循环的未来的某个(可能下一个)回合的 microtask queue 中
- setTimeout 的回调也是个 task ,它会被放入 macrotask queue 即使是 0ms 的情况
- microtask queue 中的 task 会在事件循环的当前回合中执行,因此 macrotask queue 中的 task 就只能等到事件循环的下一个回合中执行了
- click ajax setTimeout 的回调是都是 task, 同时,包裹在一个 script 标签中的js代码也是一个 task 确切说是 macrotask。
mark
包裹在一个 script 标签中的js代码也是一个 task,意思就是这些代码会进入事件循环,那为什么会产生阻塞呢?包裹在一个 script 标签中的js代码是指哪些代码?
<script>
for(var i=0,l=10000;i<l;i++){
....
}
</script>
这就是在一个 script
标签中的代码了
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
上面 script 标签中的代码不是异步执行的
确切的说应该是 script 中的异步代码会进入事件循环等待执行
哦哦,感谢解答
mark
mark
你好 我想问假如是http.get fs.readfile的回调算是macrotask吗?还是应该是microtask?(因为想了一下假如代码是这样的 fs.readfile('123.txt',function(){ console.log('1'); } settimeout(function(){ console.log('2'); ,2} 这样的话感觉fs.readfile的回调不可能是以macrotask的形式呀.. )
@authhwang
fs.readfile
属于 IO 操作 所以是 macrotask
@ccforward 那同类型的macrotask是怎么判断执行先后顺序的 例如fs.readfile与setTimeout之间的顺序先后 是不是除了代码的编写顺序以外还有别的判断?
@authhwang
你的例子中 fs.readfile
和 setTimeout
是按顺序执行的(几乎是同时),但是回调函数谁先执行就不一定了
@ccforward 今天研究了一下 我觉得假如是这个几乎是同时的例子是基本上不需要i/o操作的会都会快过需要i/o操作的 我想问 Object.observe MutationObserver 这两个跟事件循环的i/o观察者 定时器观察者有关系吗
@ccforward 你好,有段代码:
- 如果按照您说的 “script 中的异步代码会进入事件循环等待执行”,那么这个自执行函数应该属于同步操作吧,那么如果先执行macro-task之后执行micro-task是否是应该先输出4,后输出5呢?
- 而如果把script中的代码全部看做是macro-task,仿佛能解释先输出5,后输出4(macro-task执行完后执行micro-task也就是then),那么我应该如何理解js中的main thread呢?main thread和task是两回事吧。
不知道自己理解的错误点在哪里,希望指正
PS: 正确的输出结果 1, 2, 3, 5, 4
@Arweil 建议你看这个 https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/ 看完就大概懂了 然后我在别的地方看到是promise队列的处理then会比事件队列的处理快些 优先级高 不过忘了在哪里看了...
@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 @authhwang 感谢回答!编辑器用的sublime text 3 主题:https://github.com/wesbos/cobalt2 不过这里给你推荐更实用的VSCode,免费并且跨平台
PS:想直接给你传图片,死活传不上去。。。
为什么这段代码执行的结果是
正常执行 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
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
@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 promise里面构造executor的时候内部是同步执行的。异步的是resolve这种结果的回调 可以参考mdn的解释https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise
@Aaaaaaaty 嗯嗯,理解了,thanks
@helios741 客气哈哈
@junfeisu 这个地方主要是node.js
在event 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 loop
的check
阶段的callback queue
当中,然后就会执行第二个setImmediate
,这个时候也会将cb
放入event loop
的check
阶段的callback queue
当中。当主线程的任务执行完了,进入event loop
,然后执行到check
阶段,因为check
阶段有2个callback
,因此会依次执行,在执行第一个callback
的时候,因为调用了一次process.nextTick
,即注册了一个microTask
,这个时候会当check
阶段结束后,立马就执行microTask
。
因此最后执行输出的内容应该是:
正常执行
nextTick1
nextTick2
setImmediate1
setImmediate2
插入
谁能解释下面的现象:
两个标签:
<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
可以这样理解 包裹在一个 script 标签中的js代码也是一个 macrotask
会优先执行一个 macrotask
@ccforward 感谢回答
macrotasks: setTimeout ,setInterval, setImmediate,requestAnimationFrame,I/O ,UI渲染 microtasks: Promise, process.nextTick, Object.observe, MutationObserver 请问这个分类的方式是在标准的哪里写的?我在whatwg里面没有找到啊。
关于这个,推荐读下国外大神的 Tasks, microtasks, queues and schedules
@zhuanyongxigua 这个应该是作者自己归类的。不过我对 setInterval 的归类持有保留态度。之前看到一个前端 promise 库就是用 setInterval 来模拟实现的。