在 JS 里面我们想要优雅进行异步操作,必须是要熟悉一个 ES6 里面非常非常重要的概念 Promise,它所具备的链式调用,很好的缓解了传统回调写法所带来的回调地狱,也是理解生成器,async/await 的基础。在最近几年前端的面试中对其的考察基本是不可避免的,本文的主要目的是记录一下我在实现一个 Promise 的一些过程和思考。

全部代码我已经上传 github

从最简单的例子开始

完全理解并实现Promise(收藏)_构造函数

这是一个 Promise 最简单的用法,代码创建了一个 Promise 对象,传入一个 executor 执行函数,在某个时刻它会按顺序执行它的参数 reslove 和 reject,然后 resolve 和 reject 的参数会作为 Promise 对象 then 的参数。了解了这些我们可以总结一下:


  • 我们会需要两个变量来分别存储 resolve 和 reject 的参数
  • 因为 resolve 和 reject 不会同时处理,所以我们需要一个状态变量来记录 Promise 对象的一个状态
  • then 方法需要传入两个方法, 在 Promise 对象改变状态的时候调用

下面就来简单实现一下吧

  • 构造函数

完全理解并实现Promise(收藏)_异步操作_02

  • then 方法

完全理解并实现Promise(收藏)_i++_03

  • 测试

完全理解并实现Promise(收藏)_递归_04

看起来现在好像之前那个简单例子的代码可以在手写的这个函数里面执行了,但是现在又有一个问题了,我们前面写的函数都是同步的逻辑那么异步问题该怎么解决呢

完全理解并实现Promise(收藏)_i++_05

这是为啥呢,因为 setTimeout 的关系 resolve 方法被放入了执行队列中的,而 then 的逻辑是同步的,那么在执行 then 方法的时候,this.status 的状态还是在 pending. 那么如何解决呢,其实很简单,我们可以在 then 方法内加入 status 为 pending 的逻辑,把传入的函数先存到一个数组中,将它放到 resolve 或者 reject 方法中执行就行了,那么我们可以稍微改造一下 like this

加入异步操作

  • 构造函数

完全理解并实现Promise(收藏)_i++_06

  • then

完全理解并实现Promise(收藏)_i++_07

  • 测试

完全理解并实现Promise(收藏)_i++_08

嘻嘻没有任何意外,完美的输出意料之中的答案,那么接下来就是最重要的链式调用以及值穿透的实现了,链式调用可以说是整个实现过程中最难理解的地方,但是说难其实如果你经常解除递归的思想以及 JS 基础比较扎实的情况下也是非常简单的,如果一开始不理解也没关系,小菜鸡在给出代码的同时也会完整的解释一遍运行的过程,相信聪明的你多看两遍一定就会懂得啦!

分析一下先:

如果需要链式调用那么关键的关键就是 xxx.then 这一部分是一个 Promise 对象对吧,那么很简单,为了保证实现这个功能,我们需要的是在调用 then 方法时一定会返回一个新的 Promise 对象,这是这条链不会断的关键,然后链的问题搞定之后我们需要处理传值的问题,根据 Promise 的规则


  • 如果 then 的返回值 x 是一个普通值,那么就会把这个结果作为参数,传递给下一个 then 的成功的回调中;
  • 如果 then 中抛出了异常,那么就会把这个异常作为参数,传递给下一个 then 的失败的回调中;
  • 如果 then 的返回值 x 是一个 promise,那么会等这个 promise 执行完,promise 如果成功,就走下一个 then 的成 - 功;如果失败,就走下一个 then 的失败;如果抛出异常,就走下一个 then 的失败;
  • 如果 then 的返回值 x 和 promise 是同一个引用对象,造成循环引用,则抛出异常,把异常传递给下一个 then 的失败的回调中;
  • 如果 then 的返回值 x 是一个 promise,且 x 同时调用 resolve 函数和 reject 函数,则第一次调用优先,其他所有调用被忽略;

总的来说就是下一个 then 前面那一部分是一个 Promise,然后它的 resolve 或者 reject 方法主要看它 return 的部分,我们要做的就是每次使用 then 返回一个 Promise 实例,然后处理其中的 return 的部分就行了。那么走着

链式调用

const PENDING = 'PENDING';const FULFILLED = 'FULFILLED';const REJECTED = 'REJECTED';
复制代码
  • 构造函数

完全理解并实现Promise(收藏)_数组_09

  • then 划重点

完全理解并实现Promise(收藏)_异步操作_10完全理解并实现Promise(收藏)_递归_11完全理解并实现Promise(收藏)_递归_12

  • getResolveValue (处理 return 部分的方法)

完全理解并实现Promise(收藏)_异步操作_13

  • 测试部分

完全理解并实现Promise(收藏)_i++_14

输出: 
lavie小师弟
hello JS
hello TS
复制代码

理解过程

结合前面的解释然后看完这段代码之后可能很多小伙伴会有一种似懂非懂的感觉,ok 那么我们就按照前面给出的测试代码(为了彻底理解我故意将测试代码写的比较绕,求各位别打我, 多递归警告⚠)走一遍相信大家看了小菜鸡写的这个过程会觉得,其实 Promsie 也不过如此,再次提醒可能会比较绕但是大家耐心看完一定会理解这几个函数的作用的这点非常重要

new PromiseAll((resolve,_)=>{
setTimeout(()=>{
resolve('lavie小师弟')
},0
)
}) //p1
.then((data)=>{ console.log(data)
return new PromiseAll((resolve,_)=>{
resolve(new PromiseAll((resolve,_)=>{
resolve('hello JS')
}))
})
} ) //p2
.then(data=>{ console.log(data)
return new PromiseAll((resolve,_)=>{
resolve('hello TS')
}) } ) //p3
.then(data=>console.log(data)) //p4
复制代码

  1. 首先这段代码创建了一个 PromiseAll 实例 p1 然后里面的有一个异步的函数加入到执行队列中,status 为 Pending
  2. 第一个 PromsieAll 实例 p1 调用了 then 方法创建了一个新实例 p2, 因为 p1 的状态为 Pending 所以将传入的函数放进 p1 的 onSuccessCallback 数组中等待 p1 的 solve 执行时再调用
  3. p2 调用 then 方法创建了 p3,然后同样的 p3 的函数保存在 p2 的 onSuccessCallback 数组中,同时 p3 调用 then 方法创建了 p4...

上述过程是在执行栈中执行的同步事件,所以会率先执行

  1. 执行完同步操作之后,那么回过头来执行 p1 里面的 resolve('lavie 小师弟') 那么 p1 的状态就被修改成 Success 了,value 也变成了 lavie 小师弟,然后调用传进去的
value=>{
try {
let result = onFulfilled(value) //此时会输出lavie小师弟
getResolveValue(promiseNext,result,resolve,reject) //这里的promsieNext为p2
} catch (error) {
reject(error)
}
}
复制代码

根据我们传进去的函数这里的 result 会等于下面这个嵌套的对象记为 PC1

//PC1
new PromiseAll((resolve,_)=>{
resolve(new PromiseAll((resolve,_)=>{
resolve('hello JS')
}))
})
复制代码

好的搞清楚 promsieNext 和 result 之后我们就去执行​​getResolveValue(promiseNext,result,resolve,reject)​​ 函数里面的逻辑

首先 then = PC1.then 用 call 方法将 this 绑定到 PC1 上面

所以第一次递归 this.value 为

new PromiseAll((resolve,_)=>{
resolve('hello JS')
})
复制代码

然后继续调用​​getResolveValue(promiseNext,next,resolve,reject)​​ 因为 next 是等于 this.value 的,所以此时 next 参数变成了 PC2

//PC2
new PromiseAll((resolve,_)=>{
resolve('hello JS')
})
复制代码

此时进行第二次递归 this.value 很明显变成了​​hello JS PC3​​因为此时 PC3 已经不是对象了所以直接调用​​resolve('hello JS')​

我们回过头来看,这个 resolve 方法其实是上面创建 p2 的时候传进去的,所以此时 p2 的状态就从 Pending 变成了 Success,this.value 也变成了 hello JS , 然后执行在创建 p3 是时候传到 p2 的 onSuccessCallback 数组里面的方法此时输出 hello JS 然后继续执行 p3 的递归..... 和前面大同小异我就不继续说了,,,这个递归啊讲起来就是这么绕,但是你只要一步步跟的话,还是非常简单的????,其实这步理解起来非常难受,但是没关系坚持自己给自己说几遍那么你一定会理解的!

穿透

这点就很简单啦

类似

new PromiseAll((resolve,_)=>{
resolve('hello JS')
}).then.().then().then(data=>console.log(data))
复制代码

这点在上面的 then 方法中其实已经实现了

onFulfilled = typeof onFulfilled === 'function'  ? onFulfilled : data => data
onRejected = typeof onRejected === 'function' ? onRejected : error => { throw error }
复制代码

如果没有穿函数进来的话直接把值传递下去就可以啦

到这里其实整个 Promsie 都已经完成了,但是我没有完全按照 Promise/A+ 规范来写,有兴趣的小伙伴可以自己去完善????

接下来简单记录一下 Promise 中其他的方法的一下实现吧, 大家细品

其他方法

Promise.prototype.catch


PromiseAll.prototype.catch = function(err){
return this.then(null,err)
}
new PromiseAll((resolve,reject)=>{
reject('错误')
}).catch(data=>console.log(data))
复制代码

Promise.resolve && Promise.reject

PromiseAll.resolve = function(value){    return new PromiseAll((resolve,_)=>{        resolve(value)    })}PromiseAll.reject= function(reason){    return new PromiseAll((_,reject)=>{        reject(reason)    })}
复制代码

Promise.all

PromiseAll.resolve = function(value){
return new PromiseAll((resolve,_)=>{
resolve(value)
})
}
PromiseAll.reject= function(reason){
return new PromiseAll((_,reject)=>{
reject(reason)
})
}

复制代码
PromiseAll.all = function(arr){
if(!Array.isArray(arr)){
throw new TypeError('请传入一个数组')
}
return new PromiseAll((resolve,reject)=>{
try {
let resultArr = []
const length = arr.length
for(let i = 0;i<length;i++){
arr[i].then(data=>{
resultArr.push(data)
if(resultArr.length === length){
resolve(resultArr)
}
},reject)
}
} catch (error) {
reject(error)
}
})
}

const p1 = new PromiseAll((resolve,_)=>{
setTimeout(()=>{
resolve('cjzz')
},1000)
})

const p2 = new PromiseAll((resolve,_)=>{
resolve('cjz')
})
PromiseAll.all([p1,p2]).then(data=>console.log(data))// ['cjz','cjzz']
复制代码

Promise.race

PromiseAll.race = function(arr){
if(!Array.isArray(arr)){
throw new TypeError('请传入一个数组')
}
return new PromiseAll((resolve,reject)=>{
try {
const length = arr.length
for(let i = 0;i<length;i++){
arr[i].then(resolve,reject)
}
} catch (error) {
reject(error)
}
})
}
PromiseAll.race([p1,p2]).then(data=>console.log(data,'race'))
复制代码


完全理解并实现Promise(收藏)_i++_15