006: Promise之问(三)——Promise 如何实现链式调用?

从现在开始,我们就来动手实现一个功能完整的Promise,一步步深挖其中的细节。我们先从链式调用开始。

简易版实现

首先写出第一版的代码:

//定义三种状态
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

function MyPromise(executor) {
  let self = this; // 缓存当前promise实例
  self.value = null;
  self.error = null; 
  self.status = PENDING;
  self.onFulfilled = null; //成功的回调函数
  self.onRejected = null; //失败的回调函数

  const resolve = (value) => {
    if(self.status !== PENDING) return;
    setTimeout(() => {
      self.status = FULFILLED;
      self.value = value;
      self.onFulfilled(self.value);//resolve时执行成功回调
    });
  };

  const reject = (error) => {
    if(self.status !== PENDING) return;
    setTimeout(() => {
      self.status = REJECTED;
      self.error = error;
      self.onRejected(self.error);//resolve时执行成功回调
    });
  };
  executor(resolve, reject);
}
MyPromise.prototype.then = function(onFulfilled, onRejected) {
  if (this.status === PENDING) {
    this.onFulfilled = onFulfilled;
    this.onRejected = onRejected;
  } else if (this.status === FULFILLED) {
    //如果状态是fulfilled,直接执行成功回调,并将成功值传入
    onFulfilled(this.value)
  } else {
    //如果状态是rejected,直接执行失败回调,并将失败原因传入
    onRejected(this.error)
  }
  return this;
}

可以看到,Promise 的本质是一个有限状态机,存在三种状态:

  • PENDING(等待)
  • FULFILLED(成功)
  • REJECTED(失败)

状态改变规则如下图:

project

对于 Promise 而言,状态的改变不可逆,即由等待态变为其他的状态后,就无法再改变了。

不过,回到目前这一版的 Promise, 还是存在一些问题的。

设置回调数组

首先只能执行一个回调函数,对于多个回调的绑定就无能为力,比如下面这样:

let promise1 = new MyPromise((resolve, reject) => {
  fs.readFile('./001.txt', (err, data) => {
    if(!err){
      resolve(data);
    }else {
      reject(err);
    }
  })
});

let x1 = promise1.then(data => {
  console.log("第一次展示", data.toString());    
});

let x2 = promise1.then(data => {
  console.log("第二次展示", data.toString());    
});

let x3 = promise1.then(data => {
  console.log("第三次展示", data.toString());    
});

这里我绑定了三个回调,想要在 resolve() 之后一起执行,那怎么办呢?

需要将 onFulfilledonRejected 改为数组,调用 resolve 时将其中的方法拿出来一一执行即可。

self.onFulfilledCallback = [];
self.onRejectedCallback = [];
MyPromise.prototype.then = function(onFulfilled, onRejected) {
  if (this.status === PENDING) {
    this.onFulfilledCallbacks.push(onFulfilled);
    this.onRejectedCallbacks.push(onRejected);
  } else if (this.status === FULFILLED) {
    onFulfilled(this.value);
  } else {
    onRejected(this.error);
  }
  return this;
}

接下来将 resolve 和 reject 方法中执行回调的部分进行修改:

// resolve 中
self.onFulfilledCallback.forEach((callback) => callback(self.value));
//reject 中
self.onRejectedCallback.forEach((callback) => callback(self.error));

链式调用完成

我们采用目前的代码来进行测试:

let fs = require('fs');
let readFilePromise = (filename) => {
  return new MyPromise((resolve, reject) => {
    fs.readFile(filename, (err, data) => {
      if(!err){
        resolve(data);
      }else {
        reject(err);
      }
    })
  })
}
readFilePromise('./001.txt').then(data => {
  console.log(data.toString());    
  return readFilePromise('./002.txt');
}).then(data => {
  console.log(data.toString());
})
// 001.txt的内容
// 001.txt的内容

咦?怎么打印了两个 001,第二次不是读的 002 文件吗?

问题出在这里:

MyPromise.prototype.then = function(onFulfilled, onRejected) {
  //...
  return this;
}

这么写每次返回的都是第一个 Promise。then 函数当中返回的第二个 Promise 直接被无视了!

说明 then 当中的实现还需要改进, 我们现在需要对 then 中返回值重视起来。

MyPromise.prototype.then = function (onFulfilled, onRejected) {
  let bridgePromise;
  let self = this;
  if (self.status === PENDING) {
    return bridgePromise = new MyPromise((resolve, reject) => {
      self.onFulfilledCallbacks.push((value) => {
        try {
          // 看到了吗?要拿到 then 中回调返回的结果。
          let x = onFulfilled(value);
          resolve(x);
        } catch (e) {
          reject(e);
        }
      });
      self.onRejectedCallbacks.push((error) => {
        try {
          let x = onRejected(error);
          resolve(x);
        } catch (e) {
          reject(e);
        }
      });
    });
  }
  //...
}

假若当前状态为 PENDING,将回调数组中添加如上的函数,当 Promise 状态变化后,会遍历相应回调数组并执行回调。

但是这段程度还是存在一些问题:

  1. 首先 then 中的两个参数不传的情况并没有处理,
  2. 假如 then 中的回调执行后返回的结果(也就是上面的x)是一个 Promise, 直接给 resolve 了,这是我们不希望看到的。

怎么来解决这两个问题呢?

先对参数不传的情况做判断:

// 成功回调不传给它一个默认函数
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value;
// 对于失败回调直接抛错
onRejected = typeof onRejected === "function" ? onRejected : error => { throw error };

然后对返回Promise的情况进行处理:

function resolvePromise(bridgePromise, x, resolve, reject) {
  //如果x是一个promise
  if (x instanceof MyPromise) {
    // 拆解这个 promise ,直到返回值不为 promise 为止
    if (x.status === PENDING) {
      x.then(y => {
        resolvePromise(bridgePromise, y, resolve, reject);
      }, error => {
        reject(error);
      });
    } else {
      x.then(resolve, reject);
    }
  } else {
    // 非 Promise 的话直接 resolve 即可
    resolve(x);
  }
}

然后在 then 的方法实现中作如下修改:

resolve(x)  ->  resolvePromise(bridgePromise, x, resolve, reject);

在这里大家好好体会一下拆解 Promise 的过程,其实不难理解,我要强调的是其中的递归调用始终传入的resolvereject这两个参数是什么含义,其实他们控制的是最开始传入的bridgePromise的状态,这一点非常重要。

紧接着,我们实现一下当 Promise 状态不为 PENDING 时的逻辑。

成功状态下调用then:

if (self.status === FULFILLED) {
  return bridgePromise = new MyPromise((resolve, reject) => {
    try {
      // 状态变为成功,会有相应的 self.value
      let x = onFulfilled(self.value);
      // 暂时可以理解为 resolve(x),后面具体实现中有拆解的过程
      resolvePromise(bridgePromise, x, resolve, reject);
    } catch (e) {
      reject(e);
    }
  })
}

失败状态下调用then:

if (self.status === REJECTED) {
  return bridgePromise = new MyPromise((resolve, reject) => {
    try {
      // 状态变为失败,会有相应的 self.error
      let x = onRejected(self.error);
      resolvePromise(bridgePromise, x, resolve, reject);
    } catch (e) {
      reject(e);
    }
  });
}

Promise A+中规定成功和失败的回调都是微任务,由于浏览器中 JS 触碰不到底层微任务的分配,可以直接拿 setTimeout(属于宏任务的范畴) 来模拟,用 setTimeout将需要执行的任务包裹 ,当然,上面的 resolve 实现也是同理, 大家注意一下即可,其实并不是真正的微任务。

if (self.status === FULFILLED) {
  return bridgePromise = new MyPromise((resolve, reject) => {
    setTimeout(() => {
      //...
    })
}
if (self.status === REJECTED) {
  return bridgePromise = new MyPromise((resolve, reject) => {
    setTimeout(() => {
      //...
    })
}

好了,到这里, 我们基本实现了 then 方法,现在我们拿刚刚的测试代码做一下测试, 依次打印如下:

001.txt的内容
002.txt的内容

可以看到,已经可以顺利地完成链式调用。

错误捕获及冒泡机制分析

现在来实现 catch 方法:

Promise.prototype.catch = function (onRejected) {
  return this.then(null, onRejected);
}

对,就是这么几行,catch 原本就是 then 方法的语法糖。

相比于实现来讲,更重要的是理解其中错误冒泡的机制,即中途一旦发生错误,可以在最后用 catch 捕获错误。

我们回顾一下 Promise 的运作流程也不难理解,贴上一行关键的代码:

// then 的实现中
onRejected = typeof onRejected === "function" ? onRejected : error => { throw error };

一旦其中有一个PENDING状态的 Promise 出现错误后状态必然会变为失败, 然后执行 onRejected函数,而这个 onRejected 执行又会抛错,把新的 Promise 状态变为失败,新的 Promise 状态变为失败后又会执行onRejected......就这样一直抛下去,直到用catch 捕获到这个错误,才停止往下抛。

这就是 Promise 的错误冒泡机制

至此,Promise 三大法宝: 回调函数延迟绑定回调返回值穿透错误冒泡

微信公众号: 前端三元同学

获取更多资料/联系加交流群