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

前言

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

注意: (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.)

一个事件循环(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. 首先执行同步任务,按出现顺序,输出 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 执行结束

第二轮事件循环

主流程 Macro event queue Micro event queue
setTimeout
  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解析

第一轮事件循环

主流程 Macro event queue Micro event queue
console.log(1) setTimeout2 process3
console.log(4) setTimeout6 then5
console.log(7) setTimeout8
  1. 主流程输出:1, 4, 7
  2. 执行第一个 Micro event queue:输出 3
  3. 第二个 Micro event queue:输出 5
  4. Micro event queue 清空,第一轮执行完毕

第二轮事件循环

主流程 Macro event queue Micro event queue
setTimeout2 setTimeout6
setTimeout8
  1. 主流程输出 2
  2. Micro event queue 为空,第二轮执行完毕

第三轮事件循环

主流程 Macro event queue Micro event queue
setTimeout6 setTimeout8
  1. 主流程输出 6
  2. 第三轮执行完毕

第四轮事件循环

主流程 Macro event queue Micro event queue
setTimeout9 setTimeout10 then9
  1. 注意,这里执行输出 8 后,resolve,这时才向 Micro event queue 压入 then 回调
  2. 执行 then9 回调,输出 9
  3. 又有新的 setTimeout,压入 Macro event queue
  4. 这轮循环没有东西可执行,结束

第五轮事件循环

主流程 Macro event queue Micro event queue
console.log(10) then12
console.log(11)
  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")
  })
  
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值