10个JavaScript Promise的面试题

Promise是JavaScript异步编程的关键特性。不管你爱它还是恨它,你都必须理解它。

在这里,我整理了一些关于Promise的面试挑战题,从基础到高级。有10个代码片段,在阅读我的分析之前,请先自己尝试一下。

同步代码块

console.log('start')

const p1 = new Promise((resolve, reject) => {
    console.log(1)
})

console.log('end')

第一个问题非常简单。

  • 我们知道:
    • 同步的代码块总是从上到下顺序执行。
    • 当我们调用 new Promise(callback) 时,回调函数会立即执行。

所以这段代码是依次输出start、1、end。

出现异步代码的地方

console.log('start')

const p1 = new Promise((resolve, reject) => {
    console.log(1)
    resolve(2)
})

p1.then(res => {
    console.log(res)
})

console.log('end')

在这段代码片段中,出现了一段异步代码。也就是.then()中的回调函数。

请记住,JavaScript引擎总是先执行同步代码,然后再执行异步代码。

遇到这个问题,我们只需要区分同步代码和异步代码即可。

所以输出的结果是start、 1、end和2。

有“resolve”的地方

console.log('start')

const p1 = new Promise((resolve, reject) => {
    console.log(1)
    resolve(2)
    console.log(3)
})

p1.then(res => {
    console.log(res)
})

console.log('end')

这段代码与前面的代码几乎相同;唯一的区别是在resolve(2)之后有一个console.log(3)。

请记住,resolve 方法不会中断函数的执行。它背后的代码仍然会继续执行。

所以输出结果是start, 1, 3, end和2。

因为我遇到过一些人认为resolve会中断函数的执行,所以在这里强调一下。

不被调用“resolve”的地方

console.log('start')

const p1 = new Promise((resolve, reject) => {
    console.log(1)
})

p1.then(res => {
    console.log(2)
})

console.log('end')

在这段代码中,resolve方法从未被调用过,因此promise1始终处于挂起状态。所以promise1.then(...)从未被执行过。2不会在控制台中打印出来。

所以输出结果是start, 1, end。

让你困惑的地方

console.log('start')

const fn = () => new Promise((resolve, reject) => {
    console.log(1)
    resolve('success')
})

console.log('middle')

fn().then(res => {
    console.log(res)
})

console.log('end')

这段代码特意增加了一个迷惑挑战者的功能,那就是fn。

但是请记住,无论有多少层函数调用,我们的基本原则都是一样的:

先执行同步代码,再执行异步代码

同步代码按调用顺序执行

所以输出结果是start、middle、1、end和success。

好的,你觉得这些挑战容易吗?

事实上,这些只是开胃菜。Promise的难点在于它与setTimeout一起出现。接下来,我们加大挑战的难度。

你准备好了吗?开发者!

Fulfilling Promise

console.log('start')

Promise.resolve(1).then(res => {
    console.log(res)
})

Promise.resolve(2).then(res => {
    console.log(res)
})

console.log('end')

这里Promise.resolve(1)将返回一个Promise对象,其状态为已完成,结果为1,它是同步代码。

所以输出结果是start、end、1和2。

测试回调基础的地方

console.log('start')

setTimeout(() => {
    console.log('setTimeout')
})

Promise.resolve().then(() => {
    console.log('resolve')
})

console.log('end')

注意!

这是一个非常困难的问题。如果你能正确回答这个问题并说明原因,那么你对 JavaScript 中异步编程的理解已经达到了中级水平。

在解释这个问题之前,让我们先讨论一下相关的理论基础知识。

之前我们说过同步代码是按照调用顺序执行的,那么这些异步回调函数是按照什么顺序执行的呢?

有人可能会说,谁先完成谁先执行。嗯,这是真的,但是如果两个异步任务同时完成呢?

比如上面的代码中,setTimeout的定时器是0秒,Promise.resolve()也会在执行后立即返回一个已完成的Promise对象。

两个异步任务都是立即完成的,那么谁的回调函数会先执行呢?

有的人可能会说setTimeout在前面,所以先打印setTimeout,再打印resolve。实际上,这种说法是错误的。

我们知道很多事情不是按照先进先出的顺序执行的,比如流量。

  • 在JavaScript EventLoop中,还有优先级的概念。
    • 具有较高优先级的任务称为微任务。包括:Promise、ObjectObserver、MutationObserver、process.nextTick、async/await。
    • 优先级较低的任务称为宏任务。包括:setTimeout、setInterval和XHR。

虽然setTimeout和Promise.resolve()是同时完成的,甚至setTimeout的代码还是领先的,但是由于它的优先级低,属于它的回调函数是在后面执行的。

所以输出结果是start、end、resolve和success。

一个有微任务和宏任务代码的

const promise = new Promise((resolve, reject) => {
    console.log(1)

    setTimeout(() => {
        console.log('timerStart')
        resolve('success')
        console.log('timerEnd')
    })

    console.log(2)
})

promise.then(res => {
    console.log(res)
})

console.log(4)

如果您已经了解了前面的代码片段,那么这个挑战很容易完成。

  • 我们只需要做三个步骤:

    • 找到同步代码。
    • 找到微任务代码
    • 找到宏任务代码
  • 首先,执行同步代码:

    • 输出1、2和4。
  • 然后执行微任务:

但是这里有个陷阱:由于当前的promise还处于pending状态,所以这里的代码暂时不会被执行。

  • 然后执行宏任务:

并且promise的状态正在成为fulfilled。

  • 然后,使用事件循环,再次执行微任务:

所以执行效果:1、4、timerStart、timerEnd、success。

将被要求在微任务和宏任务之间进行优先排序

在介绍微任务和宏任务的优先级之前,这里先看一下微任务和宏任务交替执行的情况。

const timer1 = setTimeout(() => {
    console.log('timer1')

    const p1 = Promise.resolve().then(() => {
        console.log('p1')
    })
}, 0)

const timer2 = setTimeout(() => {
    console.log('timer2')
})

输出:timer1、p1、timer2

最后一次测试你的Promise基础

这是我们最后一个挑战,如果你能正确说出这段代码的输出结果,那么你对Promise的理解就已经很强了,同类型的面试题根本难不倒你。

console.log('start')

const p1 = Promise.resolve().then(() => {
    console.log('p1')

    const t2 = setTimeout(() => {
        console.log('t2')
    }, 0)
})

const t1 = setTimeout(() => {
    console.log('t1')
    const p2 = Promise.resolve().then(() => {
        console.log('p2')
    })
}, 0)

console.log('end')

本次挑战是上一次挑战的升级版,但核心原理保持不变。

  • 记住我们之前学到的:
    • 同步代码
    • 所有微任务
    • 第一个宏任务
    • 所有新添加的微任务
    • 下一个宏任务

输出:start、end、p1、t1、p2、t2。

贡献者: mankueng