前言
最近在准备面试题,console的输出顺序之前一直迷迷糊糊。
必备知识
JS是单线程的
单线程是 JavaScript 核心特征之一。这意味着,在 JS 中所有任务都需要排队执行,前一个任务结束,才会执行后一个任务。
所以这就造成了一个问题:如果前一个任务耗时很长,后一个任务就不得不一直等着前面的任务执行完才能执行。比如我们向服务器请求一段数据,由于网络问题,可能需要等待 60 秒左右才能成功返回数据,此时只能等待请求完成,JS 才能去处理后面的代码。
同步任务和异步任务
为了解决JS单线程带来的问题,JavaScript 就将所有任务分成了同步任务和异步任务。
同步任务(Synchronous)
同步任务指的是当前一个(如果有)任务执行完毕,接下来可以立即执行的任务。这些任务将在主线程上依次排队执行。也就是说排排队
//for(){} 和 console.log() 将会依次执行,最终输出 0 1 2 3 4 done。
for (let i = 0; i < 5; i++) {
console.log(i)
}
console.log('done')
异步任务(Asynchronous)
异步任务相对于同步任务,指的是不需要进入主线程排队执行,而是进入超车道、并车道。也就是任务队列中,形成一系列的任务。这些任务只有当被通知可以执行的时候,该任务才会重新进入主线程执行。
//下面的 then() 方法需要等待 Promise 被 resolve() 之后才能执行,它是一个异步任务。最终输出 1 3 2。
console.log(1)
Promise.resolve().then(() => {
console.log(2)
})
console.log(3)
具体来说就是,所有同步任务会在主线程上依次排队执行,形成一个执行栈(Execution Context
Stack)。主线程之外,还存在一个任务队列。当异步任务有了运行结果,会在任务队列之中放置对应的事件。当执行栈中的所有同步任务执行完毕,任务队列里的异步任务就会进入执行栈,然后继续依次执行。
异步任务(任务队列)可以分为
-
macrotasks(taskQueue):宏任务 task,也是我们常说的任务队列
- macrotasks 的划分:(注意先后顺序!)
- (1)setTimeout(延迟调用)
- (2)setInterval(间歇调用)
- (3)setImmediate(Node 的立即调用)
- (4)requestAnimationFrame(高频的 RAF)
- (5)I/O(I/O 操作)
- (6)UI rendering(UI 渲染)
- (7) 包裹在一个 script 标签中的 js 代码也是一个 Macrotasks
- macrotasks 的划分:(注意先后顺序!)
注意: (1)每一个 macrotask 的回调函数要放在下一车的开头去执行! (2)只有 setImmediate 能够确保在下一轮事件循环立即得到处理
-
microtasks:微任务(也称 job)调度在当前脚本执行结束后,立即执行的任务,以避免付出额外一个 task 的费用。
- microtasks :(注意先后顺序!)
- (1)process.nextTick(Node 中 定义出一个动作,并且让这个动作在下一个事件轮询的时间点上执行)
- (2)Promises(详情看这篇文章:www.jianshu.com/p/06d16ce41…
- (3)Object.observe(原生观察者实现,已废弃)
- (4)MutationObserver(监听 DOM change) 只有在 nextTick 空了才处理其它 microtask。(Next tick queue has even higher priority
over the Other Micro tasks queue.)
- microtasks :(注意先后顺序!)
一个事件循环(eventLoop)的执行顺序(非常重要):
- ① 开始执行脚本。
- ② 取 macrotasks(taskQueue)中的第一个 task 执行,该 task 的回调函数 放在下一个 task 开头 执行。
- ③ 取 microtasks 中的全部 microtask 依次执行,当这些 microtask 执行结束后,可继续添加 microtask 继续执行,直到 microtask 队列为空。
- ④ 取 macrotasks(taskQueue)中的第二个 task 执行,该 task 的回调函数 放在下一个 task 开头 执行。
- ⑤ 再取 microtasks 中的全部 microtask 依次执行,当这些 microtask 执行结束后,可继续添加 microtask 继续执行,直到 microtask 队列为空。
- ⑥ 循环 ② ③ 直到 macrotasks、microtasks 为空。
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 先执行。
更多面试题解答参见 前端进阶面试题详细解答
练习题
练习题做题的依据就是以下图为主,注意要点宏任务是在下一次才执行呢,本次有微任务需要先执行,随后参考下图
习题1
console.log(1)
setTimeout(()=>{
console.log(2)
},0)
process.nextTick(()=>{
console.log(3)
})
new Promise((resolve)=>{
console.log(4)
resolve()
}).then(()=>{
console.log(5)
})
习题1解析
第一轮事件循环
主流程 | Macro event queue | Micro event queue |
---|---|---|
console.log(1) | setTimeout | process |
console.log(4) | then |
- 首先执行同步任务,按出现顺序,输出 1
- 遇到 setTimeout,放入Macro event queue
- 遇到 process,放入 Micro event queue
- 遇到 promise,先立即执行,输出 4,并将 then 回调放入 Micro event queue
- 然后看 Micro event queue,逐个执行,输出 3, 输出 5
- 第一轮 Event Loop 执行结束
第二轮事件循环
主流程 | Macro event queue | Micro event queue |
---|---|---|
setTimeout | ||
- 取出 Macro event queue 第一个放入主流程执行
- 输出 2
- Micro event queue 没有任务
- 第二轮 Event Loop 执行结束
习题2
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
习题2解析
第一轮事件循环
主流程 | Macro event queue | Micro event queue |
---|---|---|
console.log(1) | setTimeout2 | process3 |
console.log(4) | setTimeout6 | then5 |
console.log(7) | setTimeout8 |
- 主流程输出:1, 4, 7
- 执行第一个 Micro event queue:输出 3
- 第二个 Micro event queue:输出 5
- Micro event queue 清空,第一轮执行完毕
第二轮事件循环
主流程 | Macro event queue | Micro event queue |
---|---|---|
setTimeout2 | setTimeout6 | |
setTimeout8 |
- 主流程输出 2
- Micro event queue 为空,第二轮执行完毕
第三轮事件循环
主流程 | Macro event queue | Micro event queue |
---|---|---|
setTimeout6 | setTimeout8 | |
- 主流程输出 6
- 第三轮执行完毕
第四轮事件循环
主流程 | Macro event queue | Micro event queue |
---|---|---|
setTimeout9 | setTimeout10 | then9 |
- 注意,这里执行输出 8 后,resolve,这时才向 Micro event queue 压入 then 回调
- 执行 then9 回调,输出 9
- 又有新的 setTimeout,压入 Macro event queue
- 这轮循环没有东西可执行,结束
第五轮事件循环
主流程 | Macro event queue | Micro event queue |
---|---|---|
console.log(10) | then12 | |
console.log(11) |
- 第五轮,setTimeout10 进入主流程,输出 10
- 遇到 promise,输出 11
- resolve, 压入 then 到 Micro event queue
- 取出 Micro event queue 执行,输出 12
习题3
// 以下代码在 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
习题4
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
习题4解析
- 主线程上没有需要执行的代码
- 接着遇到setTimeout 0,它的作用是在 0ms 后将回调函数放到宏任务队列中(这个任务在下一次的事件循环中执行)。
- 接着遇到setTimeout 0,它的作用是在 0ms 后将回调函数放到宏任务队列中(这个任务在再下一次的事件循环中执行)。
- 首先检查微任务队列, 即 microtask队列,发现此队列不为空,执行第一个promise的then回调,输出 '111'。
- 此时microtask队列为空,进入下一个事件循环, 检查宏任务队列,发现有 setTimeout的回调函数,立即执行回调函数输出 'setTimeout1',检查microtask
队列,发现队列不为空,执行promise的then回调,输出'222',microtask队列为空,进入下一个事件循环。 - 检查宏任务队列,发现有 setTimeout的回调函数, 立即执行回调函数输出'setTimeout2'。
习题5
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*/
习题5解析
- 首先顺序执行完主进程上的同步任务,第一句和最后一句的console.log
- 接着遇到setTimeout 0,它的作用是在 0ms 后将回调函数放到宏任务队列中(这个任务在下一次的事件循环中执行)。
- 接着遇到setTimeout 200,它的作用是在 200ms 后将回调函数放到宏任务队列中(这个任务在再下一次的事件循环中执行)。
- 同步任务执行完之后,首先检查微任务队列, 即 microtask队列,发现此队列不为空,执行第一个promise的then回调,输出 'promise1',然后执行第二个promise的then回调,输出'
promise3',由于第一个promise的.then()的返回依然是promise,所以第二个.then()会放到microtask队列继续执行,输出 'promise2'; - 此时microtask队列为空,进入下一个事件循环, 检查宏任务队列,发现有 setTimeout的回调函数,立即执行回调函数输出 'setTimeout---0',检查microtask 队列,队列为空,进入下一次事件循环.
- 检查宏任务队列,发现有 setTimeout的回调函数, 立即执行回调函数输出'setTimeout---200'.
- 接着遇到setTimeout 0,它的作用是在 0ms 后将回调函数放到宏任务队列中,检查微任务队列,即 microtask 队列,发现此队列不为空,执行promise的then回调,输出'promise5'。
- 此时microtask队列为空,进入下一个事件循环,检查宏任务队列,发现有 setTimeout 的回调函数,立即执行回调函数输出,输出'inner-setTimeout---0'。代码执行结束.
习题6
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
习题6解析
习题7
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
习题7解析
习题8
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
习题8解析
习题9
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号
习题9解析
习题10
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
习题10解析
结合我们上述的JS运行机制再来看这道题就简单明了的多了
- JS是顺序从上而下执行
- 执行到test(),test方法为同步,直接执行,console.log(1)打印1
- test方法中setTimeout为异步宏任务,回调我们把它记做timer1放入宏任务队列
- 接着执行,test方法下面有一个setTimeout为异步宏任务,回调我们把它记做timer2放入宏任务队列
- 接着执行promise,new Promise是同步任务,直接执行,打印4
- new Promise里面的setTimeout是异步宏任务,回调我们记做timer3放到宏任务队列
- Promise.then是微任务,放到微任务队列
- console.log(8)是同步任务,直接执行,打印8
- 主线程任务执行完毕,检查微任务队列中有Promise.then
- 开始执行微任务,发现有setTimeout是异步宏任务,记做timer4放到宏任务队列
- 微任务队列中的console.log(7)是同步任务,直接执行,打印7
- 微任务执行完毕,第一次循环结束
- 检查宏任务队列,里面有timer1、timer2、timer3、timer4,四个定时器宏任务,按照定时器延迟时间得到可以执行的顺序,即Event
Queue:timer2、timer4、timer3、timer1,依次拿出放入执行栈末尾执行 (插播一条:浏览器 event loop 的 Macrotask queue,就是宏任务队列在每次循环中只会读取一个任务) - 执行timer2,console.log(3)为同步任务,直接执行,打印3
- 检查没有微任务,第二次Event Loop结束
- 执行timer4,console.log(6)为同步任务,直接执行,打印6
- 检查没有微任务,第三次Event Loop结束
- 执行timer3,console.log(5)同步任务,直接执行,打印5
- 检查没有微任务,第四次Event Loop结束
- 执行timer1,console.log(2)同步任务,直接执行,打印2
- 检查没有微任务,也没有宏任务,第五次Event Loop结束 结果:1,4,8,7,3,6,5,2
习题11
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
习题11解析
习题12
for (let i = 0; i < 5; i++) {
console.log(i)
}
console.log('done')
// 0 1 2 3 4 done
习题12解析
习题13
console.log(1)
Promise.resolve().then(() => {
console.log(2)
})
console.log(3)
//1 3 2
习题13解析
习题14
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
习题14解析
习题15
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
习题15解析
习题16
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
习题16解析
习题17
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
习题17解析
-
整体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')
习题18
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
习题18解析
首先,事件循环从宏任务 (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
是一门单线程语言,异步操作都是放到事件循环队列里面,等待主执行栈来执行的,并没有专门的异步执行线程。
习题19
console.log(1)
setTimeout(function() {
console.log(2)
},0)
setTimeout(function() {
console.log(3)
},0)
console.log(4)
// 1 4 2 3
习题19解析
习题20
function fun1(){
console.log(1)
}
function fun2(){
console.log(2)
fun1()
console.log(3)
}
fun2()
// 2 1 3
习题20解析
习题21
function func1(){
console.log(1)
}
function func2(){
setTimeout(()=>{
console.log(2)
},0)
func1()
console.log(3)
}
func2()
// 1 3 2
习题21解析
习题22
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
习题22解析
习题21
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
习题22解析
解析: (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 已空, 同时整段代码执行完毕。
标签:执行,console,log,22,js,几道,任务,resolve,setTimeout From: https://www.cnblogs.com/loveX001/p/16749952.html