携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情
前言
最近在刷牛客面经,发现大的公司对于JS基本功的要求真是越来越高了,面试的时候真的有公司会让你手写Promise。。。(害怕.jpeg)。日常学习过程中倒没怎么认真学习过源码,只是理解原理,真要实战面试时候手撕代码的能力确实还是很重要的,这也是面试官筛选面试者的标准。这不,最近疯狂逛面试经验帖的时候看到的一些👇
手写Promise是大厂手撕代码中经常会被问到的,如果面试前准备不充分,很可能达不到面试官的要求。
当然,Promise相关的面试题还有一种常见的考法是阅读代码看输出,涉及到的关联知识点有Event Loop、宏任务/微任务、async/await等。异步回调确实是JS中的一座大山,不妨看一下下面这道题看看能不能说出输出结果:
const async1 = async () => {
console.log('async1');
setTimeout(() => {
console.log('timer1')
}, 2000)
await new Promise(resolve => {
console.log('promise1')
})
console.log('async1 end')
return 'async1 success'
}
console.log('script start');
async1().then(res => console.log(res));
console.log('script end');
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.catch(4)
.then(res => console.log(res))
setTimeout(() => {
console.log('timer2')
}, 1000)
参考答案
'script start'
'async1'
'promise1'
'script end'
1
'timer2'
'timer1'
怎么样,是不是很“恶心”呢?其实这种题掌握了技巧还是比较容易的,只是“坑”确实很多,由于异步输出的问题不是本文的重点,如果你对自己这部分的知识点没有自信的话,这里推荐一个掘金大佬@霖呆呆的文章【建议星星】要就来45道Promise面试题一次爽到底(1.1w字用心整理),还是个小姐姐呦~我觉得小姐姐文章整理的真的很不错了,至少我刷完两遍之后,仿佛通透了一样,然后面经上出现类似的异步输出的题目基本都是秒杀哈哈哈哈,这里再次感谢呆姐 ~
Promise的基本概念和用法我就不多说了,能刷进来的掘金er们都应该是有基本功的,不过文章中为了便于理解还是会适当穿插一些讲解。下面让我们愉快的一起手撕代码吧~
Promise类核心逻辑实现
初学Promise异步回调的时候,我一直都有个疑问———为什么new一个Promise时,传入的参数是一个函数,并且这个函数里的逻辑会立即执行呢?new Promise里传入的这个函数为啥不放到微任务队列里去呢?
比如new Promise(() => {console.log('Hi Promise')})会立即输出Hi Promise
其实Promise就是一个类,在执行这个类的时候需要传递一个执行器executor进去,该执行器会立即执行。
由此Promise的类应该先是这样的
class MyPromise {
constructor(executor) {
executor()
}
}
同时我们知道Promise有三种状态分别为:等待Pending 、成功Fulfilled、失败Rejected,并且Promise传入的参数接收两个函数,可以通过resolve和reject这两个函数来改变Promise的状态,因此我们完善一下Promise的结构:
const PENDING = 'pending' //等待
const FULFILLED = 'fulfilled' //成功
const REJECTED = 'rejected' //失败
class MyPromise {
constructor(executor) {
executor(this.resolve, this.reject)
}
status = PENDING //初始时Promise的状态是pending
resolve = () => {
//resolve函数将状态变为fulfilled
this.status = FULFILLED
}
reject = () => {
//reject函数将状态变为rejected
this.status = REJECTED
}
}
到这里有两个问题要说一下:
1.resolve和reject为什么是箭头函数?因为在JS中普通函数的this指向window,箭头函数的this指向外部作用域,这里用箭头函数就是为了在new Promise传入的函数中调用resolve和reject这两个方法时的this指向Promise自身,不信的话在nodejs环境下就会报错👇
2.Promise的状态一旦改变就不能再改变,即只能从pending->fulfilled或者pending->rejected,但是现在我们的Promise是可以被多次改变的
可以看到执行resolve()和rejected()最终会输出reject,这显然不是合理的结果,因此我们加一层判断
if (this.status !== PENDING) return
const PENDING = 'pending' //等待
const FULFILLED = 'fulfilled' //成功
const REJECTED = 'rejected' //失败
class MyPromise {
constructor(executor) {
executor(this.resolve, this.reject)
}
status = PENDING //初始时Promise的状态是pending
resolve = () => {
//如果状态不是等待 阻止程序向下执行
if (this.status !== PENDING) return
//resolve函数将状态变为fulfilled
this.status = FULFILLED
}
reject = () => {
//如果状态不是等待 阻止程序向下执行
if (this.status !== PENDING) return
//reject函数将状态变为rejected
this.status = REJECTED
}
}
Promise还有个then方法,该方法内部做的事情就是判断状态 如果状态是成功则调用成功的回调函数,如果状态是失败则调用失败的回调函数。同时then成功回调有一个参数,表示成功之后的值,then失败回调也有一个参数,表示失败后的原因。
const PENDING = 'pending' //等待
const FULFILLED = 'fulfilled' //成功
const REJECTED = 'rejected' //失败
class MyPromise {
constructor(executor) {
executor(this.resolve, this.reject)
}
status = PENDING //初始时Promise的状态是pending
value = undefined //成功后的值
reason = undefined //失败后的原因
resolve = value => {
//如果状态不是等待 阻止程序向下执行
if (this.status !== PENDING) return
//resolve函数将状态变为fulfilled
this.status = FULFILLED
//保存成功之后的值
this.value = value
}
reject = reason => {
//如果状态不是等待 阻止程序向下执行
if (this.status !== PENDING) return
//reject函数将状态变为rejected
this.status = REJECTED
//保存失败的原因
this.reason = reason
}
then(successCallback, failCallback) {
if (this.status === FULFILLED) {
successCallback(this.value)
} else if (this.status === REJECTED) {
failCallback(this.reason)
}
}
}
我们再来验证下我们的结果
const promise = new MyPromise((resolve, reject) => {
resolve(123)
})
promise.then(value => {
console.log('fulfilled:', value)
}, reason => {
console.log('rejected:', reason)
})
查看控制台,此时只输出fulfilled: 123,符合预期。至此我们实现了Promise类的核心逻辑,但我们目前还没有考虑异步情况,比如加入setTimeout的情况:
const promise = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('success')
}, 2000)
})
promise.then(value => {
console.log('fulfilled:', value)
}, reason => {
console.log('rejected:', reason)
})
2s后promise才从pending变成fulfilled状态,而根据浏览器事件循环的原理,promise.then方法会在定时器之前先执行,而此时promise还是pending状态,不会进入resolve和reject的逻辑,因此控制台没有任何输出。下面我们就来处理一下异步的情况。
Promise中加入异步逻辑
上面我们已经分析过了刚刚那段代码的执行过程,问题就出在promise还是pending状态时,我们什么都没做,那么当2s后promise变成了fulfilled,then方法已经执行完了,promise成功的回调就没有机会执行了。
那该怎么办呢?很简单,我们的目标就是要让成功的回调延迟到resolve之后再执行,那么就在我们的Promise类中加入对pending状态的逻辑,并且把成功的回调successCallback和失败的回调failCallback用变量缓存起来,等到resolve或者reject方法执行时,判断是否有回调,如果有就执行回调,代码如下:
const PENDING = 'pending' //等待
const FULFILLED = 'fulfilled' //成功
const REJECTED = 'rejected' //失败
class MyPromise {
constructor(executor) {
executor(this.resolve, this.reject)
}
status = PENDING //初始时Promise的状态是pending
value = undefined //成功后的值
reason = undefined //失败后的原因
//成功回调
successCallback = null
//失败回调
failCallback = null
resolve = value => {
//如果状态不是等待 阻止程序向下执行
if (this.status !== PENDING) return
//resolve函数将状态变为fulfilled
this.status = FULFILLED
//保存成功之后的值
this.value = value
//判断成功回调是否存在 如果存在 调用
this.successCallback && this.successCallback(this.value)
}
reject = reason => {
//如果状态不是等待 阻止程序向下执行
if (this.status !== PENDING) return
//reject函数将状态变为rejected
this.status = REJECTED
//保存失败的原因
this.reason = reason
//判断失败回调是否存在 如果存在 调用
this.failCallback && this.failCallback(this.reason)
}
then(successCallback, failCallback) {
if (this.status === FULFILLED) {
successCallback(this.value)
} else if (this.status === REJECTED) {
failCallback(this.reason)
} else {
//等待
//将成功回调和失败回调缓存起来,等待合适时机调用
this.successCallback = successCallback
this.failCallback = failCallback
}
}
}
实现then方法多次调用添加多个处理函数
我们知道,同一个promise的then方法是可以被多次调用的,比如
promise.then(value=>{
console.log('a')
})
promise.then(value=>{
console.log('b')
})
promise.then(value=>{
console.log('c')
})
但是当遇到异步情况,多个then又会出现问题,比如:
const promise = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve()
}, 2000)
})
promise.then(value => {
console.log('a')
})
promise.then(value => {
console.log('b')
})
promise.then(value => {
console.log('c')
})
当2s后promise状态变为fulfilled时,所有的then里面成功的回调都应该依次执行,因此我们需要将成功的回调和失败的回调定义成数组:
//成功回调
successCallback = []
//失败回调
failCallback = []
并且在then方法中 处理遇到pending状态时:
//等待
//将成功回调和失败回调缓存起来,等待合适时机调用
this.successCallback.push(successCallback)
this.failCallback.push(failCallback)
同时别忘了改写我们的resolve方法和reject方法
resolve = value => {
//如果状态不是等待 阻止程序向下执行
if (this.status !== PENDING) return
//resolve函数将状态变为fulfilled
this.status = FULFILLED
//保存成功之后的值
this.value = value
//判断成功回调是否存在 如果存在 调用
// this.successCallback && this.successCallback(this.value)
while (this.successCallback.length) {
this.successCallback.shift()(this.value)
}
}
reject = reason => {
//如果状态不是等待 阻止程序向下执行
if (this.status !== PENDING) return
//reject函数将状态变为rejected
this.status = REJECTED
//保存失败的原因
this.reason = reason
//判断失败回调是否存在 如果存在 调用
// this.failCallback && this.failCallback(this.reason)
while (this.failCallback.length) {
this.failCallback.shift()(this.reason)
}
}
此时完整代码如下:
const PENDING = 'pending' //等待
const FULFILLED = 'fulfilled' //成功
const REJECTED = 'rejected' //失败
class MyPromise {
constructor(executor) {
executor(this.resolve, this.reject)
}
status = PENDING //初始时Promise的状态是pending
value = undefined //成功后的值
reason = undefined //失败后的原因
//成功回调
successCallback = []
//失败回调
failCallback = []
resolve = value => {
//如果状态不是等待 阻止程序向下执行
if (this.status !== PENDING) return
//resolve函数将状态变为fulfilled
this.status = FULFILLED
//保存成功之后的值
this.value = value
//判断成功回调是否存在 如果存在 调用
// this.successCallback && this.successCallback(this.value)
while (this.successCallback.length) {
this.successCallback.shift()(this.value)
}
}
reject = reason => {
//如果状态不是等待 阻止程序向下执行
if (this.status !== PENDING) return
//reject函数将状态变为rejected
this.status = REJECTED
//保存失败的原因
this.reason = reason
//判断失败回调是否存在 如果存在 调用
// this.failCallback && this.failCallback(this.reason)
while (this.failCallback.length) {
this.failCallback.shift()(this.reason)
}
}
then(successCallback, failCallback) {
if (this.status === FULFILLED) {
successCallback(this.value)
} else if (this.status === REJECTED) {
failCallback(this.reason)
} else {
//等待
//将成功回调和失败回调缓存起来,等待合适时机调用
this.successCallback.push(successCallback)
this.failCallback.push(failCallback)
}
}
}
测试一下:
const promise = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve()
}, 2000)
})
promise.then(value => {
console.log('a')
})
promise.then(value => {
console.log('b')
})
promise.then(value => {
console.log('c')
})
依次输出a b c ,结果正确,多个then函数并且存在异步的情况也被我们解决了。
实现then方法的链式调用
Promise的精髓在于链式调用,然而我们写了这么多还没有实现链式调用,下面我们接着肝!!
分析一下不难发现,要实现Promise的链式调用,我们必须要在then方法中返回一个Promise对象,因为只有Promise对象才能调用then方法。
因此我们修改一下then方法:
then(successCallback, failCallback) {
let promise2 = new MyPromise(() => {
if (this.status === FULFILLED) {
successCallback(this.value)
} else if (this.status === REJECTED) {
failCallback(this.reason)
} else {
//等待
//将成功回调和失败回调缓存起来,等待合适时机调用
this.successCallback.push(successCallback)
this.failCallback.push(failCallback)
}
})
return promise2
}
我们new了一个promise2,把原来的逻辑写到了promise2的执行器中,这样就能在then方法中返回一个Promise对象了。然后接下来我们又遇到了一个问题:如何将上一个Promise的then方法中的返回值传递给下一个then呢?
这里我们就可以利用promise2的闭包,调用resolve()方法将上一个then的返回值保存到promise2的value中
then(successCallback, failCallback) {
let promise2 = new MyPromise((resolve, reject) => {
if (this.status === FULFILLED) {
let x = successCallback(this.value)
resolve(x) //利用promise2的闭包保存了这个x
} else if (this.status === REJECTED) {
failCallback(this.reason)
} else {
//等待
//将成功回调和失败回调缓存起来,等待合适时机调用
this.successCallback.push(successCallback)
this.failCallback.push(failCallback)
}
})
return promise2
}
但是这样还没完,接下来我们要做的事情是:
- 判断x的值是普通值还是promise对象
- 如果是普通值 直接resolve
- 如果是promise对象 查看promise对象返回的结果
- 再根据promise对象返回的结果决定调用resolve还是reject
下面我们将这个逻辑封装成一个函数
function resolvePromise(x, resolve, reject) {
if (x instanceof MyPromise) {
x.then(resolve, reject)
} else {
resolve(x)
}
}
修改一下then函数
then(successCallback, failCallback) {
let promise2 = new MyPromise((resolve, reject) => {
if (this.status === FULFILLED) {
let x = successCallback(this.value)
resolvePromise(x, resolve, reject)
} else if (this.status === REJECTED) {
failCallback(this.reason)
} else {
//等待
//将成功回调和失败回调缓存起来,等待合适时机调用
this.successCallback.push(successCallback)
this.failCallback.push(failCallback)
}
})
return promise2
}
下面我们来测试一下:
let promise = new MyPromise((resolve, reject) => {
resolve('成功')
})
function other() {
return new MyPromise((resolve, reject) => {
resolve('other')
})
}
promise.then(value => {
console.log(value)
return other()
}).then(value => {
console.log(value)
})
完整代码
const PENDING = 'pending' //等待
const FULFILLED = 'fulfilled' //成功
const REJECTED = 'rejected' //失败
class MyPromise {
constructor(executor) {
executor(this.resolve, this.reject)
}
status = PENDING //初始时Promise的状态是pending
value = undefined //成功后的值
reason = undefined //失败后的原因
//成功回调
successCallback = []
//失败回调
failCallback = []
resolve = value => {
//如果状态不是等待 阻止程序向下执行
if (this.status !== PENDING) return
//resolve函数将状态变为fulfilled
this.status = FULFILLED
//保存成功之后的值
this.value = value
//判断成功回调是否存在 如果存在 调用
// this.successCallback && this.successCallback(this.value)
while (this.successCallback.length) {
this.successCallback.shift()(this.value)
}
}
reject = reason => {
//如果状态不是等待 阻止程序向下执行
if (this.status !== PENDING) return
//reject函数将状态变为rejected
this.status = REJECTED
//保存失败的原因
this.reason = reason
//判断失败回调是否存在 如果存在 调用
// this.failCallback && this.failCallback(this.reason)
while (this.failCallback.length) {
this.failCallback.shift()(this.reason)
}
}
then(successCallback, failCallback) {
let promise2 = new MyPromise((resolve, reject) => {
if (this.status === FULFILLED) {
let x = successCallback(this.value)
resolvePromise(x, resolve, reject)
} else if (this.status === REJECTED) {
failCallback(this.reason)
} else {
//等待
//将成功回调和失败回调缓存起来,等待合适时机调用
this.successCallback.push(successCallback)
this.failCallback.push(failCallback)
}
})
return promise2
}
}
function resolvePromise(x, resolve, reject) {
if (x instanceof MyPromise) {
x.then(resolve, reject)
} else {
resolve(x)
}
}
补充说明
至此,一个微型的Promise就实现了,但是这个版本的Promise其实和真实的Promise相比较还有很多情况没有考虑,比如then方法链式调用识别Promise对象自返回问题、异常捕获catch、then方法的参数变为可选参数、then方法的透传问题、finally()、Promise.resolve()、Promise.reject()、Promise.race()、Promise.all()。。。。。。。这些问题由于作者精力有限,现在准备面试也是焦头烂额,明天再补充一篇手写Promise.all()吧。