使用es5实现(手写)promise

338 阅读7分钟

前言

Promise 作为ES6新增的重头戏,给前端开发者们带来了极大的便利。个人觉得呢,掌握一个知识点最好的方式,就是运用之前的知识去实现它,不能实现的,则多实践,多运用,Promise则完全可以用es5之前的知识支实现的。手写Promsie不仅可以更加熟练的使用Promise,还可以大大加深对回调函数的运用。在es6的Promise出现之前,社区里面出现了一些Promse规范,如Promise A+等等。下面则按照es6的标准,使用es5的知识实现一个完整的Promise

核心方法then

Promise的核心方法是then方法,其它方法均可以通过then方法实现,先把then方法的基础功能实现,再解决其中的一些问题、完善其它的功能,再利用then方法实现其它方法即可

架构搭建

定义好Promise函数、相关的状态值、原型方法和静态方法等。后面的代码只贴在前一部分上修改的部分

// 定义 promise 类
function MyPromise() {}
​
// 使用属性描述符定义几种状态,防止被修改
Object.defineProperties(MyPromise, {
  STATUS_PENDING: {
    value: 'pending',
  },
  STATUS_FULFILLED: {
    value: 'fulfilled',
  },
  STATUS_REJECTED: {
    value: 'rejected',
  },
});
​
// 原型方法
MyPromise.prototype = {
  then: function () {},
};
// 修正构造器
Object.defineProperty(MyPromise.prototype, 'constructor', {
  value: MyPromise,
});
​
// all、allSettled等静态方法
var staticMethods = {
  all: function () {},
};
for (const mKey in staticMethods) {
  if (!MyPromise.hasOwnProperty(mKey)) {
    Object.defineProperty(MyPromise, mKey, { value: staticMethods[mKey] });
  }
}

then方法基础功能实现

实现then方法的基础功能:

  • 当调用 resolve 时,promise 进入fulfilled 状态,调用 then方法传入的第1个回调
  • 当调用 reject 时,promise 进入rejected 状态,调用 then方法传入的第2个回调
  • 执行创建Promise时传入的回调如果出现异常,promise 进入rejected 状态
  • 同一个实例可以多次调用 then 方法,并且可以正常执行回调
function MyPromise(executor) {
  var self = this
  this._status = MyPromise.STATUS_PENDING
  this._onFulfilledFns = [] //用于存放当 promise 状态变为 fulfilled 时的回调函数
  this._onRejectedFns = []
​
  // 产生异常时,进入 rejected 状态
  try {
    executor(resolve, reject)
  } catch (error) {
    reject(error)
  }
​
  function resolve(result) {
    // 异步执行 fulfilled 回调
    setTimeout(function() {
      //  校验状态,防止多次执行
      if (self._status !== MyPromise.STATUS_PENDING) {
        return
      }
      self._status = MyPromise.STATUS_FULFILLED
      self._onFulfilledFns.forEach(function (fn) {
        fn(result)
      })
    })
  }
  function reject(reason) {
    setTimeout(function() {
      if (self._status !== MyPromise.STATUS_PENDING) {
        return
      }
      self._status = MyPromise.STATUS_REJECTED
      self._onRejectedFns.forEach(function (fn) {
        fn(reason)
      })
    })
  }
}
// ...
MyPromise.prototype = {
  then: function (onResolved, onRejected) {
    if (typeof onResolved === 'function') {
      this._onFulfilledFns.push(onResolved)
    }
    if (typeof onRejected === 'function') {
      this._onRejectedFns.push(onRejected)
    }
  },
};

上面的代码实现了基础功能,但是同时还还在以下问题:

  • 不支持链式调用
  • 不支持状态已确定的promise:当调用 then 方法时,如果 promise 状态已经发生改变,那么将不会执行回调

支持链式调用、状态确定的promise

想让 then 方法支持链式调用,那么肯定要返回一个 promise 对象;而要支持状态确定的 promise,我们需要在 promise状态发生改变时,保存 promise 的值,并且在调用 then 方法时,直接执行回调函数

function MyPromise(executor) {
  // ...
  function resolve(result) {
    // 异步执行回调
    setTimeout(function() {
      if (self._status !== MyPromise.STATUS_PENDING) {
        return
      }
      self._status = MyPromise.STATUS_FULFILLED
      self._value = result // 保存 promise 的结果值
      self._onFulfilledFns.forEach(function (fn) {
        fn(result)
      })
    })
  }
  function reject(reason) {
    setTimeout(function() {
      if (self._status !== MyPromise.STATUS_PENDING) {
        return
      }
      self._status = MyPromise.STATUS_REJECTED
      self._value = reason
      self._onRejectedFns.forEach(function (fn) {
        fn(reason)
      })
    })
  }
}
​
// 状态发生变化时的中转站,由此调用 resolve 和 reject 方法
function stateChangedMiddleware(onStateChanged, value, resolve, reject) {
  try {
    var result = onStateChanged(value)
    resolve(result)
  } catch (err) {
    reject(err)
  }
}
​
// 原型方法
MyPromise.prototype = {
  then: function (onResolved, onRejected) {
    var self = this
    // 根据es6 Promise 的功能,当回调执行异常时此 promise 进行 rejected 状态,否则进入 resolved 状态
    return new MyPromise(function (resolve, reject) {
      if (typeof onResolved === 'function') {
         // promise 状态已经发生改变的话,异步执行
        if (self._status === MyPromise.STATUS_FULFILLED) {
          setTimeout(function() {
            stateChangedMiddleware(onResolved, self._value, resolve, reject)
          })
        } else {
          //因为我们要拿到执行结果并传递给 resolve,所以在这里套一层函数
          self._onFulfilledFns.push(function(result) {
            stateChangedMiddleware(onResolved, result, resolve, reject)
          })
        }
      }
      if (typeof onRejected === 'function') {
        if (self._status === MyPromise.STATUS_REJECTED) {
          setTimeout(function() {
            stateChangedMiddleware(onRejected, self._value, resolve, reject)
          })
        } else {
          self._onRejectedFns.push(function(reason) {
            stateChangedMiddleware(onRejected, reason, resolve, reject)
          })
        }
      }
    })
  },
};

到此,then 方法的核心功能已经实现了,但是相较于 es6 Promiseresolve 回调,它还不支持 thenable 对象

支持 thenable 对象

thenable 对象

所谓 thenable 对象,就是我们在调用 promise 对象的 resolve 回调时,如果传入的是一个带有 then 属性、并且这个属性是一个方法时,那么 promise 会将 resolvereject 作为参数传入该方法,并且该 promise 的状态也由 then 方法决定。比如在下面这段代码中,输出的是thenable fulfilled而不是带有 then 方法的对象:

new Promise(resolve => {
  resolve({
    then(innerResolve) {
      innerResolve('thenable fulfilled')
    }
  })
}).then(res => console.log(res)) // 'thenable fulfilled'

要支持此功能,只需要判断一下 resolve 传入的参数即可:

function resolve(result) {
    if (result !== null && typeof result === 'object' && typeof result.then === 'function') {
      // 异步调用
      setTimeout(function () {
        result.then(resolve, reject)
      })
      return
    }
    // ...
  }

catchfinally

catch方法

在es6的promise 中,可以使用 then 方法的第二个传入 rejected 时的函数,也可以通过catch方法传入。所以理论上,我们只需要将catch 的参数 传入then 方法的第二个参数即可:

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

但是我们在实现 then 方法的时候,并没有处理传入的参数不是函数的情况,导致在参数不为函数时,当前 promise 的状态和结果不可以传递到下一个 promise,所以这种情况下,我们要传入一个函数,将 promise 的结果传递下去:

MyPromise.prototype = {
  then: function (onResolved, onRejected) {
    var self = this
    return new MyPromise(function (resolve, reject) {
      if (!(typeof onResolved === 'function')) {
        onResolved = function (res) { return res }
      }
      // ...
      if (!(typeof onRejected === 'function')) {
        onRejected = function (reason) { throw reason }
      }
      // ...
    })
  },
  catch: function (onRejected) {
    return this.then(null, onRejected)
  }
};

finally 方法

类似的,只需要将 finally 方法的参数传递给 then 方法的两个参数即可实现 finally 方法:

MyPromise.prototype = {
  // ...
  finally: function (onStateChanged) {
    return this.then(onStateChanged, onStateChanged)
  }
};

静态方法

resolvereject

同样通过 then 方法返回一个 fulfilledrejected 状态的 promise 即可:

var staticMethods = {
  resolve: function (p) {
    return new MyPromise(function (resolve) {
      resolve(p)
    })
  },
  reject: function (p) {
    return new MyPromise(function (resolve, reject) {
      reject(p)
    })
  }
};

all等其它静态方法

allallSettledanyrace等均返回一个 promise 对象,其状态和结果根据传入的参数决定。只要我们在合适的条件下调用 resolvereject 回调即可

全部代码

以下代码尽量按照es6 Promise 的功能实现,但是没有保证所有场景均和 es6 Promise 表现一致,如 es6 Promise.all 方法支持可迭代对象,但是这里只支持数组。

// 定义 promise 类
function MyPromise(executor) {
  var self = this;
  this._status = MyPromise.STATUS_PENDING;
  this._onFulfilledFns = []; //用于存放当 promise 状态变为 fulfilled 时的回调函数
  this._onRejectedFns = [];
​
  // 产生异常时,进入 rejected 状态
  try {
    executor(resolve, reject);
  } catch (error) {
    reject(error);
  }
​
  function resolve(result) {
    if (isThenable(result)) {
      // 异步调用
      setTimeout(function () {
        result.then(resolve, reject);
      });
      return;
    }
    // 校验状态,防止多次执行
    // 异步执行回调
    setTimeout(function () {
      if (self._status !== MyPromise.STATUS_PENDING) {
        return;
      }
      self._status = MyPromise.STATUS_FULFILLED;
      self._value = result; // 保存 promise 的结果值
      self._onFulfilledFns.forEach(function (fn) {
        fn(result);
      });
    });
  }
  function reject(reason) {
    setTimeout(function () {
      if (self._status !== MyPromise.STATUS_PENDING) {
        return;
      }
      self._status = MyPromise.STATUS_REJECTED;
      self._value = reason;
      self._onRejectedFns.forEach(function (fn) {
        fn(reason);
      });
    });
  }
}
​
// 使用属性描述符定义几种状态,防止被修改
Object.defineProperties(MyPromise, {
  STATUS_PENDING: {
    value: 'pending',
  },
  STATUS_FULFILLED: {
    value: 'fulfilled',
  },
  STATUS_REJECTED: {
    value: 'rejected',
  },
});
​
// 状态发生变化时的中转站,由此调用 resolve 和 reject 方法
function stateChangedMiddleware(onStateChanged, value, resolve, reject) {
  var result;
  try {
    result = onStateChanged(value);
    resolve(result);
  } catch (err) {
    reject(err);
  }
  // 为了 any 方法的错误可以抛出
  if (result instanceof AggregateError) {
    throw result;
  }
}
​
// 原型方法
MyPromise.prototype = {
  then: function (onResolved, onRejected) {
    var self = this;
    // 根据es6 Promise 的功能,当回调执行异常时此 promise 进行 rejected 状态,否则进入 resolved 状态
    return new MyPromise(function (resolve, reject) {
      if (!(typeof onResolved === 'function')) {
        onResolved = function (res) {
          return res;
        };
      }
      // promise 状态已经发生改变的话,异步执行
      if (self._status === MyPromise.STATUS_FULFILLED) {
        setTimeout(function () {
          stateChangedMiddleware(onResolved, self._value, resolve, reject);
        });
      } else {
        //因为我们要拿到执行结果并传递给 resolve,所以在这里套一层函数
        self._onFulfilledFns.push(function (result) {
          stateChangedMiddleware(onResolved, result, resolve, reject);
        });
      }
      if (!(typeof onRejected === 'function')) {
        onRejected = function (reason) {
          throw reason;
        };
      }
      if (self._status === MyPromise.STATUS_REJECTED) {
        setTimeout(function () {
          stateChangedMiddleware(onRejected, self._value, resolve, reject);
        });
      } else {
        self._onRejectedFns.push(function (reason) {
          stateChangedMiddleware(onRejected, reason, resolve, reject);
        });
      }
    });
  },
  catch: function (onRejected) {
    return this.then(null, onRejected);
  },
  finally: function (onStateChanged) {
    return this.then(onStateChanged, onStateChanged);
  },
};
// 修正构造器
Object.defineProperty(MyPromise.prototype, 'constructor', {
  value: MyPromise,
});
​
function isThenable(o) {
  return o !== null && typeof o === 'object' && typeof o.then === 'function';
}
function formatePms(pms) {
  if (Object.prototype.toString.call(pms) !== '[object Array]') {
    throw new TypeError('excepted array');
  }
  pms.forEach(function (pm, i) {
    if (!isThenable(pm)) {
      pm = MyPromise.resolve(pm);
    }
  });
}
​
// all、allSettled等静态方法
var staticMethods = {
  resolve: function (p) {
    return new MyPromise(function (resolve) {
      resolve(p);
    });
  },
  reject: function (p) {
    return new MyPromise(function (resolve, reject) {
      reject(p);
    });
  },
  // es6 promise 下列静方法的参数是可迭代对象
  all: function (pms) {
    var count = 0,
      results = [];
    return new MyPromise(function (resolve, reject) {
      formatePms(pms);
      pms.forEach(function (pm, i) {
        pm.then(function (result) {
          results[i] = result;
          if (++count === pms.length) {
            resolve(results);
          }
        }).catch(function (reason) {
          reject(reason);
        });
      });
    });
  },
  allSettled: function (pms) {
    var count = 0,
      results = [];
    return new MyPromise(function (resolve) {
      formatePms(pms);
      pms.forEach(function (pm, i) {
        pm.then(function (result) {
          results[i] = { status: MyPromise.STATUS_FULFILLED, value: result };
          if (++count === pms.length) {
            resolve(results);
          }
        }).catch(function (reason) {
          results[i] = { status: MyPromise.STATUS_REJECTED, reason: reason };
          if (++count === pms.length) {
            resolve(results);
          }
        });
      });
    });
  },
  any: function (pms) {
    var count = 0;
    return new MyPromise(function (resolve) {
      formatePms(pms);
      pms.forEach(function (pm) {
        pm.then(function (result) {
          resolve(result);
        }).catch(function () {
          if (++count === pms.length) {
            return new AggregateError('All promises were rejected');
          }
        });
      });
    });
  },
  race: function (pms) {
    return new MyPromise(function (resolve, reject) {
      formatePms(pms);
      pms.forEach(function (pm) {
        pm.then(function (result) {
          resolve(result);
        }).catch(function (reason) {
          reject(reason);
        });
      });
    });
  },
};
for (const mKey in staticMethods) {
  if (!MyPromise.hasOwnProperty(mKey)) {
    Object.defineProperty(MyPromise, mKey, { value: staticMethods[mKey] });
  }
}