写点什么

22 道 js 输出顺序问题, 你能做出几道

作者:loveX001
  • 2023-01-09
    浙江
  • 本文字数:9760 字

    阅读完需:约 32 分钟

前言

最近在准备面试题,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 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

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 解析

第一轮事件循环



  1. 首先执行同步任务,按出现顺序,输出 1

  2. 遇到 setTimeout,放入 Macro event queue

  3. 遇到 process,放入 Micro event queue

  4. 遇到 promise,先立即执行,输出 4,并将 then 回调放入 Micro event queue

  5. 然后看 Micro event queue,逐个执行,输出 3, 输出 5

  6. 第一轮 Event Loop 执行结束


第二轮事件循环



  1. 取出 Macro event queue 第一个放入主流程执行

  2. 输出 2

  3. Micro event queue 没有任务

  4. 第二轮 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 解析

第一轮事件循环



  1. 主流程输出:1, 4, 7

  2. 执行第一个 Micro event queue:输出 3

  3. 第二个 Micro event queue:输出 5

  4. Micro event queue 清空,第一轮执行完毕


第二轮事件循环



  1. 主流程输出 2

  2. Micro event queue 为空,第二轮执行完毕


第三轮事件循环



  1. 主流程输出 6

  2. 第三轮执行完毕


第四轮事件循环



  1. 注意,这里执行输出 8 后,resolve,这时才向 Micro event queue 压入 then 回调

  2. 执行 then9 回调,输出 9

  3. 又有新的 setTimeout,压入 Macro event queue

  4. 这轮循环没有东西可执行,结束


第五轮事件循环



  1. 第五轮,setTimeout10 进入主流程,输出 10

  2. 遇到 promise,输出 11

  3. resolve, 压入 then 到 Micro event queue

  4. 取出 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 解析

  1. 主线程上没有需要执行的代码

  2. 接着遇到 setTimeout 0,它的作用是在 0ms 后将回调函数放到宏任务队列中(这个任务在下一次的事件循环中执行)。

  3. 接着遇到 setTimeout 0,它的作用是在 0ms 后将回调函数放到宏任务队列中(这个任务在再下一次的事件循环中执行)。

  4. 首先检查微任务队列, 即 microtask 队列,发现此队列不为空,执行第一个 promise 的 then 回调,输出 '111'。

  5. 此时 microtask 队列为空,进入下一个事件循环, 检查宏任务队列,发现有 setTimeout 的回调函数,立即执行回调函数输出 'setTimeout1',检查 microtask 队列,发现队列不为空,执行 promise 的 then 回调,输出'222',microtask 队列为空,进入下一个事件循环。

  6. 检查宏任务队列,发现有 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 解析

  1. 首先顺序执行完主进程上的同步任务,第一句和最后一句的 console.log

  2. 接着遇到 setTimeout 0,它的作用是在 0ms 后将回调函数放到宏任务队列中(这个任务在下一次的事件循环中执行)。

  3. 接着遇到 setTimeout 200,它的作用是在 200ms 后将回调函数放到宏任务队列中(这个任务在再下一次的事件循环中执行)。

  4. 同步任务执行完之后,首先检查微任务队列, 即 microtask 队列,发现此队列不为空,执行第一个 promise 的 then 回调,输出 'promise1',然后执行第二个 promise 的 then 回调,输出'promise3',由于第一个 promise 的.then()的返回依然是 promise,所以第二个.then()会放到 microtask 队列继续执行,输出 'promise2';

  5. 此时 microtask 队列为空,进入下一个事件循环, 检查宏任务队列,发现有 setTimeout 的回调函数,立即执行回调函数输出 'setTimeout---0',检查 microtask 队列,队列为空,进入下一次事件循环.

  6. 检查宏任务队列,发现有 setTimeout 的回调函数, 立即执行回调函数输出'setTimeout---200'.

  7. 接着遇到 setTimeout 0,它的作用是在 0ms 后将回调函数放到宏任务队列中,检查微任务队列,即 microtask 队列,发现此队列不为空,执行 promise 的 then 回调,输出'promise5'。

  8. 此时 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 运行机制再来看这道题就简单明了的多了


  1. JS 是顺序从上而下执行

  2. 执行到 test(),test 方法为同步,直接执行,console.log(1)打印 1

  3. test 方法中 setTimeout 为异步宏任务,回调我们把它记做 timer1 放入宏任务队列

  4. 接着执行,test 方法下面有一个 setTimeout 为异步宏任务,回调我们把它记做 timer2 放入宏任务队列

  5. 接着执行 promise,new Promise 是同步任务,直接执行,打印 4

  6. new Promise 里面的 setTimeout 是异步宏任务,回调我们记做 timer3 放到宏任务队列

  7. Promise.then 是微任务,放到微任务队列

  8. console.log(8)是同步任务,直接执行,打印 8

  9. 主线程任务执行完毕,检查微任务队列中有 Promise.then

  10. 开始执行微任务,发现有 setTimeout 是异步宏任务,记做 timer4 放到宏任务队列

  11. 微任务队列中的 console.log(7)是同步任务,直接执行,打印 7

  12. 微任务执行完毕,第一次循环结束

  13. 检查宏任务队列,里面有 timer1、timer2、timer3、timer4,四个定时器宏任务,按照定时器延迟时间得到可以执行的顺序,即 EventQueue:timer2、timer4、timer3、timer1,依次拿出放入执行栈末尾执行 (插播一条:浏览器 event loop 的 Macrotask queue,就是宏任务队列在每次循环中只会读取一个任务)

  14. 执行 timer2,console.log(3)为同步任务,直接执行,打印 3

  15. 检查没有微任务,第二次 Event Loop 结束

  16. 执行 timer4,console.log(6)为同步任务,直接执行,打印 6

  17. 检查没有微任务,第三次 Event Loop 结束

  18. 执行 timer3,console.log(5)同步任务,直接执行,打印 5

  19. 检查没有微任务,第四次 Event Loop 结束

  20. 执行 timer1,console.log(2)同步任务,直接执行,打印 2

  21. 检查没有微任务,也没有宏任务,第五次 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 解析

  1. 整体 script 作为第一个宏任务进入主线程,遇到 console.log,输出 script start

  2. 遇到 setTimeout,其回调函数被分发到宏任务 Event Queue 中

  3. 遇到 Promise,其 then 函数被分到到微任务 Event Queue 中,记为 then1,之后又遇到了 then 函数,将其分到微任务 Event Queue 中,记为 then2

  4. 遇到 console.log,输出 script end。至此,Event Queue 中存在三个任务,如下表:


习题 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 已空, 同时整段代码执行完毕。


用户头像

loveX001

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
22道js输出顺序问题,你能做出几道_JavaScript_loveX001_InfoQ写作社区