遵循Promises/A+规范,手把手带你实现Promise源码(核心篇)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 遵循Promises/A+规范,手把手带你实现Promise源码(核心篇)

前言


上几周小包一直在跟 Promise 较劲,不彻底学会 Promise 不罢休,Promise 不止用起来强大,学起来也是让人受益匪浅,Promise基础篇部分我们可以学会基础 Promise 功能,异步逻辑,链式调用的编写。Promise 静态四兄弟 部分我们可以学习 Promise 的四种静态方法 all、race、allSettled、any 的使用及其实现。


Promise 部分还缺少最核心的一环 —— Promise Resolution Procedure。那这个东西又是什么呐?


Promise Resolution Procedure


我们回忆下基础篇链式调用部分的代码,代码是这样处理的:


image.png

then 方法中按照 promise 状态分成了三种情况,回调函数后的返回值 x,直接作为 promise 的成功值。


看到这里,你应该会产生疑惑,难道所有类型的返回值 x 都直接返回吗?


当然不是,Promises/A+ 针对返回值 x 有非常复杂的处理,这也就是本文要讲解的 The Promise Resolution Procedure


我们从基础篇的实现代码也可以发现,三种情况的处理代码是高度类似的,如果再加入对返回值 x 多种情况的处理,代码不堪想象,因此我们将这部分逻辑抽离出来成 resolvePromise 方法。


resolvePromise 方法接受四个参数:


resolvePromise(promise2, x, resolve, reject);
复制代码


promise2 就是 then 方法返回值,xthen 回调函数的返回值,我们根据返回值 x 的情况,决定调用 resolvereject


返回值 x


在阅读 Promises/A+ 规范之前,我们先来思考一下返回值 x 会有那些情况:


  • x 是个普通值(原始类型)
  • x 是基于 Promises/A+ 规范 promise 对象
  • x 是基于其他规范的 promise 对象
  • x 就是 promise2 (x 与 promise2 指向同一对象)


返回值 x 的情况是非常复杂的,我们接下来来看一下 Promises/A+ 规范是如何处理上述多种情况。


Promises/A+ 规范解读


针对 Promise Resolution Procedure 部分,Promises/A+ 规范洋洋洒洒的写了很大篇幅:


image.png

下面咱们来理解上述规范到底讲述了什么。


Promise 解析过程是一个抽象操作,参数值为 promise 和 value,我们将其表示为 [[Resolve]](promise,x)


这里与上面讲述的 resolvePromise 是相通的,只不过为了代码编写,我们同时将 promise2resovlereject 方法作为参数传入。


如果 x 可 thenable,那么我们就认为 x 是一个类 promise 对象,它会尝试让 promise 采用 x 的状态。只要它们符合 Promises/A+ 的 then 方法,允许 promise 对 thenable 的处理进行互操作。


promise 有很多规范,Promises/A+ 只是其中之一,Promises/A+ 希望自身能兼容其他规范下的 promise 对象,判断依据为该对象是否可 thenable


  1. 如果 promisex 引用的同一对象,则以 TypeError 理由拒绝 (避免循环引用)

什么情况下会出现这种现象呐?看这样一个栗子:


const p = new Promise((resolve, reject) => {
  resolve(1);
});
const promise2 = p.then((data) => {
  return promise2;
});
// TypeError: Chaining cycle detected for promise #<Promise>
promise2.then(null, (err) => console.log(err));
复制代码


  1. 如果 x 是一个 promise ,采用它的状态
  • 如果 x 状态是 pending ,则 promise 也需要保持 pending 状态直至 x 状态转变为 fulfilledrejected
  • 如果 x 状态是 fulfilled ,则以同样的值完成 promise
  • 如果 x 状态是 rejected ,则以同样的原因拒绝 promise


上面规范指出了当返回值 xpromise 对象时,我们应该如何处理,但并没有给出如何判断返回值 x 是否为 promise 对象


  1. x 是一个对象或者函数
  • 声明 then 其值为 x.then
  • 如果检索属性 x.then 导致抛出异常 e,则以 e 为拒绝原因拒绝 promise。
  • 如果 then 是一个函数,x 作为 thenthis 调用该方法,第一个参数是成功的回调函数,第二个参数是失败的回调函数—— 判断是否为 promise 的最小判断
  • 如果成功回调以值 y 调用,运行 [[Resolve]](promise,y)
  • 如果失败回调以原因 r 调用,用 r 拒绝 promise
  • 如果成功回调与失败回调都被调用或多次调用同一个参数,则第一个调用优先,其他调用都将被忽略。
  • 如果调用 then 方法抛出异常 e:
  • 若成功回调或失败回调都调用过,忽略
  • 未调用,用 e 作为原因拒绝 promise
  • 如果 then 不是函数,用 x 作为值完成 promise
  1. 如果 x 既不是对象也不是函数,使用 x 作为值完成 promise

我们已经解读规范完毕,小包下面提出几个问题,加深一下大家对 Promise Resolution 的理解。


问题


  1. 检索 x.then 属性会存在异常情况,你能举个类似栗子吗?


规范考虑的非常全面,由于 Promises/A+ 规范可以兼容其他具有 thenable 能力的 promise 实现,假设这样一个场景:


// 某个人非常变态
// 它给对象x的then属性的getter设置为报错
const x = {};
Object.defineProperty(x, "then", {
  get() {
    throw Error("cant execute then function");
  },
});
// 此时如果在调用 x.then 就会抛出异常
// Uncaught Error: cant execute then function
x.then;
复制代码


  1. 为什么 then 方法通过 call 调用,而非 x.then 调用?


then 属性已经被检索成功,如果再次检索,会存在一定风险。还是上面那个栗子,我们稍微改一下:


let n = 0;
const x = {};
Object.defineProperty(x, "then", {
  get() {
    // 修改成第2次调用抛出异常
    n++;
    if (n >= 2) {
      throw Error("cant execute then function");
    }
    return "success";
  },
});
// success
x.then;
// Uncaught Error: cant execute then function
x.then;
复制代码


then.call(x)x.then 效果相同,而且通过 then.call(x) 可以减少二次检索的风险。


  1. 如果成功回调以值 y 调用,运行 [[Resolve]](promise, y) ,这条规范啥意思?


小包想了很久,终于想通了这里,开始小包误以为此条规范针对了两种 onfulfilled 情况:


  • onFulfilled 函数返回 Promise 实例
  • onFulfilled 函数执行时参数为 Promsie 实例。


但经过对比思考,第二种情况是根本无法达到此规范。小包还是对第二种非常好奇,于是去反复翻阅了 Promises/A+ 规范,发现这竟然是规范的漏网之鱼,规范没有提到这点的处理。但我通过在浏览器进行尝试,发现对于第二种情况,ES6 同样会对此情况递归解析(有机会小包会单独写文章对比这两种情况)


对于onFulfilled 返回 Promise 实例,小包来举个栗子:


const p1 = new Promise((resolve, reject) => {
  resolve("i am p1");
});
const p2 = new Promise((resolve, reject) => {
  resolve("i am p2");
});
const p3 = p2.then((res) => {
  console.log(res);
  return p1;
});
// {
//     [[Prototype]]: Promise
//     [[PromiseState]]: "fulfilled"
//     [[PromiseResult]]: "i am p1"
// }
console.log(p3);
// false
console.log(p3 === p1);
复制代码


从输出结果可以发现,p2then 方法返回值为 p1 ,返回的是全新的 Promise 实例,与 p1 不同,只不过采用了 p1 状态。


  1. 如果成功回调与失败回调都被调用或多次调用同一个参数,则第一个调用优先,其他调用都将被忽略。这条规范又是在处理什么情况?


看到这条规范,你可能会很奇怪,因为咱们手写的 Promise 在基础篇已经处理过当前情况,成功与失败回调只会调用其中之一。 Promises/A+ 规范中多次提到,可以兼容其他具备 thenable 能力的 promise 对象,其他规范实现的 promise 实例未必会处理此情况,因此此条规范是为了兼容其他不完善的 promise 实现。


源码实现


循环引用


如果 promisex 引用的同一对象,则以 TypeError 理由拒绝。因此我们需要给 resolvePromise 添加一步校验:


const resolvePromise = function (promise, x, resolve, reject) {
  // 循环引用,自己等待自己完成
  if (promise === x) {
    // 用一个类型错误,结束掉 promise
    return reject(
      new TypeError(
        `TypeError: Chaining cycle detected for promise #<myPromise> `
      )
    );
  }
};
复制代码


判断 x 是否为 promise 实例


通过上面规范的解读,我们可以把判断 x 是否为 promise 实例归结为以下步骤:


  1. promise 实例应该是对象或者函数: 首先判断 x 是否为对象或函数
  2. promise 对象必须具备 thenable 能力: 接着检索 x.then 属性
  3. then 属性应该是个可执行的函数: 最后判断 then 是否为函数 (这是最小判断)
  4. 如果上述都满足,Promises/A+ 就认为 x 是一个 promise 实例


精炼一下: 首先判断 x 是否为对象或函数;然后判断 x.then 是否为函数


我们来编写一下这部分代码:


function resolvePromise(promise, x, resolve, reject) {
  // 判断 x 是否为对象(排除null情况)或函数
  if ((typeof x === "object" && x !== null) || typeof x === "function") {
    // 检索 x.then 可能会抛出异常
    try {
      // 检索 x.then
      let then = x.then;
      // 判断 then 是否为函数
      // 这是最小判断,满足此条件后,认定为 promise 实例
      if (typeof then === "function") {
        // 执行 x.then 会再次检索 then 属性,有风险发生错误
        // 这里的另外两个参数后面会详细讲解
        then.call(x);
      } else {
        // then 方法不是函数,为普通值——{then:123}
        resolve(x);
      }
    } catch (e) {
      // 存在异常,执行 reject(e)
      reject(e);
    }
  } else {
    // 既不是对象也不是函数,说明是普通值,调用 resolve(x) 完成
    resolve(x);
  }
}
复制代码


返回值 x 为 promise


上文我们已经对此条规范做了详细的解析,但应该如何实现此条规范呐?非常简单,我们只需对 then.call(x) 略作修改即可。


then.call(
  x,
  (y) => {
    // 如果x是一个 promise 就用他的状态来决定 走成功还是失败
    resolvePromise(promise, y, resolve, reject); //递归解析y的值
  },
  (r) => {
    // 一旦失败了 就不在解析失败的结果了
    reject(r);
  }
);
复制代码


不知道大家能不能理解上述递归的原理?小包给举个栗子。


// 假设 y 是 promise,就以下面为例
const y = new Promise((resolve, reject) => {
  resolve(1);
});
// 经过 resolvePromise
resolvePromise(promise, y, resolve, reject);
复制代码


resolvePromise(promise, y, resolve, reject) 的执行流程是这样的:


  1. 经过一系列判断,最终通过 y.then 为函数判断出 ypromise
  2. 执行 then(y, resolvePromsie, rejectPromise)
  3. 上面代码等同于执行下面代码


y.then(
  (y1) => {
    resolvePromise(promise, y1, resolve, reject);
  },
  (r1) => {
    reject(r1);
  }
);
复制代码


  1. y1 的值是 1,为普通值,因此直接调用 resolve(y1)
  2. 因此实现了 promise2 采纳返回值 x 的状态


兼容不完善的 promise 实现


为了兼容不完善的 promise 实现,因此我们需要给 resolvePromise 中执行添加一个锁。


function resolvePromise(promise, x, resolve, reject) {
  // 判断 x 是否为对象(排除null情况)或函数
  let called = false;
  if ((typeof x === "object" && x !== null) || typeof x === "function") {
    try {
      let then = x.then;
      if (typeof then === "function") {
        then.call(
          x,
          (y) => {
            // 添加锁,避免成功后执行失败
            if (called) return;
            called = true;
            resolvePromise(promise, y, resolve, reject);
          },
          (r) => {
            // 添加锁,避免失败后执行成功
            if (called) return;
            called = true;
            reject(r);
          }
        );
      } else {
        resolve(x);
      }
    } catch (e) {
      // 添加锁,避免失败后执行成功
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    // 既不是对象也不是函数,说明是普通值,调用 resolve(x) 完成
    resolve(x);
  }
}
复制代码


then 方法修改


文章最开始我们提到将 then 方法三种情况代码重复度过高,我们将此部分抽离为 resolvePromise ,第一个参数为 promise2


我们取出基础篇 then 方法部分代码,重点关注 resolvePromise 调用部分。你应该很容易问题,promise2then 整体执行完毕后才可以访问,resolvePromise 此时应该是无法访问到该方法。


then(onFulfilled, onRejected) {
  let promise2 = new Promise((resolve, reject) => {
    if (this.status === FULFILLED) {
      try {
        let x = onFulfilled(this.value);
        resolvePromise(promise2, x, resolve, reject);
      } catch (e) {
        reject(e);
      }
    }
  });
  return promise2;
}
复制代码


因此我们需要给 resolvePromise 加一下异步操作,本手写使用 setTimeout 实现。


then(onFulfilled, onRejected) {
  let promise2 = new Promise((resolve, reject) => {
    if (this.status === FULFILLED) {
      // 这样resolvePromise执行时,就可以获取promise2对象了
      setTimeout(() => {
        try {
          let x = onFulfilled(this.value);
          resolvePromise(promise2, x, resolve, reject);
        } catch (e) {
          reject(e);
        }
      }, 0)
    }
  });
  return promise2;
}
复制代码


实现到这里,手写 Promise 就全部剧终了,下面我们来测试一下我们的手写 Promise 是否可以通过 Promises/A+ 提供的案例测试。


完整版 promise 代码: 手写 Promise 完全版


案例测试


延迟对象


在我们的手写 Promise 中添加 deferred 部分代码:


Promise.deferred = function () {
  let dfd = {};
  dfd.promise = new Promise((resolve, reject) => {
    dfd.resolve = resolve;
    dfd.reject = reject;
  });
  return dfd;
};
复制代码


promises-aplus-tests


使用 npm 安装 promises-aplus-tests


npm i promises-aplus-tests
复制代码


然后进入到待测试的 promise 文件夹,执行


promises-aplus-tests promise.js
复制代码


image.png

测试通过,大功告成! 手写 Promise 的部分就全部结束了,不知道你有没有收获很多。




相关文章
|
前端开发
14 # promise 规范测试
14 # promise 规范测试
36 0
|
6月前
|
前端开发 JavaScript 安全
Promise/A+ 规范详解:打造健壮异步代码的必备知识(上)
Promise/A+ 规范详解:打造健壮异步代码的必备知识(上)
Promise/A+ 规范详解:打造健壮异步代码的必备知识(上)
|
6月前
|
前端开发 JavaScript API
Promise 对象与 Promises/A+ 规范
Promise 对象与 Promises/A+ 规范
97 0
|
6月前
|
前端开发 安全
Promise/A+ 规范详解:打造健壮异步代码的必备知识(下)
Promise/A+ 规范详解:打造健壮异步代码的必备知识(下)
Promise/A+ 规范详解:打造健壮异步代码的必备知识(下)
|
6月前
|
前端开发
【源码共读】如何优雅的处理 Promise 的错误
【源码共读】如何优雅的处理 Promise 的错误
95 0
|
存储 前端开发 JavaScript
带你读《现代Javascript高级教程》二十七、实现符合Promise/A+规范的Promise(1)
带你读《现代Javascript高级教程》二十七、实现符合Promise/A+规范的Promise(1)
|
6月前
|
前端开发 JavaScript
如何处理 JavaScript 中的异步操作和 Promise?
如何处理 JavaScript 中的异步操作和 Promise?
64 1
|
6月前
|
前端开发 JavaScript
在JavaScript中,什么是promise、怎么使用promise、怎么手写promise
在JavaScript中,什么是promise、怎么使用promise、怎么手写promise
94 4
|
6月前
|
前端开发 JavaScript 开发者
JavaScript 中的异步编程:Promise 和 Async/Await
在现代的 JavaScript 开发中,异步编程是至关重要的。本文将介绍 JavaScript 中的异步编程概念,重点讨论 Promise 和 Async/Await 这两种常见的处理异步操作的方法。通过本文的阐述,读者将能够更好地理解和应用这些技术,提高自己在 JavaScript 开发中处理异步任务的能力。
|
5月前
|
前端开发 JavaScript 开发者
JavaScript进阶-Promise与异步编程
【6月更文挑战第20天】JavaScript的Promise简化了异步操作,从ES6开始成为标准。Promise有三种状态:pending、fulfilled和rejected。基本用法涉及构造函数和`.then`处理结果,如: ```javascript new Promise((resolve, reject) =&gt; { setTimeout(resolve, 2000, &#39;成功&#39;); }).then(console.log); // 输出: 成功
86 4