最近在准备面试题,console的输出顺序之前一直迷迷糊糊。
单线程是 JavaScript 核心特征之一。这意味着,在 JS 中所有任务都需要排队执行,前一个任务结束,才会执行后一个任务。
所以这就造成了一个问题:如果前一个任务耗时很长,后一个任务就不得不一直等着前面的任务执行完才能执行。比如我们向服务器请求一段数据,由于网络问题,可能需要等待 60 秒左右才能成功返回数据,此时只能等待请求完成,JS 才能去处理后面的代码。
为了解决JS单线程带来的问题,JavaScript 就将所有任务分成了同步任务和异步任务。
同步任务指的是当前一个(如果有)任务执行完毕,接下来可以立即执行的任务。这些任务将在主线程上依次排队执行。也就是说排排队
//for(){} 和 console.log() 将会依次执行,最终输出 0 1 2 3 4 done。
for (let i = 0; i < 5; i++) {
console.log(i)
}
console.log('done')
异步任务相对于同步任务,指的是不需要进入主线程排队执行,而是进入超车道、并车道。也就是任务队列中,形成一系列的任务。这些任务只有当被通知可以执行的时候,该任务才会重新进入主线程执行。
//下面的 then() 方法需要等待 Promise 被 resolve() 之后才能执行,它是一个异步任务。最终输出 1 3 2。
console.log(1)
Promise.resolve().then(() => {
console.log(2)
})
console.log(3)
具体来说就是,所有同步任务会在主线程上依次排队执行,形成一个执行栈(Execution Context
Stack)。主线程之外,还存在一个任务队列。当异步任务有了运行结果,会在任务队列之中放置对应的事件。当执行栈中的所有同步任务执行完毕,任务队列里的异步任务就会进入执行栈,然后继续依次执行。
异步任务(任务队列)可以分为
macrotasks(taskQueue):宏任务 task,也是我们常说的任务队列
注意: (1)每一个 macrotask 的回调函数要放在下一车的开头去执行! (2)只有 setImmediate 能够确保在下一轮事件循环立即得到处理
microtasks:微任务(也称 job)调度在当前脚本执行结束后,立即执行的任务,以避免付出额外一个 task 的费用。
Promise 之所以无法使用 catch 捕获 setTimeout 回调中的错误,是因为 Promise 的 then/catch 是在 setTimeout 之前执行的。
事件循环的顺序,决定了 JavaScript 代码的执行顺序。它从 script (整体代码) 开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空 (只剩全局),然后执行所有的 microtasks。当所有可执行的
microtasks 执行完毕之后。循环再次从 macrotasks 开始,找到其中一个任务队列执行完毕,然后再执行所有的 microtasks,这样一直循环下去。
翻译过来就是,先执行 Microtasks queue 中的所有 Microtasks,再挑一个 Macrotasks queue 来执行其中所有 Macrotasks,然后继续执行 Microtasks queue 中的所有
Microtasks,再挑一个 Macrotasks queue 来执行其中所有 Macrotasks ……
这也就解释了,为什么同一个事件循环中的 Microtasks 会比 Macrotasks 先执行。
console.log(1)
setTimeout(()=>{
console.log(2)
},0)
process.nextTick(()=>{
console.log(3)
})
new Promise((resolve)=>{
console.log(4)
resolve()
}).then(()=>{
console.log(5)
})
第一轮事件循环
主流程 | Macro event queue | Micro event queue |
---|---|---|
console.log(1) | setTimeout | process |
console.log(4) | then |
第二轮事件循环
主流程 | Macro event queue | Micro event queue |
---|---|---|
setTimeout | ||
console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
process.nextTick(() => {
console.log(3)
})
new Promise((resolve) => {
console.log(4)
resolve()
}).then(() => {
console.log(5)
})
setTimeout(() => {
console.log(6)
}, 0)
new Promise((resolve) => {
console.log(7)
setTimeout(() => {
console.log(8)
resolve()
}, 0)
}).then(() => {
console.log(9)
setTimeout(() => {
console.log(10)
new Promise((resolve) => {
console.log(11)
resolve()
}).then(() => {
console.log(12)
})
}, 0)
})
// 1, 4, 7, 3, 5, 2, 6, 8, 9, 10, 11, 12
第一轮事件循环
主流程 | Macro event queue | Micro event queue |
---|---|---|
console.log(1) | setTimeout2 | process3 |
console.log(4) | setTimeout6 | then5 |
console.log(7) | setTimeout8 |
第二轮事件循环
主流程 | Macro event queue | Micro event queue |
---|---|---|
setTimeout2 | setTimeout6 | |
setTimeout8 |
第三轮事件循环
主流程 | Macro event queue | Micro event queue |
---|---|---|
setTimeout6 | setTimeout8 | |
第四轮事件循环
主流程 | Macro event queue | Micro event queue |
---|---|---|
setTimeout9 | setTimeout10 | then9 |
第五轮事件循环
主流程 | Macro event queue | Micro event queue |
---|---|---|
console.log(10) | then12 | |
console.log(11) |
// 以下代码在 Node 环境运行:process.nextTick 由 Node 提供
console.log("1")
setTimeout(function () {
console.log("2")
process.nextTick(function () {
console.log("3")
})
new Promise(function (resolve) {
console.log("4")
resolve()
}).then(function () {
console.log("5")
})
})
process.nextTick(function () {
console.log("6")
})
new Promise(function (resolve) {
console.log("7")
resolve()
}).then(function () {
console.log("8")
})
setTimeout(function () {
console.log("9")
process.nextTick(function () {
console.log("10")
})
new Promise(function (resolve) {
console.log("11")
resolve()
}).then(function () {
console.log("12")
})
})
// 最终输出:1 7 6 8 2 4 3 5 9 11 10 12
setTimeout(()=>{
console.log("setTimeout1");
Promise.resolve().then(data => {
console.log(222);
});
},0);
setTimeout(()=>{
console.log("setTimeout2");
},0);
Promise.resolve().then(data=>{
console.log(111);
});
//111 setTimeout1 222 setTimeout2
console.log('script start');
setTimeout(function () {
console.log('setTimeout---0');
}, 0);
setTimeout(function () {
console.log('setTimeout---200');
setTimeout(function () {
console.log('inner-setTimeout---0');
});
Promise.resolve().then(function () {
console.log('promise5');
});
}, 200);
Promise.resolve().then(function () {
console.log('promise1');
}).then(function () {
console.log('promise2');
});
Promise.resolve().then(function () {
console.log('promise3');
});
console.log('script end');
/*script startscript endpromise1promise3promise2setTimeout---0setTimeout---200promise5inner-setTimeout---0*/
console.log("1");
setTimeout(function cb1(){
console.log("2")
}, 0);
new Promise(function(resolve, reject) {
console.log("3")
resolve();
}).then(function cb2(){
console.log("4");
})
console.log("5")
// 1 3 5 4 2
console.log("1");
setTimeout(() => {
console.log("2")
new Promise(resolve => {
resolve()
}).then(() => {
console.log("3")
})
}, 0);
setTimeout(() => {
console.log("4")
}, 0);
console.log("5")
// 1 5 2 3 4
console.log("1");
setTimeout(() => {
console.log("2")
new Promise(resolve => {
console.log(6)
resolve()
}).then(() => {
console.log("3")
})
}, 0);
setTimeout(() => {
console.log("4")
}, 0);
console.log("5")
// 1 5 2 6 3 4
console.log('start')
setTimeout(function(){
console.log('宏任务1号')
})
Promise.resolve().then(function(){
console.log('微任务0号')
})
console.log('执行栈执行中')
setTimeout(function(){
console.log('宏任务2号')
Promise.resolve().then(function(){
console.log('微任务1号')
})
},500)
setTimeout(function(){
console.log('宏任务3号')
setTimeout(function(){
console.log('宏任务4号')
Promise.resolve().then(function(){
console.log('微任务2号')
})
},500)
Promise.resolve().then(function(){
console.log('微任务3号')
})
},600)
console.log('end')
// start 执行栈执行中 end 微任务0号 宏任务1号 宏任务2号 微任务1号 宏任务3号 微任务3号 宏任务4号 微任务2号
function test() {
console.log(1)
setTimeout(function () { // timer1
console.log(2)
}, 1000)
}
test();
setTimeout(function () { // timer2
console.log(3)
})
new Promise(function (resolve) {
console.log(4)
setTimeout(function () { // timer3
console.log(5)
}, 100)
resolve()
}).then(function () {
setTimeout(function () { // timer4
console.log(6)
}, 0)
console.log(7)
})
console.log(8)
//1 4 8 7 3 6 5 2
结合我们上述的JS运行机制再来看这道题就简单明了的多了
setTimeout(() => {
console.log(1)
}, 0)
new Promise((resolve, reject) => {
console.log(2)
resolve(3)
}).then(val => {
console.log(val)
})
console.log(4)
// 2 4 3 1
for (let i = 0; i < 5; i++) {
console.log(i)
}
console.log('done')
// 0 1 2 3 4 done
console.log(1)
Promise.resolve().then(() => {
console.log(2)
})
console.log(3)
//1 3 2
setTimeout(() => {
console.log(1)
}, 0)
for (let i = 2; i <= 3; i++) {
console.log(i)
}
console.log(4)
setTimeout(() => {
console.log(5)
}, 0)
for (let i = 6; i <= 7; i++) {
console.log(i)
}
console.log(8)
//2 3 4 6 7 8 1 5
console.log(1)
async function async1() {
await async2()
console.log(2)
}
async function async2() {
console.log(3)
}
async1()
setTimeout(() => {
console.log(4)
}, 0)
new Promise(resolve => {
console.log(5)
resolve()
})
.then(() => {
console.log(6)
})
.then(() => {
console.log(7)
})
console.log(8)
// 1 3 5 8 2 6 7 4
console.log(1)
function a() {
return new Promise(resolve => {
console.log(2)
setTimeout(() => {
console.log(3)
}, 0)
resolve()
})
}
a().then(() => {
console.log(4)
})
//1 2 4 3
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
//script start、script end、promise1、promise2、setTimeout
整体script作为第一个宏任务进入主线程,遇到console.log,输出script start
遇到 setTimeout,其回调函数被分发到宏任务 Event Queue 中
遇到 Promise,其 then函数被分到到微任务 Event Queue 中,记为 then1,之后又遇到了 then 函数,将其分到微任务 Event Queue 中,记为 then2
遇到 console.log,输出 script end。至此,Event Queue中存在三个任务,如下表:
主流程 | Macro event queue | Micro event queue |
---|---|---|
console.log(‘script start’) | console.log(‘setTimeout’) | console.log(‘promise1’) |
console.log(‘script end’) | console.log(‘promise1’) |
console.log('script start');
setTimeout(function() {
console.log('timeout1');
}, 10);
new Promise(resolve => {
console.log('promise1');
resolve();
setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
console.log('then1')
})
console.log('script end');
//script start、promise1、script end、then1、timeout1、timeout2
首先,事件循环从宏任务 (macrotask) 队列开始,最初始,宏任务队列中,只有一个 script(整体代码)任务;当遇到任务源 (task source)
时,则会先分发任务到对应的任务队列中去。所以,就和上面例子类似,首先遇到了console.log,输出 script start; 接着往下走,遇到 setTimeout 任务源,将其分发到任务队列中去,记为 timeout1; 接着遇到
promise,new promise 中的代码立即执行,输出 promise1, 然后执行 resolve ,遇到 setTimeout ,将其分发到任务队列中去,记为 timemout2, 将其 then 分发到微任务队列中去,记为
then1; 接着遇到 console.log 代码,直接输出 script end 接着检查微任务队列,发现有个 then1 微任务,执行,输出then1 再检查微任务队列,发现已经清空,则开始检查宏任务队列,执行 timeout1,输出
timeout1; 接着执行 timeout2,输出 timeout2 至此,所有的都队列都已清空,执行完毕。其输出的顺序依次是:script start, promise1, script end, then1, timeout1,
timeout2。
有个小 tip:从规范来看,microtask 优先于 task 执行,所以如果有需要优先执行的逻辑,放入microtask 队列会比 task 更早的被执行。 最后的最后,记住,JavaScript
是一门单线程语言,异步操作都是放到事件循环队列里面,等待主执行栈来执行的,并没有专门的异步执行线程。
console.log(1)
setTimeout(function() {
console.log(2)
},0)
setTimeout(function() {
console.log(3)
},0)
console.log(4)
// 1 4 2 3
function fun1(){
console.log(1)
}
function fun2(){
console.log(2)
fun1()
console.log(3)
}
fun2()
// 2 1 3
function func1(){
console.log(1)
}
function func2(){
setTimeout(()=>{
console.log(2)
},0)
func1()
console.log(3)
}
func2()
// 1 3 2
var p = new Promise(resolve=>{
console.log(4) //这里没有执行p也要有输出 所以4是最开始的
resolve(5)
})
function func1(){
console.log(1)
}
function func2(){
setTimeout(()=>{
console.log(2)
},0)
func1()
console.log(3)
p.then(resolve=>{
console.log(resolve)
})
}
func2()
//4 1 3 5 2
console.log('start')
const interval = setInterval(() => {
console.log('setInterval')
}, 0)
setTimeout(() => {
console.log('setTimeout 1')
Promise.resolve()
.then(() => {
console.log('promise 1')
})
.then(() => {
console.log('promise 2')
})
.then(() => {
setTimeout(() => {
console.log('setTimeout 2')
Promise.resolve()
.then(() => {
console.log('promise 3')
})
.then(() => {
console.log('promise 4')
})
.then(() => {
clearInterval(interval)
})
}, 0)
})
console.log('time end')
}, 0)
Promise.resolve().then(() => {
console.log('promise 5')
}).then(() => {
console.log('promise 6')
})
// start
// promise 5
// promise 6
// setInterval
// setTimeout 1
// time end
// promise 1
// promise 2
// setInterval
// setTimeout 2
// setInterval
// promise 3
// promise 4
解析: (1)先按照 macrotask 和 microtask 划分代码:
console.log('start')
setInterval 是 macrotask,其回调函数在 microtask 后执行
const interval = setInterval(() => {
console.log('setInterval')
}, 0)
setTimeout 是 macrotask,其回调函数放在下一车(cycle 2)执行
setTimeout(() => ... , 0)
Promise.resolve () 的两个 then () 是 microtask
Promise.resolve()
//microtask
.then(() => {
console.log('promise 5')
})
//microtask
.then(() => {
console.log('promise 6')
})
(2)第一班车(cycle 1):
进栈
第一个 macrotask 是 setInterval,回调函数放下一车(cycle 2)的开头执行, 第二个 macrotask 是 setTimeout,回调函数放下下一车(cycle 3)的开头执行,
清空栈, 输出:start 执行 microtasks,直至清空该队列,即 Promise.resolve () 的两个 then (), 输出:promise 5 promise 6
(3)第二班车(cycle 2): 执行 setInterval 的回调, 输出:setInterval, 同时下一个 setInterval 也是 macrotask 但要放到 下下下一车(cycle 4)执行回调,即
下下一车(cycle 3)setTimeout 的后面
此时 setInterval 中没有 microtasks,所以该队列是空的,故进行下一车(cycle 3)
(4)第三班车(cycle 3) 执行 setTimeout 的回调, 输出 setTimeout 1 执行 microtasks,直至清空该队列,即 Promise.resolve () 的第一个和第二个 then (),
输出:promise 1 promise 2
而 第三个 then () 中的 setTimeout 是 macrotask ,放到下下下下一车(cycle 5)执行回调, 第四个 then () 是紧跟着第三个 then () 的,所以在 下下下下一车(cycle 5)执行
此时 microtasks 已空,故进行下一车(cycle 4)
(5)第四班车(cycle 4) 由(3)得,执行 setInterval , 输出:setInterval
此时 setInterval 中没有 microtasks,所以该队列是空的,故进行下一车(cycle 5)
同时下一个 setInterval 也是 macrotask 但要放到 下下下下下一车(cycle 6)执行回调,
(6)第五班车(‘cycle 5’) 由(4)得,执行 setTimeout 输出:setTimeout 2
执行 microtasks,直至清空该队列,即 Promise.resolve () 的第一个和第二个 then (),
输出:promise 3 promise 4
接着执行第三个 then () --> clearInterval(interval),将下下下下下一车(cycle 6)要执行回调的 setInterval 清除
此时 microtasks 已空, 同时整段代码执行完毕。