任务队列原理
-
js是单线程机制,事件循环是唯一的,但是任务队列可以拥有多个
-
任务队列分为两种:macroTask(宏任务)与microTask(微任务)。在最新标准中,它们被分别称为task与jobs
-
macroTask大概包括:script(整体代码)、setTimeout、setInterval、 setImmediate、I/O、UI rendering
-
microTask大概包括: process.nextTick、Promise、 Object.observe(已废弃)、MutationObserver(html5新特性)
-
setTimeout/Promise等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务
-
来自不同任务源的任务会进入到不同的任务队列。其中setTimeout与setInterval是同源的
-
其中每一个任务的执行,无论是macroTask还是microTask,都是借助函数调用栈来完成
从script(整体代码)开始第一次循环,之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的microTask。当所有可执行的microTask执行完毕之后。循环再次从macroTask开始,找到其中一个任务队列执行完毕,然后再执行所有的microTask,这样一直循环下去。
script(主程序代码)—>process.nextTick—>Promises…——>setTimeout——>setInterval——>setImmediate——> I/O——>UI rendering
问题引入
setTimeout(function(){console.log(1)},0);
console.log(2)
//2 1
首先执行主线程中的同步任务,当主线程任务执行完之后,再从event loop中读取任务,因此先输出2,再输出1。
event loop读取任务的先后顺序,取决于任务队列(Job queue)中对于不同任务读取规则的限定。
setTimeout(function () {
console.log(4);
}, 0);
Promise.resolve().then(function () {
console.log(2);
});
console.log(1);
//1 2 3
先输出1,没有问题,因为是同步任务在主线程中优先执行,这里的问题是setTimeout和Promise.then任务的执行优先级是如何定义的?
任务队列中的执行顺序
任务队列分为:macroTask(宏任务)与microTask(微任务)。我们来假设
macroTask队列包含任务: a1, a2 , a3
microTask队列包含任务: b1, b2 , b3
执行顺序为,首先执行marcoTask队列开头的任务,也就是 a1 任务,执行完毕后,在执行microTask队列里的所有任务,也就是依次执行b1, b2 , b3,执行完后清空microTask中的任务,接着执行marcoTask中的第二个任务,依次循环。
setTimeout(function(){
console.log(1)
},0);
new Promise(function(resolve,reject){
console.log(2);
resolve();
}).then(function(){
console.log(3)
}).then(function(){
console.log(4)
});
process.nextTick(function(){
console.log(5)
});
console.log(6);
//2 6 5 3 4 1
script(主程序代码)——>process.nextTick——>promise——>setTimeout
- 1、主体部分:定义promise的构造部分是同步的, 因此先输出2 ,主体部分再输出6,(同步任务,严格按照顺序执行)
- 2、process.nextTick: 输出5
- 3、这里的promise部分,严格的说其实是promise.then部分,输出的是3,4
- 4、setTimeout : 最后输出1
更复杂的例子
new Promise(function(resolve,reject){
console.log(2);
setTimeout(function(){
resolve()
},0)
}).then(function(){
console.log(3)
}).then(function(){
console.log(4)
});
setTimeout(function(){
console.log(1)
},0);
process.nextTick(function(){
console.log(5);
});
console.log(6);
//2 6 5 1 3 4
区别在于Promise的构造中,没有同步的resolve,因此promise.then在当前的执行队列中是不存在的,只有promise从pending转移到resolve,才会有then方法,而这个resolve是在一个setTimout时间中完成的,因此3,4最后输出
任务队列执行顺序升级版
console.log('1');
setTimeout(function () {
console.log('2');
new Promise(function (resolve) {
console.log('3');
resolve();
}).then(function () {
console.log('4')
}).then(function () {
console.log('5')
});
})
new Promise(function (resolve) {
console.log('7');
resolve();
}).then(function () {
console.log('8')
})
setTimeout(function () {
console.log('9');
new Promise(function (resolve) {
resolve();
}).then(function () {
console.log('10')
}).then(function () {
console.log('11')
});
new Promise(function (resolve) {
resolve();
}).then(function () {
console.log('12')
})
})
1、第一轮事件循环流程分析如下:
- 整体script作为第一个宏任务进入主线程,遇到console.log,输出1
- 遇到setTimeout,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1
- 遇到Promise,new Promise直接执行,输出7。then被分发到微任务Event Queue中。我们记为then1
- 又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2
| 宏任务Event Queue | 微任务Event Queue |
|---|---|
| setTimeout1 | then1 |
| setTimeout2 |
第一轮事件循环宏任务结束时各Event Queue的情况如上表,此时已经输出了1和7。清空微任务,执行then1,输出8。第一轮事件循环正式结束,这一轮的结果是输出1,7,8。
2、第二轮事件循环流分析:
从setTimeout1宏任务开始
- 先输出2
- new Promise立即执行输出3,then也分发到微任务Event Queue中,记为then2
| 宏任务Event Queue | 微任务Event Queue |
|---|---|
| setTimeout2 | then2 |
第二轮事件循环宏任务执行结束,执行微任务then2,输出4,then被分发到微任务Event Queue中,记为then3
| 宏任务Event Queue | 微任务Event Queue |
|---|---|
| setTimeout2 | then3 |
执行微任务then3,输出5。第二轮事件循环正式结束,这一轮的结果是输出2,3,4,5。
3、第三轮事件循环流分析:
从setTimeout2宏任务开始
- 先输出9
- 第一个new Promise立即执行,then也分发到微任务Event Queue中,记为then4
- 第二个new Promise立即执行,then也分发到微任务Event Queue中,记为then5
| 宏任务Event Queue | 微任务Event Queue |
|---|---|
| then4 | |
| then5 |
第二轮事件循环宏任务执行结束,执行微任务then4,输出10,then被分发到微任务Event Queue中,记为then6。执行微任务then5,输出12
| 宏任务Event Queue | 微任务Event Queue |
|---|---|
| then6 |
执行微任务then6,输出11。第三轮事件循环正式结束,这一轮的结果是输出9,10,12,11。
整段代码,共进行了三次事件循环,完整的输出为1,7,8,2,3,4,5,9,10,12,11。(node环境执行结果会有些不一样)
参考文章:
从promise、process.nextTick、setTimeout出发,谈谈Event Loop中的Job queue