金三银四进大厂 |一文搞懂Promise面试题(一)

651 阅读12分钟

我正在参与掘金创作者训练营第4期,点击了解活动详情,一起学习吧!

前言

作者寄语

对于Promise,你是不是这样的?

image.png

之前对于 Promise,宏任务,微任务这些概念一直都是混淆不清,每次遇到类似的面试题,我都是靠着盲猜,就算猜对了,面试官问我为什么是这样的?为什么?我也不知道为什么?你是不是也是出于这样的现状,那么就好好看看这篇文章,带你彻底搞懂晦涩难懂的Promise面试题。

本文的题目没有到特别深入,不过应该覆盖了大部分的考点,另外为了不把大家绕混,答案也没有考虑在 Node 的执行结果,执行结果全为浏览器环境下。因此如果你都会做或者有什么疑问的话,可以尽情的在评论区留言,我会一一答复的。

📢一共分为上下上下两篇文章。

💡 通过阅读本篇文章你可以学到:

  • ✅ Promise 的几道基础题。
  • ✅ Promise 结合 setTimeout。
  • ✅ Promise 中的 then、catch、finally。

前期准备

在做下面 👇 的题目之前,我希望你能清楚几个知识点💡💡💡。

event loop 它的执行顺序:

  • 📌一开始整个脚本作为一个宏任务执行
  • 📌 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
  • 📌 当前宏任务执行完出队,检查微任务列表,有则依次执行,直到全部执行完
  • 📌 执行浏览器 UI 线程的渲染工作
  • 📌 检查是否有 Web Worker 任务,有则执行
  • 📌 执行完本轮的宏任务,回到 2,依此循环,直到宏任务和微任务队列都为空

微任务包括:MutationObserver、Promise.then()或 reject()、Promise 为基础开发的其它技术,比如 fetch API、V8 的垃圾回收过程、Node 独有的 process.nextTick。

宏任务包括:script 、setTimeout 、setInterval 、setImmediate 、I/O 、UI rendering。

注意 🚸:在所有任务开始的时候,由于宏任务中包括了 script,所以浏览器会先执行一个宏任务,在这个过程中你看到的延迟任务(例如 setTimeout)将被放到下一轮宏任务中来执行。

👇 下面我们从易到难,循序渐进。

📕备注:下面所有的题目可以直接复制到浏览器查看执行结果,建议大家先看题目,然后再查看答案,这样就明白自己的思路是对的还是错的,然后再看我的过程分析,就掌握的更加透彻,记忆更加的深刻了。

1.基础题型

题 1-1

题目

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

过程分析

  • 从上至下,先遇到 new Promise,执行该构造函数中的代码 promise1
  • 然后执行同步代码 1,此时 promise1 没有被 resolve 或者 reject,因此状态还是 pending

执行结果

'promise1'
'1' Promise{<pending>}

题 1-2

题目

const promise = new Promise((resolve, reject) => {
  console.log(1);
  resolve('success');
  console.log(2);
});
promise.then(() => {
  console.log(3);
});
console.log(4);

过程分析

  • 从上至下,先遇到 new Promise,执行其中的同步代码 1
  • 再遇到 resolve('success'), 将 promise 的状态改为了 resolved 并且将值保存下来
  • 继续执行同步代码 2
  • 跳出 promise,往下执行,碰到 promise.then 这个微任务,将其加入微任务队列
  • 执行同步代码 4
  • 本轮宏任务全部执行完毕,检查微任务队列,发现 promise.then 这个微任务且状态为 resolved,执行它。

执行结果

1 2 4 3

题 1-3

题目

const promise1 = new Promise((resolve, reject) => {
  console.log('promise1');
  resolve('resolve1');
});
const promise2 = promise1.then((res) => {
  console.log(res);
});
console.log('1', promise1);
console.log('2', promise2);

过程分析

  • 从上至下,先遇到 new Promise,执行该构造函数中的代码 promise1
  • 碰到 resolve 函数, 将 promise1 的状态改变为 resolved, 并将结果保存下来
  • 碰到 promise1.then 这个微任务,将它放入微任务队列
  • promise2 是一个新的状态为 pending 的 Promise
  • 执行同步代码 1, 同时打印出 promise1 的状态是 resolved
  • 执行同步代码 2,同时打印出 promise2 的状态是 pending
  • 宏任务执行完毕,查找微任务队列,发现 promise1.then 这个微任务且状态为 resolved,执行它

执行结果

'promise1'
'1' Promise{<resolved>: 'resolve1'}
'2' Promise{<pending>}
'resolve1'

好嘞,学完了这几道基础题,是不是感觉自己又行了?

image.png

2.Promise 结合 setTimeout

题 2-1

题目

console.log('start');
setTimeout(() => {
  console.log('time');
});
Promise.resolve().then(() => {
  console.log('resolve');
});
console.log('end');

过程分析

  • 刚开始整个脚本作为一个宏任务来执行,对于同步代码直接压入执行栈进行执行,因此先打印出 start 和 end。
  • setTimout 作为一个宏任务被放入宏任务队列(下一个)
  • Promise.then 作为一个微任务被放入微任务队列
  • 本次宏任务执行完,检查微任务,发现 Promise.then,执行它
  • 接下来进入下一个宏任务,发现 setTimeout,执行。

执行结果

'start'
'end'
'resolve'
'time'

题 2-2

题目

const promise = new Promise((resolve, reject) => {
  console.log(1);
  setTimeout(() => {
    console.log('timerStart');
    resolve('success');
    console.log('timerEnd');
  }, 0);
  console.log(2);
});
promise.then((res) => {
  console.log(res);
});
console.log(4);

过程分析

  • 从上至下,先遇到 new Promise,执行该构造函数中的代码 1
  • 然后碰到了定时器,将这个定时器中的函数放到下一个宏任务的延迟队列中等待执行
  • 执行同步代码 2
  • 跳出 promise 函数,遇到 promise.then,但其状态还是为 pending,这里理解为先不执行
  • 执行同步代码 4
  • 一轮循环过后,进入第二次宏任务,发现延迟队列中有 setTimeout 定时器,执行它
  • 首先执行 timerStart,然后遇到了 resolve,将 promise 的状态改为 resolved 且保存结果并将之前的 promise.then 推入微任务队列
  • 继续执行同步代码 timerEnd
  • 宏任务全部执行完毕,查找微任务队列,发现 promise.then 这个微任务,执行它。

执行结果

1
2
4
"timerStart"
"timerEnd"
"success"

题 2-3

题目

Promise.resolve().then(() => {
  console.log('promise1');
  const timer2 = setTimeout(() => {
    console.log('timer2');
  }, 0);
});
const timer1 = setTimeout(() => {
  console.log('timer1');
  Promise.resolve().then(() => {
    console.log('promise2');
  });
}, 0);
console.log('start');

过程分析

这道题稍微的难一些,在 promise 中执行定时器,又在定时器中执行 promise;

并且要注意的是,这里的 Promise 是直接 resolve 的,而之前的 new Promise 不一样。

因此执行过程为:

  • 刚开始整个脚本作为第一次宏任务来执行,我们将它标记为宏 1,从上至下执行
  • 遇到 Promise.resolve().then 这个微任务,将 then 中的内容加入第一次的微任务队列标记为微 1
  • 遇到定时器 timer1,将它加入下一次宏任务的延迟列表,标记为宏 2,等待执行(先不管里面是什么内容)
  • 执行宏 1 中的同步代码 start
  • 第一次宏任务(宏 1)执行完毕,检查第一次的微任务队列(微 1),发现有一个 promise.then 这个微任务需要执行
  • 执行打印出微 1 中同步代码 promise1,然后发现定时器 timer2,将它加入宏 2 的后面,标记为宏 3
  • 第一次微任务队列(微 1)执行完毕,执行第二次宏任务(宏 2),首先执行同步代码 timer1
  • 然后遇到了 promise2 这个微任务,将它加入此次循环的微任务队列,标记为微 2
  • 宏 2 中没有同步代码可执行了,查找本次循环的微任务队列(微 2),发现了 promise2,执行它
  • 第二轮执行完毕,执行宏 3,打印出 timer2

执行结果

'start'
'promise1'
'timer1'
'promise2'
'timer2'

3.Promise 中的 then、catch、finally

题 3-1

题目

const promise = new Promise((resolve, reject) => {
  resolve('success1');
  reject('error');
  resolve('success2');
});
promise
  .then((res) => {
    console.log('then: ', res);
  })
  .catch((err) => {
    console.log('catch: ', err);
  });

过程分析

构造函数中的 resolve 或 reject 只有第一次执行有效,多次调用没有任何作用 。得出结论:Promise 的状态一经改变就不能再改变。

执行结果

"then: success1"

题 3-2

题目

const promise = newPromise((resolve, reject) => {
  reject('error');
  resolve('success2');
});
promise
  .then((res) => {
    console.log('then: ', res);
  })
  .then((res) => {
    console.log('then: ', res);
  })
  .catch((err) => {
    console.log('catch: ', err);
  })
  .then((res) => {
    console.log('then: ', res);
  });

执行结果

"catch: " "error"
"then3: " undefined

结论

catch 不管被连接到哪里,都能捕获上层的错误。

题 3-3

题目

Promise.resolve(1)
  .then((res) => {
    console.log(res);
    return2;
  })
  .catch((err) => {
    return3;
  })
  .then((res) => {
    console.log(res);
  });

过程分析

Promise 可以链式调用,不过 promise 每次调用 .then 或者 .catch 都会返回一个新的 promise,从而实现了链式调用, 它并不像一般我们任务的链式调用一样 return this。

上面的输出结果之所以依次打印出 1 和 2,那是因为 resolve(1)之后走的是第一个 then 方法,并没有走 catch 里,所以第二个 then 中的 res 得到的实际上是第一个 then 的返回值。且 return 2 会被包装成 resolve(2)。

执行结果

1
2

题 3-4

题目

Promise.resolve()
  .then(() => {
    returnnewError('error!!!');
  })
  .then((res) => {
    console.log('then: ', res);
  })
  .catch((err) => {
    console.log('catch: ', err);
  });

过程分析

猜猜这里的结果输出的是什么 🤔️ ?

你可能想到的是进入.catch 然后被捕获了错误。

结果并不是这样的,它走的是.then 里面,返回任意一个非 promise 的值都会被包裹成 promise 对象,因此这里的 return new Error('error!!!')也被包裹成了 return Promise.resolve(new Error('error!!!'))。

当然如果你抛出一个错误的话,可以用下面 👇 两的任意一种:

returnPromise.reject(newError('error!!!'));
// or
thrownewError('error!!!');

执行结果

"then: " "Error: error!!!"

题 3-5

题目

接下来介绍一下.then 函数中的两个参数。

第一个参数是用来处理 Promise 成功的函数,第二个则是处理失败的函数。

也就是说 Promise.resolve('1')的值会进入成功的函数,Promise.reject('2')的值会进入失败的函数。

让我们来看看这个例子 🌰:

Promise.reject('err!!!')
  .then(
    (res) => {
      console.log('success', res);
    },
    (err) => {
      console.log('error', err);
    }
  )
  .catch((err) => {
    console.log('catch', err);
  });

执行结果

'error' 'error!!!'

它进入的是 then()中的第二个参数里面,而如果把第二个参数去掉,就进入了 catch()中:

Promise.reject('err!!!')
  .then((res) => {
    console.log('success', res);
  })
  .catch((err) => {
    console.log('catch', err);
  });

执行结果

'catch' 'error!!!'

题 3-6

接着来看看.finally(),这个功能一般不太用在面试中,不过如果碰到了你也应该知道该如何处理。

其实你只要记住它三个很重要的知识点就可以了:

  • .finally()方法不管 Promise 对象最后的状态如何都会执行
  • .finally()方法的回调函数不接受任何的参数,也就是说你在.finally()函数中是没法知道 Promise 最终的状态是 resolved 还是 rejected 的
  • 它最终返回的默认会是一个原来的 Promise 对象值,不过如果抛出的是一个异常则返回异常的 Promise 对象。

来看看这个简单的例子 🌰:

题目

Promise.resolve('1')
  .then((res) => {
    console.log(res);
  })
  .finally(() => {
    console.log('finally');
  });
Promise.resolve('2')
  .finally(() => {
    console.log('finally2');
    return '我是finally2返回的值';
  })
  .then((res) => {
    console.log('finally2后面的then函数', res);
  });

过程分析: 这两个 Promise 的.finally 都会执行,且就算 finally2 返回了新的值,它后面的 then()函数接收到的结果却还是'2',因此打印结果为:

执行结果

'1'
'finally2'
'finally'
'finally2后面的then函数' '2'

让我们来看一个比较难的例子 🌰:

function promise1 () {
  let p = new Promise((resolve) => {
    console.log('promise1');
    resolve('1')
  })
  return p;
}
function promise2 () {
  retur nnew Promise((resolve, reject) => {
    reject('error')
  })
}
promise1()
  .then(res =>console.log(res))
  .catch(err =>console.log(err))
  .finally(() =>console.log('finally1'))

promise2()
  .then(res =>console.log(res))
  .catch(err =>console.log(err))
  .finally(() =>console.log('finally2'))

过程分析

  • 首先定义了两个函数 promise1 和 promise2,先不管接着往下看。
  • promise1 函数先被调用了,然后执行里面 new Promise 的同步代码打印出 promise1
  • 之后遇到了 resolve(1),将 p 的状态改为了 resolved 并将结果保存下来。
  • 此时 promise1 内的函数内容已经执行完了,跳出该函数
  • 碰到了 promise1().then(),由于 promise1 的状态已经发生了改变且为 resolved 因此将 promise1().then()这条微任务加入本轮的微任务列表(这是第一个微任务)
  • 这时候要注意了,代码并不会接着往链式调用的下面走,也就是不会先将.finally 加入微任务列表,那是因为.then 本身就是一个微任务,它链式后面的内容必须得等当前这个微任务执行完才会执行,因此这里我们先不管.finally()
  • 再往下走碰到了 promise2()函数,其中返回的 new Promise 中并没有同步代码需要执行,所以执行 reject('error')的时候将 promise2 函数中的 Promise 的状态变为了 rejected
  • 跳出 promise2 函数,遇到了 promise2().then(),将其加入当前的微任务队列(这是第二个微任务),且链式调用后面的内容得等该任务执行完后才执行,和.then()一样。
  • OK, 本轮的宏任务全部执行完了,来看看微任务列表,存在 promise1().then(),执行它,打印出 1,然后遇到了.finally()这个微任务将它加入微任务列表(这是第三个微任务)等待执行
  • 再执行 promise2().catch()打印出 error,执行完后将 finally2 加入微任务加入微任务列表(这是第四个微任务)
  • OK, 本轮又全部执行完了,但是微任务列表还有两个新的微任务没有执行完,因此依次执行 finally1 和 finally2。

执行结果

'promise1'
'1'
'error'
'finally1'
'finally2'

一口气看了这么多题,让我们喝一口 🍵,然后总结一下:

📗总结结论

  • 📝 Promise 的状态一经改变就不能再改变.
  • 📝 .then 和.catch 都会返回一个新的 Promise.
  • 📝 catch 不管被连接到哪里,都能捕获上层的错误。 -在 Promise 中,返回任意一个非 promise 的值都会被包裹成 promise 对象,例如 return 2 会被包装为 return Promise.resolve(2)。
  • 📝 Promise 的 .then 或者 .catch 可以被调用多次, 当如果 Promise 内部的状态一经改变,并且有了一个值,那么后续每次调用.then 或者.catch 的时候都会直接拿到该值。
  • 📝 .then 或者 .catch 中 return 一个 error 对象并不会抛出错误,所以不会被后续的 .catch 捕获。
  • 📝 .then 或 .catch 返回的值不能是 promise 本身,否则会造成死循环。
  • 📝 .then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透。
  • 📝 .then 方法是能接收两个参数的,第一个是处理成功的函数,第二个是处理失败的函数,再某些时候你可以认为 catch 是.then 第二个参数的简便写法。
  • 📝 .finally 方法也是返回一个 Promise,他在 Promise 结束的时候,无论结果为 resolved 还是 rejected,都会执行里面的回调函数。

下期文章预告:

  • Promise 中的 all 和 race
  • async/await 的几道题
  • 大厂面试题

写在最后

朽木不雕难成才,知耻后勇凌云志。 我是朽木白,一个热爱分享的程序猿。如果觉得本文还不错,记得点赞➕关注,说不定哪天就用得上!您的鼓励是我坚持下去的最大动力❤️❤️❤️。