多动手系列:手写Promise

335 阅读9分钟

最近查看Promise的实现这种文章,发现网上的教程都经不起推敲,一到then的链式调用云里雾里,所以我决定自己手写一下,再会过头发现 网上的将的Promise案例是错误的逻辑,所以大家看着看着就不懂了

但你认真看这篇手写Promise,你就会明白的Promise怎么实现的then链式调用的。

这里我不会进行对比的,大家看我这篇应该是能顺懂的,然后你再看其他的Promise教程,你就会发现他们的问题啦。

OIP-C[1].jpg

使用Promise

先来看看Promise 场景的两种使用情况:

/* 模拟一个简单的异步行为 */
function myAsyncFunction() {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve('123'), 1000);
    });
}
// 用法1:
myAsyncFunction().then(res => {
     console.log(res) // after 1000ms : 输出 123
}, (err) => {})

// 用法2:
myAsyncFunction().then(res => {
     console.log('res:', res) // after 1000ms: 输出 123
     return res + '_123'
}, (err) => {}).then(res => {
     console.log('res:', res) // res: 123_123
     return res + '_123'
}, (err) => {}).then(res => {
     console.log('res:', res) // res: 123_123_123
}, (err) => {})

// 用法3:
let p = myAsyncFunction()
p.then(res => {
     console.log(res) // 输出 res:123
     return 456
}, (err) => {})
p.then(res => {
     console.log(res) // 输出 res:123
}, (err) => {})

用法1是正常的使用,用法2叫链式调用,用法3是对同一个实例进行多次调用

看以上Prmise的使用我们大致有个框架了,下面来搭建一下基本框架。

简单版Promise

1. 搭建Promise框架

Promise 是一个类,它接收一个函数作为参数,会传递給该函数两个默认参数,来通知执行then方法去执行成功或者失败的函数。

// new Promise((resolve, reject) => { resolve('123') });
class MyPromise {
    // executor 为用户传递进来的函数
    constructor(executor) {
        executor(this.resolve(), this.reject())
    }
    
    resolve(data) {}
    reject(reason) {}
    then(onFulfilled, onRejected) {}
}

2. 设置Promise状态

从Promise的官方规范可知Promise有三种状态:pending、fulfilled、rejected

resolve(data)函数代表成功,会将状态为fulfilled
reject(value)函数代表失败,会将状态为rejected

另外,两个的函数执行会触发then函数的执行,其中的datavalue会传递给 then(resolveFunc, rejectFunc) 这两个回调函数当参数。

将上面的思路应用在上一步的框架上:

class MyPromise {
  static pending = "pending";
  static fulfilled = "fulfilled";
  static rejected = "rejected";
  
  constructor(executor) {
      this.status = MyPromise.pending; // 初始化状态为 pending
      this.data = undefined; // 操作成功时 存放成功数据
      this.reason = undefined; // 操作失败时 存放失败的原因
      
      this.resolveFunc = () => {}; // 操作成功时 存放成功的回调函数
      this.rejectFunc = () => {}; // 操作失败时 存放失败的回调函数
      
      // 应为resolve函数是在executor中调用 ,也就是默认resolve函数的this指向的是executor函数
      // 所以要修改resolve的this指向为当前的MyPromise
      executor(this.resolve.bind(this), this.reject.bind(this))
  }
  
  resolve(data) {
      this.status = MyPromise.fulfilled; // 更改状态
      this.data = data; // 存放成功的数据
      
      // 执行操作成功时的回调函数
      this.resolveFunc(this.data); 
  }
  
  reject(reason) {
    this.status = MyPromise.rejected; // 更改状态
    this.reason = reason; // 存放失败的原因

    // 执行操作失败时的回调函数
    this.rejectFunc(this.reason);
  }
  
  then(onFulfilledFunc, onRejectedFunc) {
      this.resolveFunc = onFulfilledFunc; // 存放操作成功时的回调函数
      this.rejectFunc = onRejectedFunc; // 存放操作失败时的回调函数
  }
}

这时MyPromis已经可以简单使用起来了,测试一下

function myAsyncFunction(isFulfilled = true) {
  // isFulfilled 控制成功、失败
  return new MyPromise((resolve, reject) => {
    setTimeout(() => {
      if (isFulfilled) {
        resolve(123);
      } else {
        reject("我是失败的原因");
      }
    }, 1000);
  });
}

myAsyncFunction().then(res => {
    console.log(res) // after 1000ms : 输出 123
}, err => {})

myAsyncFunction(false).then(res => {}, err => {
    console.log(err) // after 1000ms : 输出 我是失败的原因
})

3. 创建then链式调用

从上面的输出结果来看,常规使用是没什么问题的。接下来就是像下面这样用的链式调用我们是不支持的。按照下面的情况我们进一步完善。

// 用法2:
myAsyncFunction().then(res => {
     console.log('res:', res)
     return res + '_123'
}, (err) => {}).then(res => {
     console.log('res:', res)
     return res + '_123'
}, (err) => {}).then(res => {
     console.log('res:', res)
}, (err) => {})

在我想既然能链式调用,我起初想的是在then中返回当前的this,就能继续往下调用了吧,尝试一下 先在 then() 中 添加 return this

// 用法2:
myAsyncFunction().then(res => {
     console.log('res:', res) // after 1000ms: 输出 123
     return res + '_123'
}, (err) => {}).then(res => {
     console.log('res:', res) // 没有执行
     return res + '_123'
}, (err) => {}).then(res => {
     console.log('res:', res) // 没有执行
}, (err) => {})

在运行过程中是没有报错,但从第二个then开始就没有执行传递的回调函数,因为这两个函数是通过resolve()reject()执行的,但到第二个then没有去执行resolve()reject()。也就不会执行then传递的回调函数。
所以返回this这条路不行,还可以返回一个新的Promise,这样就可以继续调用then了。

下面将过程抽象一下:

class MyPromise{
    ...
    resolve(data) {
      this.status = MyPromise.fulfilled; // 更改状态
      this.data = data; // 存放成功的数据
      
      // 执行操作成功时的回调函数
      this.resolveFunc(this.data); 
    }
    then(onFulfilledFunc, onRejectedFunc){
       //  this.resolveFunc = onFulfilledFunc; // 存放操作成功时的回调函数
       //  this.rejectFunc = onRejectedFunc; // 存放操作失败时的回调函数
       let that = this

       // 注意:下面的this都是指向的是调用者
       // 也就是调用者成功才会促使p成功
        let p = new MyPromise( (resolve, reject) => {
            that.resolveFunc = () => {
                let data = onFulfilledFunc(this.data);
                resolve(data);  // 将值传递下去
            }
            that.rejectFunc = () => {
                onRejectedFunc(this.reason);
                // 失败就不用继续传递了
            }
        })
        return p // 返回新Promise实例
    }
}
// 使用
let P1 = new MyPromise(...);
P1.then(res => res + '_123') // 这个then的调用者是P1
.then(res => res + '_123')  // 这个then的调用者是P1 then产生的P2
.then(res => res + '_123') // 这个then的调用者是P2 then产生的P3

总结:P1的成功 才会调用P2的成功 才会调用P3的成功,这情况不就是链式调用了么。

测试情况如下:

myAsyncFunction().then(res => {
     console.log('res:', res) 
     return res + '_123'
}, (err) => {}).then(res => {
     console.log('res:', res)
     return res + '_123'
}, (err) => {}).then(res => {
     console.log('res:', res)
}, (err) => {})

// after 1000ms 输出
res: 123
res: 123_123
res: 123_123_123

从结果上说明,下个then能拿到上一个的值,并将值传递回去,这里链式调用通了,但如果值是一个新的Promise呢,对于这种情况先想一下, 首先需要在接收值得时候判断一下是否为Promise的实例,然后为该实例添加then函数,当实例成功或失败的时候还是走原来的逻辑

3. 完善then链式调用

then(onFulfilledFunc, onRejectedFunc){
       // 注意:下面的this都是指向的是调用者
       // 也就是调用者成功才会促使p成功
        let p = new MyPromise( (resolve, reject) => {
            this.resolveFunc = () => {
                let data = onFulfilledFunc(this.data);
                // 判断返回值是否为新的Promise 
                if (data instanceof MyPromise) {
                    // 此时 data 为 promise
                    // 为实例设置成功、失败的回调函数
                    data.then((y) => resolve(y), (err) => reject(err));
                    // 注意 reslove 、reject都是p的 
                    // 这样在调用data.then之后 就间接通知p成功、失败并把值传给p.then
                }else{
                    resolve(data);  // 将值传递下去
                }
            }
            this.rejectFunc = () => {
                onRejectedFunc(this.reason);
                // 失败就不用继续传递了
            }
        })
        return p // 返回新Promise实例
        // 调用示例: p.then().then().then()
    }

这样就处理完对于返回MyPromise的情况,测试一下:

myAsyncFunction().then(res => {
     console.log('res:', res) 
     return new MyPromise((resolve, reject) => {
            setTimeout(() => {
                resolve(456)
            }, 1000);
          });
}, (err) => {}).then(res => {
     console.log('res:', res)
     return res + '_123'
}, (err) => {}).then(res => {
     console.log('res:', res)
}, (err) => {})

// after 1000ms: 输出
res: 123
// after 1000ms: 输出
res: 456
res: 456_123

发现是成功的,说明链式调用我们完成了。

但我发现还有用法3这种情况:同一个实例多次调用then

// 用法3:
function myAsyncFunction() {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve('123'), 1000);
    });
}
let p = myAsyncFunction()
p.then(res => {
     console.log(res) // 输出 res:123
     return 456 // 注意它不会影响p
}, (err) => {})
p.then(res => {
     console.log(res) // 输出 res:123
}, (err) => {})

发现了么,第一个p then 的 返回 456 不会修改下一次调用p的结果,这样也合理他们的调用者都是p,所以在then中返回值是没有用的,按这种要求我们改起来还是很简单的,直接看代码。

class MyPromise {
   constructor(executor) {
      ....
      // this.resolveFunc = () => {}; // 操作成功时 存放成功的回调函数
      // this.rejectFunc = () => {}; // 操作失败时 存放失败的回调函数
      // 用数组存放同一个实例多次调用then的回调函数
      // 注意哦:不是为了then的链式调用才用数组,是为了同一个实例多次调用
      this.resolveFuncArr = [];  // 操作成功时 存放成功的回调函数
      this.rejectFuncArr = [];  // 操作失败时 存放失败的回调函数
  }
  resolve(data) {
      ...
      // this.resolveFunc(this.data); 
      this.resolveFuncArr.forEach( resolveFunc=> resolveFunc(this.data) )
  }
  reject(reason) {
      ...
      // this.rejectFunc(this.reason); 
      this.rejectFuncArr.forEach( rejectFunc=> rejectFunc(this.reason) )
  }
 then(onFulfilledFunc, onRejectedFunc){
      let p = new MyPromise( (resolve, reject) => {
            // this.resolveFunc = () => {...}
            this.resolveFuncArr.push(() => {
                let data = onFulfilledFunc(this.data);
                // 判断返回值是否为新的Promise
                if (data instanceof MyPromise) {
                    // 此时 data 为 promise
                    // 为实例设置成功、失败的回调函数
                    data.then((y) => resolve(y), (err) => reject(err));
                    // 注意 reslove 、reject都是p的 
                    // 这样在调用data.then之后 就间接通知p成功、失败并把值传给p.then
                }else{
                    resolve(data);  // 将值传递下去
                }
            })
            // this.rejectFunc = () => {...}
            this.rejectFuncArr.push(() => {
                onRejectedFunc(this.reason);
                // 失败就不用继续传递了
            })
        })
        return p // 返回新Promise实例
  }
}

以上就是完成了对同一个实例多次调用then的封装,其实就是换了一种存储方式,测试一下

let p = myAsyncFunction()
p.then(res => {
     console.log(res) // 输出 res:123
     return 456
}, (err) => {})
p.then(res => {
     console.log(res) // 输出 res:123
}, (err) => {})
// after 1000ms 输出
res:123
res:123

以上就完成了Promise的总体框架。

完整版Promise

看一下总体代码:

class MyPromise {
  static pending = "pending";
  static fulfilled = "fulfilled";
  static rejected = "rejected";
  
  constructor(executor) {
      this.status = MyPromise.pending; // 初始化状态为 pending
      this.data = undefined; // 操作成功时 存放成功数据
      this.reason = undefined; // 操作失败时 存放失败的原因
      
      // 用数组存放同一个实例多次调用then的回调函数
      // 注意哦:不是为了then的链式调用才用数组,是为了同一个实例多次调用
      // 即不是为了 p.then().then().then() 而是为了: p.then() p.then() p.then()
      this.resolveFuncArr = [];  // 操作成功时 存放成功的回调函数
      this.rejectFuncArr = [];  // 操作失败时 存放失败的回调函数
      
      // 应为resolve函数是在executor中调用 ,也就是默认resolve函数的this指向的是executor函数
      // 所以要修改resolve的this指向为当前的MyPromise
      executor(this.resolve.bind(this), this.reject.bind(this))
  }
  
  resolve(data) {
      this.status = MyPromise.fulfilled; // 更改状态
      this.data = data; // 存放成功的数据
      
      // 执行操作成功时的回调函数
      this.resolveFuncArr.forEach( resolveFunc=> resolveFunc(this.data) )
  }
  
  reject(reason) {
    this.status = MyPromise.rejected; // 更改状态
    this.reason = reason; // 存放失败的原因

    // 执行操作失败时的回调函数
    this.rejectFuncArr.forEach( rejectFunc=> rejectFunc(this.reason) )
  }
  then(onFulfilledFunc, onRejectedFunc){
       // 注意:下面的this都是指向的是上一个then
       // 也就是调用者成功才会促使p成功
       let p = new MyPromise( (resolve, reject) => {
           this.resolveFuncArr.push(() => {
                let data = onFulfilledFunc(this.data);
                // 判断返回值是否为新的Promise
                if (data instanceof MyPromise) {
                    // 此时 data 为 promise
                    // 为实例设置成功、失败的回调函数
                    data.then((y) => resolve(y), (err) => reject(err));
                    // 注意 reslove 、reject都是p的 
                    // 这样在调用data.then之后 就间接通知p成功、失败并把值传给p.then
                }else{
                    resolve(data);  // 将值传递下去
                }
            })
            this.rejectFuncArr.push(() => {
                onRejectedFunc(this.reason);
                // 失败就不用继续传递了
            })
        })
        return p // 返回新Promise实例
       
    }
}

OIP-C[1].jpg

至此,一个Promise框架就完成了,当然还有很多需要处理,比如循环调用问题,异常处理等等,但这些都是细节,也就是不用太费脑的事情,不过是在相应的地方加上判断或加上try{}catch{} 或者在进一步抽象出函数让代码可读性更高即可。

欢迎大家留言讨论