实现 Promise

发布于 2025-05-11 04:56:29 字数 11561 浏览 9 评论 0

使用 Promise 是极好的,它是如此有用以至于我觉得应该好好研究一下 Promise,甚至是实现一个简易的版本。实现之前,我们先来看看 Promise 的用途:

使用 Promise

callback hell

Promise 的第一个用途是能够很好地解决 回调黑洞 的问题,假设要实现一个用户展示的任务,这个任务分为三步:

  1. 获取用户信息
  2. 获取用户图像
  3. 弹窗提示

不使用 Promise,我们的实现可能是这样子:

getUserInfo(id, function (info) {
  getUserImage(info.img, function () {
    showTip();
  })
})

这里只是三步,如果有更长串的任务时,我们就会陷入到回调黑洞之中,为了解决这个问题,我们就可以使用 Promise 来处理这一长串任务,使用 Promise 的版本是这样子的:

// getUserInfo 返回 promise
getUserInfo(id)
  .then(getUserImage)
  .then(showTip)
  .catch(function (e) {
     console.log(e);
  });

原来向右发展的代码,开始向下发展,这样也更适合编程习惯,如果要让我们的代码更加健壮,我们就需要在每一步来处理错误信息,使用 promise 这后,我们只需要在最后的 catch 中做善后处理。

并发

假如我们要显示某一个页的 10 条记录,但是我们只有一个通过 id 获取记录的接口,这样我们就需要发送 10 个请求,并且所有请求都完成之后再将记录全部添加到页面之中,Promise 在这个场景下使用是特别合适的。

代码可能是这样子:

// ids 要获取信息的所有记录 id
// getRecordById 获取记录的接口,返回 promise
Promise.all(ids.map(getRecordById))
  .then(showRecords)
  .catch(function (e) {
     console.log(e);
  });

这就是 Promise 的一些简单的用途,当然令人兴奋的是 Promise 已经是 ES6 的标准,而且目前很多浏览器已经原生支持 Promise 了。对于那些无法使用 Promise 的浏览器,我们就只能自己去实现了,下面就来看看 Promise 的简单实现吧。

实现

warm up

先来盗用一张 MDN 的图,先来热热身,看看 Promise 的状态迁移:

Promise 有三种状态:

  1. pending:初始状态,非 fulfilled 或 rejected
  2. fulfilled: 成功的操作
  3. rejected: 失败的操作

我们可以看出新建的 Promise 是 pending 状态,fulfill 之后就会执行调用 then 的回调函数了,倘若 reject 了就会调用 catch 来进行异常处理了,并且无论是调用 then 还是 catch 都会返回新的 promise,这就是为什么 promise 可以链式调用了。

接着,我们来研究一下规范是怎么描述
promise 的。这里只抽取核心部分,边界问题不考虑。

构造函数:Promise ( executor )

  1. 检查参数:例如 executor 是不是函数啊
  2. 初始化: [[State]]=pending[[FulfillReactions]]=[] , [[RejectReactions]]=[]
  3. 创建 resolve 对象: {[[Resolve]]: resolve, [[Reject]]: reject}
  4. 执行 executor: executor(resolve, reject)

因此构造函数里面传入的 excuter 是立即被执行的。FulfillReactions 存储着 promise 执行成功时要做的操作,RejectReactions 存储着 promise 是要执行的操作。

function Promise(resolver) {
  this._id = counter++;
  this._state = PENDING;
  this._result = undefined;
  this._subscribers = [];

  var promise = this;

  if (noop !== resolver) {
    try {
      resolver(function (value) {
        resolve(promise, value);
      }, function (reason) {
        reject(promise, reason);
      });
    } catch (e) {
      reject(promise, e);
    }
  }
}

FulfillPromise(promise, value)

  1. 检查[[state]],必须为 pending(不是 pending 的表示已经解析,不能重复解析)
  2. 赋值: [[Result]]=value[[state]]=fulfilled
  3. 触发[[FulfillReactions]]的操作

和 FulfillPromise 联系最紧密的就是 ResolvePromise 了,这里我们给出的是 ResolvePromise 的实现,区别只是多了直接解析 Promise。

function resolve(promise, value) {
  // 要 resolve 的为 promise(then 的 callback 返回的是 promise)
  if (typeof value === 'object'
    && promise.constructor === value.constructor) {
    handleOwnThenable(promise, value);
  } 
  // 要 resolve 的是值
  else {
    if (promise._state !== PENDING) { return; }

    promise._result = value;
    promise._state = FULFILLED;

    asap(publish, promise);
  }
}

function handleOwnThenable(promise, thenable) {
  // 如果返回的 promise 已经完成
  // 直接用该 promise 的值 resolve 父 promise
  if (thenable._state === FULFILLED) {
    resolve(promise, thenable._result);
  } else if (thenable._state === REJECTED) {
    reject(promise, thenable._result);
  }
  // 如果返回的 promise 未完成
  // 要等该 promise 完成再 resolve 父 promise
  else {
    subscribe(thenable, undefined, function(value) {
      resolve(promise, value);
    }, function(reason) {
      reject(promise, reason);
    });
  }
}

RejectPromise(promise, reason)

  1. 检查[[state]],必须为 pending(不是 pending 的表示已经解析,不能重复解析)
  2. 赋值: [[Result]]=reason[[state]]=rejected
  3. 触发[[RejectReactions]]的操作

触发[[FulfillReactions]]和触发[[RejectReactions]]实际就是遍历数组,执行所有的回调函数。

function reject(promise, reason) {
  if (promise._state !== PENDING) { return; }

  promise._state = REJECTED;
  promise._result = reason;

  asap(publish, promise);
}

Promise.prototype.then(onFullfilled, onRejected)

  1. promise=this
  2. 新建 resultCapability 三元组, {[[Promise]], [[Resolve]], [[Reject]]} ([[Promise]]新建的)
  3. fulfillReaction={[[Capabilities]]: resultCapability, [[Handler]]: onFulfilled}
  4. rejectReaction={[[Capabilities]]: resultCapability, [[Handler]]: onRejected}
  5. 如果[[state]]是 pending:fulfillReaction 加入[[FulfillReactions]],rejectReaction 加入[[RejectReactions]]
  6. 如果[[state]]是 fulfilled:fulfillReaction 加入执行队列
  7. 如果[[state]]是 rejected:rejectReaction 加入执行队列
  8. 返回 resultCapability.[[Promise]]

这里可以看出构造函数和 then 的关系是很紧密的,新建的 promise 如果是异步操作,那么状态就是 pending,调用 then 时会新建子 promise,并且将回调操作加入父 promise 的[[FulfillReactions]]或[[RejectReactions]]的数组里,这实际就是 发布订阅模式

他们是这样的关系:

无论是 new promise 还是调用 then 或 catch,都会得到一个新的 promise,这些 promise 都会订阅父级 promise 的完成事件,父级 promise 完成之后就会执行一系列的回调操作,也就是发布。

Promise.prototype.catch(onRejected)

  • then 的语法糖: then(null, onRejected)

下面就是 Promise 原型:

Promise.prototype = {
  constructor: Promise,

  then: function (onFulfillment, onRejection) {
    var parent = this;
    var state = parent._state;

    if (state === FULFILLED && !onFulfillment
        || state === REJECTED && !onRejection) {
        return this;
    }

    var child = new Promise(noop);
    var result = parent._result;

    if (state) {
      var callback = arguments[state - 1];
      asap(function () {
        invokeCallback(state, child, callback, result);
      });
    } else {
      subscribe(parent, child, onFulfillment, onRejection);
    }

    return child;
  },

  'catch': function (onRejection) {
    return this.then(null, onRejection);
  }
};

Promise.resolve(value)

  1. 新建 promise
  2. 调用 ResolvePromise(promise, value) (未列出,会判断一些情况然后调用 FulfillPromise)
  3. 返回 promise
Promise.resolve = function (arg) {
  var child = new Promise(noop);
  resolve(child, arg);
  return child;
};

Promise.reject(value)

  1. 新建 promise
  2. 调用 RejectPromise(promise, value)
  3. 返回 promise
Promise.reject = function (reason) {
  var child = new Promise(noop);
  reject(child, reason);
  return child;
};

Promise.all(iterator)

到这里我们已经能够实现基本的 promise 了, Promise.allPromise.race 就不继续描述了,有兴趣的可以继续去读规范,这里上图来说明我对这两个函数的理解:

调用 promise.all 会新建一个对象来存储所有 promise 的处理状态,保存执行的结果,当 remain 为 0 时,就可以 resolve 新建的 promise,这样就可以继续往后执行了。

Promise.all = function (promises) {
  var child = new Promise(noop);
  var record = {
    remain: promises.length,
    values: []
  };
  promises.forEach(function (promise, i) {
    if (promise._state === PENDING) {
      subscribe(promise, undefined, onFulfilled(i), onRejected);
    } else if (promise._state === REJECTED) {
      reject(child, promise._result);
      return false;
    } else {
      --record.remain;
      record.values[i] = promise._result;
      if (record.remain == 0) {
        resolve(child, values);
      }
   }
  });
  return child;

  function onFulfilled(i) {
    return function (val) {
      --record.remain;
      record.values[i] = val;
      if (record.remian === 0) {
        resolve(child, record.values);
      }
    }
  }
  
  function onRejected(reason) {
    reject(child, reason);
  }
};

Promise.race(iterator)

promise.race 与 promise.all 类似,不过只要有一个 promise 完成了,我们就可以 resolve 新建的 promise 了。

Promise.race = function (promises) {
  var child = new Promise(noop);

  promises.forEach(function (promise, i) {
    if (promise._state === PENDING) {
      subscribe(promise, undefined, onFulfilled, onRejected);
    } else if (promise._state === REJECTED) {
      reject(child, promise._result);
      return false;
    } else {
      resolve(child, promise._result);
      return false;
    }
  });
  return child;

  function onFulfilled(val) {
    resolve(child, val);
  }
        
  function onRejected(reason) {
    reject(child, reason);
  }
};

这就是 promise 的基本内容了,完整代码请戳 这里

其他问题

promises 穿透

如果传入 then 里面的参数不是函数,就会被忽略,这就是 promise 穿透的原因,所以 永远往 then 里面传递函数 。答案可以从 then 方法里面调用的一个关键函数 invokeCallback 中找到答案:

function invokeCallback(settled, promise, callback, detail) {
    var hasCallback = (typeof callback === 'function'),
      value, error, succeeded, failed;

    if (hasCallback) {

      try {
        value = callback(detail);
      } catch (e) {
        value = {
          error: e
        };
      }

      if (value && !!value.error) {
        failed = true;
        error = value.error;
        value = null;
      } else {
        succeeded = true;
      }

    }
    // then 的参数不是函数
    // 会被忽略,也就是 promise 穿透
    else {
      value = detail;
      succeeded = true;
    }

    if (promise._state === PENDING) {
      if (hasCallback && succeeded
          || settled === FULFILLED) {
        resolve(promise, value);
      } else if (failed || settled === REJECTED) {
        reject(promise, error);
      }
    }
  }

例如如下例子,结果都是输出 foo:

Promise.resolve('foo').then(Promise.resolve('bar')).then(function (result) {
  console.log(result);
});

Promise.resolve('foo').then(null).then(function (result) {
  console.log(result);
});

拥抱金字塔

promise 能够很好的解决金字塔问题,但是有时候我们也是需要适当使用金字塔的,例如我们要同时获取两个 promise 的结果,但是这两个 promise 是有关联的,也就是有顺序的,该怎么办?

也许解决方案会是这样,定义一个全局变量,这样在第二个 then 里面就可以使用两个 promise 的结果了。

var user;
getUserByName('nolan').then(function (result) {
  user = result;
  return getUserAccountById(user.id);
}).then(function (userAccount) {
  // 好了, "user" 和 "userAccount" 都有了
});

但是这不是最好的方案,此时何不抛弃成见,拥抱金字塔:

getUserByName('nolan').then(function (user) {
  return getUserAccountById(user.id).then(function (userAccount) {
    // 好了, "user" 和 "userAccount" 都有了
  });
});

promise 是如此强大而且难以理解,但是抓住实质之后其实并没有想象的那么复杂,这也是为什么我要写下这篇文章。更过关于如何正确使用 promise,请看第三篇 参考文章 ,强力推荐。

参考

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。