22 道 js 输出顺序问题, 你能做出几道
前言
最近在准备面试题,console 的输出顺序之前一直迷迷糊糊。
必备知识
JS 是单线程的
单线程是 JavaScript 核心特征之一。这意味着,在 JS 中所有任务都需要排队执行,前一个任务结束,才会执行后一个任务。所以这就造成了一个问题:如果前一个任务耗时很长,后一个任务就不得不一直等着前面的任务执行完才能执行。比如我们向服务器请求一段数据,由于网络问题,可能需要等待 60 秒左右才能成功返回数据,此时只能等待请求完成,JS 才能去处理后面的代码。
同步任务和异步任务
为了解决 JS 单线程带来的问题,JavaScript 就将所有任务分成了同步任务和异步任务。
同步任务(Synchronous)
同步任务指的是当前一个(如果有)任务执行完毕,接下来可以立即执行的任务。这些任务将在主线程上依次排队执行。也就是说排排队
异步任务(Asynchronous)
异步任务相对于同步任务,指的是不需要进入主线程排队执行,而是进入超车道、并车道。也就是任务队列中,形成一系列的任务。这些任务只有当被通知可以执行的时候,该任务才会重新进入主线程执行。
具体来说就是,所有同步任务会在主线程上依次排队执行,形成一个执行栈(Execution ContextStack)。主线程之外,还存在一个任务队列。当异步任务有了运行结果,会在任务队列之中放置对应的事件。当执行栈中的所有同步任务执行完毕,任务队列里的异步任务就会进入执行栈,然后继续依次执行。
异步任务(任务队列)可以分为
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
注意: (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 priorityover the Other Micro tasks queue.)
一个事件循环(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
习题 1 解析
第一轮事件循环
首先执行同步任务,按出现顺序,输出 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 第一个放入主流程执行
输出 2
Micro event queue 没有任务
第二轮 Event Loop 执行结束
习题 2
习题 2 解析
第一轮事件循环
主流程输出:1, 4, 7
执行第一个 Micro event queue:输出 3
第二个 Micro event queue:输出 5
Micro event queue 清空,第一轮执行完毕
第二轮事件循环
主流程输出 2
Micro event queue 为空,第二轮执行完毕
第三轮事件循环
主流程输出 6
第三轮执行完毕
第四轮事件循环
注意,这里执行输出 8 后,resolve,这时才向 Micro event queue 压入 then 回调
执行 then9 回调,输出 9
又有新的 setTimeout,压入 Macro event queue
这轮循环没有东西可执行,结束
第五轮事件循环
第五轮,setTimeout10 进入主流程,输出 10
遇到 promise,输出 11
resolve, 压入 then 到 Micro event queue
取出 Micro event queue 执行,输出 12
习题 3
习题 4
习题 4 解析
主线程上没有需要执行的代码
接着遇到 setTimeout 0,它的作用是在 0ms 后将回调函数放到宏任务队列中(这个任务在下一次的事件循环中执行)。
接着遇到 setTimeout 0,它的作用是在 0ms 后将回调函数放到宏任务队列中(这个任务在再下一次的事件循环中执行)。
首先检查微任务队列, 即 microtask 队列,发现此队列不为空,执行第一个 promise 的 then 回调,输出 '111'。
此时 microtask 队列为空,进入下一个事件循环, 检查宏任务队列,发现有 setTimeout 的回调函数,立即执行回调函数输出 'setTimeout1',检查 microtask 队列,发现队列不为空,执行 promise 的 then 回调,输出'222',microtask 队列为空,进入下一个事件循环。
检查宏任务队列,发现有 setTimeout 的回调函数, 立即执行回调函数输出'setTimeout2'。参考: 前端进阶面试题详细解答
习题 5
习题 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
习题 6 解析
习题 7
习题 7 解析
习题 8
习题 8 解析
习题 9
习题 9 解析
习题 10
习题 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,四个定时器宏任务,按照定时器延迟时间得到可以执行的顺序,即 EventQueue: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
习题 11 解析
习题 12
习题 12 解析
习题 13
习题 13 解析
习题 14
习题 14 解析
习题 15
习题 15 解析
习题 16
习题 16 解析
习题 17
习题 17 解析
整体 script 作为第一个宏任务进入主线程,遇到 console.log,输出 script start
遇到 setTimeout,其回调函数被分发到宏任务 Event Queue 中
遇到 Promise,其 then 函数被分到到微任务 Event Queue 中,记为 then1,之后又遇到了 then 函数,将其分到微任务 Event Queue 中,记为 then2
遇到 console.log,输出 script end。至此,Event Queue 中存在三个任务,如下表:
习题 18
习题 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
习题 19 解析
习题 20
习题 20 解析
习题 21
习题 21 解析
习题 22
习题 22 解析
习题 21
习题 22 解析
解析: (1)先按照 macrotask 和 microtask 划分代码:
setInterval 是 macrotask,其回调函数在 microtask 后执行
setTimeout 是 macrotask,其回调函数放在下一车(cycle 2)执行
Promise.resolve () 的两个 then () 是 microtask
(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 已空, 同时整段代码执行完毕。
评论