前言
在很久之前就想了解一下如何手写 promise ,奈何一直身边事情焦虑不安,没有定下心来好好研究。最近下定决心好好看完,于是写一篇比较通俗的手写 promise ,这是一篇基础入门,只实现 promise 的核心功能代码。其他的交给后面的文章来完成。
使用方法
我们先思考一下,平时我们是怎么使用 promise 的?应该是像下面这种方式:
function handle(resolve,reject){
setTimeout(resolve(1),1000)//模拟异步操作
}
new promise(handle)
传入promise一个处理函数,然后这个处理函数接收两个参数resolve,reject。其中resolve用于处理成功的异步,reject用于处理失败的异步。resolve,reject都本质是一个函数形式,可以用于调用传入成功、失败参数。
基础实现
根据以上,我们可以写出如下简单的大框架形式代码(停一停,想想是不是这么回事):
function diyPromise(fn){
function resolve(value){
console.log('这里是我成功的函数执行传入的值:',value)
}
function reject(value){
console.log('这里是我失败的函数执行传入的值:',value)
}
fn(resolve,reject)
}
接下来,我们开始进一步看。
我们都知道promise有三种状态:pending,reject,resolve。
(注:这里的reject和resolve不要跟前面的函数搞混,虽然名字叫一样,但是这里是个字符串常量)
于是我们引入状态:
const PENDING = 'PENDING';//即将开始
const REJECT = 'REJECT';//失败
const RESOLVE = 'RESOLVE';//成功
同时我们还需要有一个内部变量保证我们成功和失败的值能够做记录,并且需要保证传进来执行的函数不会报错额外的东西,我们需要捕获错误。因此代码到了这里,整体变成了这样:
const PENDING = 'PENDING';//即将开始
const REJECT = 'REJECT';//失败
const RESOLVE = 'RESOLVE';//成功
function diyPromise(fn){
this.successValue;
this.errorValue;
this.status = PENDING;//默认pending
function resolve(value){
//有坑,注意!
if(this.status===PENDING){
this.status = RESOLVE;/变更状态
this.successValue = value;/储存成功的值
}
}
function reject(value){
if(this.status===PENDING){
this.staus = REJECT;
this.errorValue = value;
}
}
try{
fn(resolve,reject)
}
catch(e){
reject(e)
}
}
这里要格外小心的一件事情,this的指向。试想一下,我们的resolve和reject函数是在什么地方执行的?是在我们的异步函数内部最终被调用执行,那么也就意味着this的指向是window(JS严格模式下undefined)。所以我们需要用一个临时变量来进行this的保存。
then调用
到目前为止,我们promise内的代码基本上搞定了。接下来处理一下then的回调。我们先看看使用代码:
function handle(resolve,reject){
setTimeout(resolve(1),1000)//模拟异步操作
}
new promise(handle).then(function(res){
console.log(res)
return '我是第二次then的值,下一个地方会打印我'
},function(err){
console.log(err)
})(function(res2){
console.log(res2)
},function(err2){
console.log(err2)
})
可以发现一件事情:then是类似于 jquery 那样的一个链式调用,上一个函数的计算结果用作下一个函数的入参。那么就很好解决了,别忘了链式调用的核心就在于每次都返回this。凭感觉,上代码:
const PENDING = 'PENDING';//即将开始
const REJECT = 'REJECT';//失败
const RESOLVE = 'RESOLVE';//成功
function diyPromise(fn){
this.successValue;
this.errorValue;
this.status = PENDING;//默认pending
let _this = this;
function resolve(value){
if(_this.status===PENDING){
_this.status = RESOLVE;/变更状态
_this.successValue = value;/储存成功的值
}
}
function reject(value){
if(_this.status===PENDING){
_this.staus = REJECT;
_this.errorValue = value;
}
}
try{
fn(resolve,reject)
}
catch(e){
reject(e)
}
}
diyPromise.prototype.then = function (resolveFn,rejectFn){
//这里有坑
if(this.status === REJECT){
rejectFn()
}
if(this.status === RESOLVE){
resolveFn()
}
return this
}
以上代码看似没啥问题,但是我们漏掉了一个地方!
我们回顾一下 promise 的使用:
1.传入一个函数立刻在构造函数里面执行它。
2.由于是异步的,此刻异步函数里面还没调用 resolve / reject 告诉我们到底成功还是失败?
3.主线程继续运行,跑到了then函数里开始执行,发现状态既不属于 REJECT 也不属于 RESOLVE!它还在 PENDING 状态!然后直接返回了 this!
看完以上流程,我们会发现,then 简直是运行了个寂寞啊...
那我们如何来处理 pending 状态下的操作呢?这是一个困难的地方,我们在这里无法知道它成功还是失败了,只是一直在 pending。
那能不能我们先记录下来,等它成功或者失败了以后再去把这记录的函数给触发了?
成功和失败了又在哪里呢?往上看,不就正好在我们的 reject 和 resolve 里么?
这个时候先别急,再考虑一个问题,反反复复的 then,那说明 then 不止一次。我们可以考虑用一个数组来记录 then 接受到的函数。最后统一在结果确定的时候触发。是不是有点发布订阅的味道?代码如下:
const PENDING = 'PENDING';//即将开始
const REJECT = 'REJECT';//失败
const RESOLVE = 'RESOLVE';//成功
function diyPromise(fn){
this.successValue;
this.errorValue;
this.status = PENDING;//默认pending
this.successList = [];//待处理的成功队列
this.errorList = [];
let _this = this;
function resolve(value){
if(_this.status===PENDING){
_this.status = RESOLVE;//变更状态
_this.successValue = value;//储存成功的值
_this.successList.forEach(fn=>{
_this.successValue = fn(_this.successValue)//成功的函数传入成功值,然后计算出来的结果再覆盖记录的成功值,留着下次then用
})
}
}
function reject(value){
if(_this.status===PENDING){
_this.staus = REJECT;
_this.errorValue = value;
_this.errorList.forEach(fn=>{
_this.errorValue = fn(_this.errorValue)//失败的函数传入失败值,然后计算出来的结果再覆盖记录的成功值,留着下次then用
})
}
}
try{
fn(resolve,reject)
}
catch(e){
reject(e)
}
}
diyPromise.prototype.then = function (resolveFn,rejectFn){
if(this.status === REJECT){
rejectFn()
}
if(this.status === RESOLVE){
resolveFn()
}
if(this.status === PENDING){
resolveFn && this.successList.push(resolveFn)
rejectFn && this.errorList.push(rejectFn)
}
return this
}
最终实现
这里我们仍旧有不完美的地方,因为每次then返回的应该是一个新的promise,我们这里的then返回的还是它自身,因此需要再来一个改造!
const PENDING = 'PENDING';//即将开始
const REJECT = 'REJECT';//失败
const RESOLVE = 'RESOLVE';//成功
function diyPromise(fn){
this.successValue;
this.errorValue;
this.status = PENDING;//默认pending
this.successList = [];//待处理的成功队列
this.errorList = [];
let _this = this;
function resolve(value){
if(_this.status===PENDING){
_this.status = RESOLVE;//变更状态
_this.successValue = value;//储存成功的值
_this.successList.forEach(fn=>{
_this.successValue = fn(_this.successValue)//成功的函数传入成功值,然后计算出来的结果再覆盖记录的成功值,留着下次then用
})
}
}
function reject(value){
if(_this.status===PENDING){
_this.staus = REJECT;
_this.errorValue = value;
_this.errorList.forEach(fn=>{
_this.errorValue = fn(_this.errorValue)//失败的函数传入失败值,然后计算出来的结果再覆盖记录的成功值,留着下次then用
})
}
}
try{
fn(resolve,reject)
}
catch(e){
reject(e)
}
}
diyPromise.prototype.then = function (resolveFn,rejectFn){
if(this.status === REJECT){
rejectFn()
}
if(this.status === RESOLVE){
resolveFn()
}
if(this.status === PENDING){
resolveFn && this.successList.push(resolveFn)
rejectFn && this.errorList.push(rejectFn)
return this//这里因为这个promise还没处理完,先返回自己
}
return new diyPromise((res)=>{//每次都返回一个新的promise,并且在下一个事件循环中进行
setTimeout(()=>{
this.success = resolveFn(this.success)//传入的then内部成功函数
})
})
}
console.log(new diyPromise((res) => {
setTimeout(() => {
res(1)
}, 1000)
}).then(res => {
console.log(res)
return 9999
}).then(res => {
console.log(res)
return 9999
}))
至此,一个核心的 promise 原理代码实现完成。