【面试题】吃透Promise?先实现一个再说(包含所有方法)

时间:2022-11-29 21:58:00


前言

在网上阅读过很多关于实现promise的文章,大致分为2类,一种是基于promiseA+规范,一种是基于ECMA规范和v8引擎下的promise,对于A+规范可以快速让我们理解promise的核心,但对些许情况,难以理解,而ECMA规范的promise却很难理解,本文会分别对其经行分析

给大家推荐一个实用面试题库

1、前端面试题库 (面试必备)            推荐:★★★★★

地址:前端面试题库

1.基于PromiseA+规范

1.1 前景概要 :

首先要了解promiseA+规范只是社区对于开发者实现promise提出的一个合理的规范而已,它与ECMA规范下的promise(浏览器下的)有很多区别,但是可以满足大部分工作以及学习需求,而且理解比较简单,我们就来简单实现一个promise以及梳理它的流程

let p1 = new Promise((resolve, reject) => {
resolve('成功')
})

复制代码

1.2 Promise是构造函数

这是promise的起点,通过形式可以看到 promise属于构造函数 我们需要通过new关键字来调用,内部接收一个回调函数(我们采用executor代理),内部有2个参数resolve,reject分别是2个回调函数,各携带一个参数,所以我们的雏形来了

class MyPromise {
constructor(executor) {
const resolve = (value) => {}
const reject = (reason) => {}
executor(resolve, reject)
}
}

复制代码

【面试题】吃透Promise?先实现一个再说(包含所有方法)

 继续通过一段代码了解promise

1.3 Promise的状态变更

let p1 = new Promise((resolve, reject) => {
resolve('成功')
reject('失败')
})
p1.then(
(value) => {
console.log(value) //成功
},
(err) => {
console.log(err)
}
)
let p2 = new Promise((resolve, reject) => {
reject('失败')
resolve('成功')
})
p2.then(
(value) => {
console.log(value)
},
(err) => {
console.log(err) //失败
}
)
复制代码

通过上述代码可以知道Promise返回一个实例,并且实例带有then方法,且then方法中包含2个回调函数,(我们以onFulfilled和onRejected代替)可以通过回调函数的参数获取,我们可以通过resolve 和reject函数传递结果,并且通过then里面的回调函数接收对应的结果,而且promise会通过resolve,reject确定状态,一旦确定好状态,就只执行对应的回调函数,忽略其他的resolve或者reject

因此我们需要来指定状态,并且存储resolve,reject的值,从而传递给then

const [PENDING, FULFILLED, REJECTED] = ['PENDING', 'FULFILLED', 'REJECTED']
class MyPromise {
constructor(executor) {
this.status = PENDING
this.value = undefined
this.reason = undefined
const resolve = (value) => {
if (this.status === PENDING) {
this.status = FULFILLED
this.value = value
}
}
const reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED
this.reason = reason
}
}
executor(resolve, reject)
}

then(onFulfilled, onRejected) {
if (this.status === FULFILLED) {
onFulfilled(this.value)
}

if (this.status === REJECTED) {
onRejected(this.reaosn)
}
}
}

复制代码

上述代码,我们声明了PENDING(等待),FULFILLED(成功),REJECTED(失败)三个状态,来记录promise的状态,并且当resolve或者reject时,我们立即修改状态,并且将成功或者失败的值存储起来。在then的回调函数中通过状态判断来执行对应的回调函数

【面试题】吃透Promise?先实现一个再说(包含所有方法)

1.4 解决异步

但是promise是用来解决异步问题的,我们的代码全部是同步执行,还有很多缺陷,例如:

const p1 = new Promise((resolve, reject) => {
setTimeout(function () {
resolve('成功')
})
},3000)
p1.then((value) => {
console.log(value)
})
复制代码

正常情况下会等待3秒确定状态,然后执行对应then的回调函数,但是我们的代码却不会执行,因为刚才也说过我们的代码全部都是同步执行,没有对PENDING状态进行处理,因此我们需要额外对pending状态进行处理 代码如下

const [PENDING, FULFILLED, REJECTED] = ['PENDING', 'FULFILLED', 'REJECTED']
class MyPromise {
constructor(executor) {
this.status = PENDING
this.value = undefined
this.reason = undefined
const resolve = (value) => {
if (this.status === PENDING) {
this.status = FULFILLED
this.value = value
this.onFulfilled && this.onFulfilled(value)
}
}
const reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED
this.reason = reason
this.onRejected && this.onRejected(reason)
}
}
executor(resolve, reject)
}
then(onFulfilled, onRejected) {
if (this.status === FULFILLED) {
onFulfilled(this.value)
}

if (this.status === REJECTED) {
onRejected(this.reaosn)
}
if (this.status === PENDING) {
// 存储回调函数
this.onFulfilled = onFulfilled
this.onRejected = onRejected
}
}
}

复制代码

【面试题】吃透Promise?先实现一个再说(包含所有方法)

 我们在pending状态下将then的回调函数存储下来,在status改变状态后立即执行达到支持异步的效果

1.5 then的微任务?

我们在通过一个例子来完善代码

let p1 = new Promise((resolve, reject) => {
resolve('成功')
})
p1.then((value) => console.log(value))
console.log(11)
// 11 => 成功
复制代码

通过上述代码,以及promise的知识我们应该知道then的回调函数实际是将这个回调加入到了微任务队列中 所以先打印11 然后再打印成功,而我们的代码却并是同步执行,我们需要将then的回调函数模拟微任务的形式,这里我们使用setTimeout来模拟微任务,修改我们的代码

then(onFulfilled, onRejected) {
if (this.status === FULFILLED) {
setTimeout(() => {
onFulfilled(this.value)
})
}

if (this.status === REJECTED) {
setTimeout(() => {
onRejected(this.reason)
})
}
if (this.status === PENDING) {
this.onFulfilled = onFulfilled
this.onRejected = onRejected
}
}
复制代码

【面试题】吃透Promise?先实现一个再说(包含所有方法)

1.6存储处理函数的数据结构

这样就可以解决上述then的回调要进入微任务的情况, 下一个我们要解决的问题是promise多次调用的问题

let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('成功')
})
})
p1.then((value) => {
console.log(value,111)
})
p1.then((value) => {
console.log(value,222)
})
复制代码

上述代码会依次打印成功,但是我们的代码不具备这种条件因为我们的then方法中的onFulfilled会覆盖第一个then的方法的OnFulfilled 这个问题也比较好解决,我们只需要通过一个数组将函数存储起来,到时候遍历调用即可 此时完整代码如下

const [PENDING, FULFILLED, REJECTED] = ['PENDING', 'FULFILLED', 'REJECTED']
class MyPromise {
constructor(executor) {
this.status = PENDING
this.value = undefined
this.reason = undefined
this.onFulfilledCallbacks = []
this.onRejectedCallbacks = []
const resolve = (value) => {
if (this.status === PENDING) {
this.status = FULFILLED
this.value = value
this.onFulfilledCallbacks.forEach((fn) => fn())
}
}
const reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED
this.reason = reason
this.onRejectedCallbacks.forEach((fn) => fn())
}
}
executor(resolve, reject)
}
then(onFulfilled, onRejected) {
if (this.status === FULFILLED) {
setTimeout(() => {
onFulfilled(this.value)
}, 0)
}

if (this.status === REJECTED) {
setTimeout(() => {
onRejected(this.reason)
}, 0)
}
if (this.status === PENDING) {
this.onFulfilledCallbacks.push(() => {
onFulfilled(this.value)
})
this.onRejectedCallbacks.push(() => {
onRejected(this.reason)
})
}
}
}

复制代码

【面试题】吃透Promise?先实现一个再说(包含所有方法)

 接下来我们放松一下,处理点小问题,

1.7 trycatch的引用

let p1 = new MyPromise((resolve, reject) => {
throw new Error('我要报错')
})
p1.then(
(value) => {
console.log(value, 111)
},
(err) => {
console.log(err)
}
)
复制代码

在executor函数中如果报错,如果我们指定了then方法的接收函数的话,promise将其定义为REJECTED状态, 那我们只需要简单的try/catch进行处理下,遇到错误直接reject就完事了(其实其中大有文章,末尾发链接

try {
executor(resolve, reject)
} catch (e) {
reject(e)
}
复制代码

好了放松完了,我们来点刺激的

【面试题】吃透Promise?先实现一个再说(包含所有方法)

2.Promise的链式调用

promise的核心以及最大特点就是链式调用,比如then回调函数的返回值会包裹成一个promise

【面试题】吃透Promise?先实现一个再说(包含所有方法)

 promiseA+规范 2.27明确说明then方法必须返回一个promsie,并且onfulfilled或者onRejected返回值需要再次进行处理(the Promise Resolution Procedure),如果出现异常我们需要reject出去

then(onFulfilled, onRejected) {
let promise2 = new MyPromise((resolve, reject) => {
if (this.status === FULFILLED) {
setTimeout(() => {
try {
let x = onFulfilled(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
}

if (this.status === REJECTED) {
setTimeout(() => {
try {
let x = onRejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
}
if (this.status === PENDING) {
this.onFulfilledCallbacks.push(() => {
try {
let x = onFulfilled(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
this.onRejectedCallbacks.push(() => {
try {
let x = onRejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
}
})
return promise2
}
复制代码

我们按照规范分别对onFulfilled以及OnRejected的函数返回值做了处理(ResolvePromise后面再提作用) 也做错了异常检测 2.27.3 与2.27.4是对then的穿透做处理比较简单 如果onfulfiled不是一个函数并且这个promise的状态是fulfilled,返回值promise2必须指定为一个fulfilled的函数并返回上一个then返回的相同的值 如果onrejected不是一个函数并且这个promsie的状态是rejected,我们只需要将这个rejected的的错误继续抛出即可

onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (value) => value
onRejected = typeof onRejected ==='function' ? onRejected :reason=>{throw reason}
复制代码

这样就简单实现了then的穿透,但是只能验证rejected的情况,需要将resolvePromise函数完成才能达到效果

2.1对then的返回值的封装(resolvePromsie)

我们直接从规范2.3.1开始

【面试题】吃透Promise?先实现一个再说(包含所有方法)

 如果这个promise与返回值x相等,则需要reject这个类型错误 类似于这种情况:

let p1 = new Promise((resolve, reject) => {
resolve('成功')
}).then((value) => {
return p1
// Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>
})
复制代码

那么我们开始封装resolvePromise

function resolvePromise(promise2, x, resolve, reject) {
if (x === promise2) {
return reject(new TypeError('Chaining cycle detected for promise #<MyPromise>'))
}
}
复制代码

2.3.2是针对x如果是一个promise对象

需要通过对PENDING,FULFILLED,REJECTED3个状态进行 如果x处于pending状态,那么在成功或者失败前,我们需要保存这个状态, 如果x处于fulfilled或者rejected状态我们只需要重新resove或者reject出去即可

2.3.3如果x一个对象或者函数 如果x不是一个对象或者函数,那么为普通值我们直接resolve出去

function resolvePromise(promise2, x, resolve, reject) {
if (x === promise2) {
return reject(new TypeError('Chaining cycle detected for promise #<MyPromise>'))
}
if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
} else {
resolve(x)
}
}
复制代码

初步的模型已经完成,此时关于前面then的穿透问题可以大致看出来已经解决

let p1 = new MyPromise((resolve, reject) => {
resolve('成功')
})
.then()
.then()
.then(
(value) => {
console.log(value) // 成功
},
(err) => {
console.log(err)
}
)
复制代码

接下来继续按照规范来,从2.3.1处开始处理对象或者函数的情况 2.3.1说假设x有一个then属性, 2.3.2:在读取属性的时候如果抛出异常则reject出去(Object.defineProerty(x,'then',{get(){throw new Error('err')}})) 则代码如下

function resolvePromise(promise2, x, resolve, reject) {
if (x === promise2) {
return reject(new TypeError('Chaining cycle detected for promise #<MyPromise>'))
}
if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
let then = x.then
try {
} catch (e) {
reject(e)
}
} else {
resolve(x)
}
}
复制代码

2.3.3开始对then进行处理,如果then是一个函数则认为x是一个promise对象,然后调用它(
If ​​​then​​​ is a function, call it with ​​x​​​ as ​​this​​​, first argument ​​resolvePromise​​​, and second argument ​​rejectPromise​​, where:)并且附带2个参数(函数)处理resolve(参数y)和reject(参数r)这里指的是2.3.3.3.1和2.3.3.3.2 2.3.3.3的意思是如果r和y被多次调用或者对某个函数重复调用,第一次调用优先,其他忽略,因此我们指定一个全局变量called来控制调用

【面试题】吃透Promise?先实现一个再说(包含所有方法)

 2.3.3.4的意思是如果调用后抛出异常,这个异常可能在调用y或者r函数后造成也可能是在之前就抛出的 因此也需要使用called来控制是否抛出异常 2.3.4以及后面的指的是如果then不是一个函数或者对象,那么确定fulfilled状态resolve出去即可 至此完整resolvePromise函数封装如下

【面试题】吃透Promise?先实现一个再说(包含所有方法)

function resolvePromise(promise2, x, resolve, reject) {
if (x === promise2) {
return reject(new TypeError('Chaining cycle detected for promise #<MyPromise>'))
}
let called = false
if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
let then = x.then
try {
if (typeof then === 'function') {
then.call(
x,
(y) => {
if (called) return
called = true
resolvePromise(promise2, y, resolve, reject)
},
(r) => {
if (called) return
called = true
reject(r)
}
)
} else {
resolve(x)
}
} catch (e) {
if (called) return
called = true
reject(e)
}
} else {
resolve(x)
}
}

复制代码

对了突然想起来对于executor函数中的resolve封装中,如果resolve里面是多层嵌套的promsie对象的话例如这样

【面试题】吃透Promise?先实现一个再说(包含所有方法)

let p1 = new Promise((resolve, reject) => {
resolve(
new Promise((resolve, reject) => {
resolve(11)
})
)
}).then((value) => {
console.log(value)
})
复制代码

我们需要对resolve的参数做一个提前的判断处理,如果是promsie的实例我们应该调用then方法 添加起来非常简单,代码如下

const resolve = (value) => {
if (value instanceof MyPromise) {
value.then(resolve, reject)
return
}
if (this.status === PENDING) {
this.status = FULFILLED
this.value = value
this.onFulfilledCallbacks.forEach((fn) => fn())
}
}
复制代码

至此完整的promise实现代码如下

const [PENDING, FULFILLED, REJECTED] = ['PENDING', 'FULFILLED', 'REJECTED']
class MyPromise {
constructor(executor) {
this.status = PENDING
this.value = undefined
this.reason = undefined
this.onFulfilledCallbacks = []
this.onRejectedCallbacks = []
const resolve = (value) => {
if (value instanceof MyPromise) {
value.then(resolve, reject)
return
}
if (this.status === PENDING) {
this.status = FULFILLED
this.value = value
this.onFulfilledCallbacks.forEach((fn) => fn())
}
}
const reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED
this.reason = reason
this.onRejectedCallbacks.forEach((fn) => fn())
}
}
try {
executor(resolve, reject)
} catch (e) {
reject(e)
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (value) => value
onRejected =
typeof onRejected === 'function'
? onRejected
: (reason) => {
throw reason
}
let promise2 = new MyPromise((resolve, reject) => {
if (this.status === FULFILLED) {
setTimeout(() => {
try {
let x = onFulfilled(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
}

if (this.status === REJECTED) {
setTimeout(() => {
try {
let x = onRejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
}
if (this.status === PENDING) {
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
})
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
})
}
})
return promise2
}
}
function resolvePromise(promise2, x, resolve, reject) {
if (x === promise2) {
return reject(new TypeError('Chaining cycle detected for promise #<MyPromise>'))
}
let called = false
if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
let then = x.then
try {
if (typeof then === 'function') {
then.call(
x,
(y) => {
if (called) return
called = true
resolvePromise(promise2, y, resolve, reject)
},
(r) => {
if (called) return
called = true
reject(r)
}
)
} else {
resolve(x)
}
} catch (e) {
if (called) return
called = true
reject(e)
}
} else {
resolve(x)
}
}

复制代码

3.Promise的静态方法以及原型方法

console.log(Reflect.ownKeys(Promise))
console.log(Promise.prototype)
复制代码

通过上述代码可查看Promise的静态方法以及原型上的方法

【面试题】吃透Promise?先实现一个再说(包含所有方法)

话不多说直接动手开干,都走到这一步了,麻烦亲坚持一下

【面试题】吃透Promise?先实现一个再说(包含所有方法)

3.1-Promise.resolve/Promise.reject

这2个方法比较简单,直接调用我们之前封装好的promsie里面的resolve和reject函数即可

static resolve(value) {
return new MyPromise((resolve, reject) {
resolve(value)
})
}

static reject(reason) {
return new MyPromise((resolve, reject) => {
reject(reason)
})
}
复制代码

3.2-Promise.all

`不了解的同学点击参考​​MDN-Promise.all​​

如果参数不是一个可迭代的对象,那么会报错,并且返回的值需要拿promise包裹出去,而且顺序不能变需要注意 代码如下

static all(promiseArr) {
let resArr = [],
idx = 0
if (!isIterable(promiseArr)) {
let type = typeof promiseArr
throw TypeError(`${type} is not a iterable (cannot read property Symbol(Symbol.iterator))
at Function.all (<anonymous>)`)
}
return new Promise((resolve, reject) => {
promiseArr.map((promise, index) => {
if (isPromise(promise)) {
promise.then((res) => {
formatArr(res, index, resolve)
}, reject)
} else {
formatArr(promise, index, resolve)
}
})
})

function formatArr(value, index, resolve) {
resArr[index] = value
// if(resArr.length ===promiseArr.length) 在某些时刻不正确,比如数组最后一项先执行完 数组就为[empty,empty,value]
if (++idx === promiseArr.length) {
resolve(resArr)
}
}
}
//工具函数封装
function isIterable(value) {
return value !== null && value !== undefined && typeof value[Symbol.iterator] === 'function'
}

function isPromise(x) {
if ((typeof x === 'object' && x !== null) || typeof x == 'function') {
let then = x.then
return typeof then === 'function'
}
return false
}


复制代码

3.3-Promise.allSettled

与promise.all的实现思想大差不多,只不过返回的数组里面包含的表明状态的对象,而且不管是成功或者失败都收集起来

static allSettled(promiseArr) {
let resArr = [],
idx = 0
if (!isIterable(promiseArr)) {
let type = typeof promiseArr
throw TypeError(`${type} is not a iterable (cannot read property Symbol(Symbol.iterator))
at Function.all (<anonymous>)`)
}

return new MyPromise((resolve, reject) => {
if (promiseArr.length === 0) {
resolve([])
}
promiseArr.forEach((promise, index) => {
if (isPromise(promise)) {
promise.then(
(value) => {
formatArr('fulfilled', value, index, resolve)
},
(err) => {
formatArr('rejected', err, index, resolve)
}
)
} else {
formatArr('fulfilled', promise, index, resolve)
}
})
})

function formatArr(status, value, index, resolve) {
switch (status) {
case 'fulfilled':
resArr[index] = {
status,
value
}
break
case 'rejected':
resArr[index] = {
status,
reason: value
}
break
default:
break
}
if (++idx === promiseArr.length) {
resolve(resArr)
}
}
}
复制代码

3.4-Promise.race

这个方法也比较简单,race是赛跑的意思,当某一项确定状态后,直接包装成promise出去就好

static race(promiseArr) {
if (!isIterable(promiseArr)) {
let type = typeof promiseArr
throw TypeError(`${type} is not a iterable (cannot read property Symbol(Symbol.iterator))
at Function.all (<anonymous>)`)
}
return new Promise((resolve, reject) => {
promiseArr.forEach((promise, index) => {
if (isPromise(promise)) {
promise.then(resolve, reject)
} else {
resolve(promise)
}
})
})ise.then
}
复制代码

3.4-Promise.prototype.finally

这个方法其实要考虑的因素蛮多,在此我列举出来 finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作

finally本质上是then方法的特例。

  1. finally 无论外面的promise状态成功还是失败 都要走 并且回调函数不带参数
  2. 正常走finallu之后then 或者 catch
  3. 如果finally 内部有promise 并且有延时处理,整个finall会等待执行
  4. 如果2个都是成功,取外面结果
  5. 如果外面是成功 里面是失败,取里面失败的结果
  6. 如果外面是失败 里面是成功 取外面失败的结果
  7. 如果外面和里面都是失败,取里面失败的结果
  8. 如果外面成功,里面成功,取外面成功的结果

我们首先要把上一次promise的值保存下来 这样只有当里面是失败的情况下,才取finally内部失败的值,其余取上一个promise的值

finally(callbacks) {
return this.then(
(value) => {
return MyPromise.resolve(callbacks()).then(() => value)
},
(err) => {
return MyPromise.resolve(callbacks()).then(() => {
throw err
})
}
)
}
复制代码

3.5-Promise.prototype.catch

这个方法比较简单,相当于调用then的第二个回调而已

catch(callback) {
return this.then(null.callback)
}
复制代码

完整带代码​​ 链接​​ 大哥看完麻烦给个star,创作不易,谢谢!!!!!!!!!

参考链接2:​​promisesaplus.com/​​ (promiseA+规范)

末尾

本文只是基于romiseA+规范进行实现,掌握可胜任工作以及,中等偏下的面试题,但是与v8引擎下的promise规范还是有很多差异 掌握本文可参考月夕大佬的promise文章

【面试题】吃透Promise?先实现一个再说(包含所有方法)

 给大家推荐一个实用面试题库

1、前端面试题库 (面试必备)            推荐:★★★★★

地址:前端面试题库