手写Promise(符合Promise/A+规范,基于ES6)

353 阅读11分钟

主题列表:juejin, github, smartblue, cyanosis, channing-cyan, fancy, hydrogen, condensed-night-purple, greenwillow, v-green, vue-pro, healer-readable, mk-cute, jzman, geek-black, awesome-green

贡献主题:github.com/xitu/juejin…

theme: fancy highlight:

Promise已经成为现代前端异步编程的基础。很多新的API和异步编程方式都建立在Promise基础上。因此Promise也是前端开发者必须掌握的技能。

为什么需要Promise?

在没有Promise之前,我们一般通过回调函数进行异步操作,而一旦异步操作依赖嵌套过多,就会出现恶心的回调地狱:

// fetchAPI是mocked请求API方法
fetchAPI("/target-a", (a) => {
  fetchAPI(`/target-b?a=${a}`, (b) => {
    fetchAPI(`/target-c?b=${b}`, (c) => {
      fetchAPI(`/target-d?c=${c}`, (d) => {
        console.log("result", d);
      });
    });
  });
});

除此之外,回调函数还存在信任问题,我们只能把自己的回调函数传给类似fetchAPI这样第三方函数,回调函数的触发时机和触发次数不受我们自己控制。

Promise的出现很好的解决了这些问题。Promise通过链式调用解决了嵌套回调;通过控制反转解决信任问题。如果fetchAPI是基于Promise实现的,上面的代码可以变成:

fetchAPI("/target-a")
  .then((a) => fetchAPI(`/target-b?a=${a}`))
  .then((b) => fetchAPI(`/target-c?b=${b}`))
  .then((c) => fetchAPI(`/target-d?c=${c}`))
  .then((d) => {
    console.log("result", d);
  });

从零实现Promise

如果你对Promise的使用还不熟悉,建议先查看:阮一峰的Promise教程,以及Promise/A+规范

本文相关代码均在toy-promise仓库

我们先来看一个简单的Promise例子:

console.log("start");

const p1 = new Promise((resolve, reject) => {
  resolve("data");
});

p1.then((data) => {
  console.log("result:", data);
});

console.log("end");

输出结果为:

start
end
result: data

根据Promise/A+规范,Promise的基本特征是:

  1. 构造Promise对象时,需要一个executor执行函数,该函数接受两个参数,分别是resolvereject,并且在构建时立即执行

  2. Promise有三种状态:pendingfulfilledrejected;Promise的默认状态是pending,且Promise的状态只能是从pendingfulfilled,或者pendingrejected,一旦状态改变确认,就不会再发生改变

  3. Promise使用value保存成功时的结果,使用reason保存失败时的结果

  4. Promise必须具有then方法,该方法接受两个参数,分别是成功时执行的onFulfilled回调,以及失败时执行的onRejected回调;

  5. 调用then时,如果Promise已经成功,则执行onFulfilled回调,其参数是value;如果Promise已经失败,则执行onRejected回调,其参数是reason

promise 根据这些规则,我们可以先试着构建一个最基础的版本:

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

class ToyPromise {
  constructor(executor) {
    // Promise默认状态是pending
    this.status = PENDING;

    // 用于存放成功的值
    this.value = undefined;

    // 用于存放失败的原因
    this.reason = undefined;

    try {
      // 立即执行executor
      executor(this.resolve, this.reject);
    } catch (error) {
      // 如果执行executor发生错误,这把该Promise置为失败
      this.reject(error);
    }
  }

  // resolve函数需要记住this值,所以使用箭头函数
  resolve = (value) => {
    // 如果不是pending状态,说明该Promise已经结束,则提前退出,防止多次调用resolve方法
    if (this.status !== PENDING) {
      return;
    }

    this.status = FULFILLED;
    this.value = value;
  };

  // reject函数需要记住this值,所以使用箭头函数
  reject = (reason) => {
    // 如果不是pending状态,说明该Promise已经结束,则提前退出,防止多次调用reject方法
    if (this.status !== PENDING) {
      return;
    }

    this.status = REJECTED;
    this.reason = reason;
  };

  then(onFulfilled, onRejected) {
    if (this.status === FULFILLED) {
      onFulfilled(this.value);
    } else if (this.status === REJECTED) {
      onRejected(this.reason);
    }
  }
}

写点代码测试一下:

const p1 = new ToyPromise((resolve) => {
  resolve("mock data");
});

p1.then(
  (data) => console.log("success", data),
  (error) => console.log("fail", error)
);

const p2 = new ToyPromise((resolve, reject) => {
  reject("error");
});

p2.then(
  (data) => console.log("success", data),
  (error) => console.log("fail", error)
);

输出结果为:

"success mock data"
"fail error"

到现在为止,同步版Promise已经实现了,但现在的版本还不支持异步操作:

const p1 = new ToyPromise((resolve) => {
  setTimeout(() => {
    resolve("mock data");
  }, 1000);
});

p1.then(
  (data) => console.log("success", data),
  (error) => console.log("fail", error)
);

上面的代码并不会输出任何结果,因为在执行then方法的时候,Promise的状态还是pending,自然不会执行任何回调。

所以,解决思路就是在调用then方法时,如果当前状态是pending,那我们需要先把回调函数存起来,等到Promise状态改变后,再执行相应的回调函数,根据这个思路再继续优化代码:

class ToyPromise {
  constructor(executor) {
    // Promise默认状态是pending
    this.status = PENDING;

    // 用于存放成功的值
    this.value = undefined;
    // 用于存放成功的回调
    this.onFulfilledQueue = [];

    // 用于存放失败的原因
		this.reason = undefined;
    // 用于存放失败的回调
    this.onRejectedQueue = [];

    try {
      // 立即执行executor
      executor(this.resolve, this.reject);
    } catch (error) {
      // 如果执行executor发生错误,这把该Promise置为失败
      this.reject(error);
    }
  }

  // resolve函数需要记住this值,所以使用箭头函数
  resolve = (value) => {
    // 如果不是pending状态,说明该Promise已经结束,则提前退出,防止多次调用resolve方法
    if (this.status !== PENDING) {
      return;
    }

    this.status = FULFILLED;
    this.value = value;
    // 依次执行相应的回调
    this.onFulfilledQueue.forEach((fn) => fn(value));
  };

  // reject函数需要记住this值,所以使用箭头函数
  reject = (reason) => {
    // 如果不是pending状态,说明该Promise已经结束,则提前退出,防止多次调用reject方法
    if (this.status !== PENDING) {
      return;
    }

    this.status = REJECTED;
    this.reason = reason;
    // 依次执行相应的回调
    this.onRejectedQueue.forEach((fn) => fn(reason));
  };

  then(onFulfilled, onRejected) {
    // 如果状态是pending,则先将两个回调存起来
    if (this.status === PENDING) {
      this.onFulfilledQueue.push(onFulfilled);
      this.onRejectedQueue.push(onRejected);
    } else if (this.status === FULFILLED) {
      onFulfilled(this.value);
    } else {
      onRejected(this.reason);
    }
  }
}

再用之前的异步代码测试一下,控制台将在1s后输出"success mock data"

then链式调用

前面提到,Promise通过链式调用消除了回调地狱,当我们使用了then方法后,还可以继续调用then方法。在Promise/A+规范中规定:

  1. Promise的then方法可以被同一个Promise多次调用,且每次都会返回一个新的Promise对象:p2=p1.then(onFulfilled,onRejected)
  2. 如果then方法中的回调函数onFulfilled或者onRejected执行时抛出异常,那么新的Promise会失败,并且把这个异常作为失败的reason
  3. 如果then方法中的回调函数onFulfilled或者onRejected执行时返回结果x
    1. 如果x是Promise,或者thenable对象
      1. 如果x和新的Promise引用相同,则抛出TypeError
      2. 否则新Promise的最终结果由该对象完成后的状态决定,如果该对象最终执行成功,则新的Promise也成功,如果该对象最终执行失败或者执行过程中抛出异常,则新的Promise也失败
    2. 如果x是其他普通值,那么新的Promise也成功,且成功的valuex

我们继续来完善then方法:

then(onFulfilled, onRejected) {
    // 返回一个新的Promise
    const p = new ToyPromise((resolve, reject) => {
      // 如果状态是pending,则先将两个回调存起来
      if (this.status === PENDING) {
        this.onFulfilledQueue.push((value) => {
          setTimeout(() => {
            try {
              const x = onFulfilled(value);
              // x可能是一个Promise或者thenable对象
              resolvePromise(p, x, resolve, reject);
            } catch (error) {
              reject(error);
            }
          });
        });
        this.onRejectedQueue.push((reason) => {
          setTimeout(() => {
            try {
              const x = onRejected(reason);
              resolvePromise(p, x, resolve, reject);
            } catch (error) {
              reject(error);
            }
          });
        });
      } else if (this.status === FULFILLED) {
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value);
            resolvePromise(p, x, resolve, reject);
          } catch (error) {
            reject(error);
          }
        });
      } else {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason);
            resolvePromise(p, x, resolve, reject);
          } catch (error) {
            reject(error);
          }
        });
      }
    });

    return p;
  }
}

function resolvePromise(promise, x, resolve, reject) {
  // 如果promise和x是同一个对象,那么就会出现自己等待自己完成的问题
  if (promise === x) {
    return reject(TypeError("Chaining cycle detected"));
  }

  // 规范中规定resolve和reject只能调用一次,使用called作为是否promise已经完成的标识
  // 虽然多次调用也没有问题
  let called = false;
  if ((x && typeof x === "object") || typeof x === "function") {
    let then;
    try {
      //将这句代码放入try/catch中,用于处理对象的设置了then的get操作符直接抛出错误的情况
      then = x.then;
      if (typeof then === "function") {
        then.call(
          x,
          (value) => {
            if (called) {
              return;
            }
            called = true;
            // 继续递归解析
            resolvePromise(promise, value, resolve, reject);
          },
          (reason) => {
            if (called) {
              return;
            }
            called = true;
            reject(reason);
          }
        );
      } else {
        if (called) {
          return;
        }
        called = true;
        // 如果then不是一个函数,则直接resolve
        resolve(x);
      }
    } catch (error) {
      if (called) {
        return;
      }
      called = true;
      reject(error);
    }
  } else {
    if (called) {
      return;
    }
    called = true;
    // 如果x是一个普通值,则直接resolve
    resolve(x);
  }
}

Promise/A+规范中提到,onFulfilledonRejected回调函数需要异步执行,原生Promise是通过创建一个微任务异步执行回调;我们这里则是通过setTimeout生成一个宏任务执行回调。

then值穿透

Promise的then方法还可以不传入参数,如promise.then().then().then(value=>{}),后续的then依旧可以得到前面的结果。这就是所谓的值穿透,要做到这一点很简单,我们只需要在then方法中增加对回调函数的判断处理即可:

then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (value) => value;
    onRejected =
      typeof onRejected === "function"
        ? onRejected
        : (reason) => {
            throw reason;
          };
  // ......
}

到此为止,我们就完成了Promise最核心的部分。写点代码测试一下:

const p1 = new ToyPromise((resolve) => {
  setTimeout(() => {
    resolve("mock data");
  }, 1000);
});

p1.then()
  .then()
  .then((value) => {
    console.log("success", value);
  });

控制台将在1s后输出"success mock data"

Resolve Promise

这里补充一点规范中没有提及的内容,如果执行executor函数时,给resolve回调传递的参数也是一个Promise或者thenable的对象,那么Promise的结果由该对象完成后的状态决定。处理方式和then方法中回调返回值是Promise或者thenable的对象时处理方法一致。因此我们可以复用之前的resolvePromise函数:

class ToyPromise {
  // resolve函数需要记住this值,所以使用箭头函数
  resolve = (value) => {
    // 此时第一个参数直接赋值为undefined即可
    resolvePromise(
      undefined,
      value,
      (x) => {
        // 如果不是pending状态,说明该Promise已经结束,则提前退出,防止多次调用resolve方法
        if (this.status !== PENDING) {
          return;
        }
        this.status = FULFILLED;
        this.value = x;
        // 依次执行相应的回调
        this.onFulfilledQueue.forEach((fn) => fn(x));
      },
      this.reject
    );
  };
	// ......
};

function resolvePromise(promise, x, resolve, reject) {
  // 如果promise和x是同一个对象,那么就会出现自己等待自己完成的问题
  // 如果是通过resolve调用,promise为undefined,需要将这种情况排除
  if (promise && promise === x) {
    return reject(TypeError("Chaining cycle detected"));
  }
  // ......
}

写点代码测试一下:

const p = new ToyPromise((resolve) => {
  resolve(
    new ToyPromise((resolve) => {
      resolve({
        then: (resolve) => {
          resolve("data");
        },
      });
    })
  );
});
p.then(
  (value) => console.log(value),
  (error) => console.warn(error)
);

控制台将输出"data"

继续完善Promise

catch

catch方法可以用来捕获Promise抛出的异常,它本身就是一个then的语法糖:

  catch(onRejected) {
    return this.then(undefined, onRejected);
  }

写点代码测试一下:

new ToyPromise((resolve, reject) => {
  reject("error");
}).catch((error) => console.warn(error));

控制台将输出"error"

finally

finally方法不管在Promise对象最后是什么状态都会执行,且finally的回调函数不接受任何参数。只要finally的回调函数本身不抛出异常或者返回一个失败的Promise,那么finally方法总是会返回和之前Promise相同的结果,本质上也是then的语法糖:

 finally(onResolved) {
    return this.then(
      (value) => ToyPromise.resolve(onResolved()).then(() => value),
      (reason) =>
        ToyPromise.resolve(onResolved()).then(() => {
          throw reason;
        })
    );
  }

写点代码测试一下:

ToyPromise.resolve("success")
  .finally(() => {
    console.log("finally");
  })
  .then((value) => {
    console.log(value);
  });

ToyPromise.resolve("success")
  .finally(() => {
    return ToyPromise.resolve("data");
  })
  .then((value) => {
    console.log(value);
  });

ToyPromise.resolve("success")
  .finally(() => {
    return ToyPromise.reject("error");
  })
  .catch((error) => {
    console.log(error);
  });

控制台将输出:

"finally"
"success"
"success"
"error"

静态方法resolve

静态方法resolve可以用于将一个任意类型的对象转换为Promise对象:

 static resolve(value) {
    return new ToyPromise((resolve) => {
      resolve(value);
    });
  }

写点代码测试一下:

ToyPromise.resolve("a").then((value) => {
  console.log(value);
});

ToyPromise.resolve(
  new ToyPromise((resolve) => {
    setTimeout(() => {
      resolve("b");
    }, 1000);
  })
).then((value) => {
  console.log(value);
});

控制台将首先输出"a",然后1s后输出"b"

静态方法reject

静态方法reject可以用于生成一个失败的Promise,reject方法并不关心所传递的对象是不是Promise或者thenable对象:

static reject(reason) {
    return new ToyPromise((resolve, reject) => {
      reject(reason);
    });
  }

写点代码测试一下:

ToyPromise.reject("a").catch((error) => {
  console.log(value);
});

const p = new ToyPromise((resolve) => {
  setTimeout(() => {
    resolve("b");
  }, 1000);
});
ToyPromise.reject(p).catch((error) => {
  console.log(error);
});

控制台将首先输出"a",然后1s后输出Promise对象p

静态方法all

all方法可以用于将多个Promise实例包装成为一个新的Promise,并且只有所有Promise成功时,新的Promise才成功,否则任意Promise失败,新的Promise即为失败:

static all(promises) {
    if (!Array.isArray(promises)) {
      return TypeError(`TypeError: ${promises} is not array`);
    }
    return new ToyPromise((resolve, reject) => {
      const result = [];
      let resolvedCount = 0;

      // 空数组判断,防止Promise永远不结束
      if (promises.length === 0) {
        resolve(result);
      }
      
      promises.forEach((promise, idx) => {
        // 将所有对象首先转换为Promise再统一处理
        ToyPromise.resolve(promise).then((value) => {
          result[idx] = value;
          if (++resolvedCount === promises.length) {
            resolve(result);
          }
        }, reject);
      });
    });
  }

写点代码测试一下:

const p1 = new ToyPromise((resolve) => {
  setTimeout(() => {
    resolve("p1");
  }, 1000);
});

const p2 = new ToyPromise((resolve) => {
  setTimeout(() => {
    resolve("p2");
  }, 2000);
});

const p3 = ToyPromise.resolve("p3");

ToyPromise.all([p1, p2, p3, "p4"]).then((result) => {
  console.log(result);
});

控制台将在2s后输出[ "p1", "p2", "p3", "p4" ]

静态方法race

race方法同样用于将多个Promise实例包装成为一个新的Promise,但新的Promise状态由最快完成的Promise决定:

static race(promises) {
    if (!Array.isArray(promises)) {
      return TypeError(`TypeError: ${promises} is not array`);
    }
    return new ToyPromise((resolve, reject) => {
      if (promises.length === 0) {
        resolve(undefined);
      }
      
      promises.forEach((promise) => {
        ToyPromise.resolve(promise).then(resolve, reject);
      });
    });
  }

写点代码测试一下:

const p1 = new ToyPromise((resolve) => {
  setTimeout(() => {
    resolve("p1");
  }, 1000);
});

const p2 = new ToyPromise((resolve) => {
  setTimeout(() => {
    resolve("p2");
  }, 2000);
});

ToyPromise.race([p1, p2]).then((result) => {
  console.log(result);
});

控制台将在1s后输出"p1"

静态方法allSettled

该方法由ES2020引入

allSettled方法同样用于将多个Promise实例包装成为一个新的Promise,新的Promise会等到所有Promise都完成后(无论成功或失败)才完成,且完成状态总为成功:

static allSettled(promises) {
    if (!Array.isArray(promises)) {
      return TypeError(`TypeError: ${promises} is not array`);
    }
    return new ToyPromise((resolve, reject) => {
      const result = [];
      let resolvedCount = 0;

      const saveResult = (data, idx) => {
        result[idx] = data;
        if (++resolvedCount === promises.length) {
          resolve(result);
        }
      };

      if (promises.length === 0) {
        resolve(result);
      }
      
      promises.forEach((promise, idx) => {
        ToyPromise.resolve(promise).then(
          (value) => {
            saveResult(value, idx);
          },
          (reason) => {
            saveResult(reason, idx);
          }
        );
      });
    });
  }

写点代码测试一下:

const p1 = new ToyPromise((resolve) => {
  setTimeout(() => {
    resolve("p1");
  }, 1000);
});

const p2 = new ToyPromise((resolve, reject) => {
  setTimeout(() => {
    reject("error");
  }, 2000);
});

ToyPromise.allSettled([p1, p2]).then((result) => {
  console.log(result);
});

控制台将在2s后输出[ "p1", "error" ]

静态方法any

该方法由ES2021引入

any方法同样用于将多个Promise实例包装成为一个新的Promise,它的处理方式和all刚好相反,只有所有Promise失败时,新的Promise才失败,否则任意Promise成功,新的Promise即为成功:

static any(promises) {
    if (!Array.isArray(promises)) {
      return TypeError(`TypeError: ${promises} is not array`);
    }
    return new ToyPromise((resolve, reject) => {
      const result = [];
      let rejectedCount = 0;

      if (promises.length === 0) {
        resolve(undefined);
      }

      promises.forEach((promise, idx) => {
        // 将所有对象首先转换为Promise再统一处理
        ToyPromise.resolve(promise).then(resolve, (reason) => {
          result[idx] = reason;
          if (++rejectedCount === promises.length) {
            reject(result);
          }
        });
      });
    });
  }

写点代码测试一下:

const p1 = new ToyPromise((resolve) => {
  setTimeout(() => {
    resolve("p1");
  }, 1000);
});

const p2 = new ToyPromise((resolve, reject) => {
  setTimeout(() => {
    reject("error");
  }, 2000);
});

ToyPromise.any([p1, p2]).then((result) => {
  console.log(result);
});

控制台将在1s后输出"p1"

测试Promise

我们可以通过promises-aplus-tests这个库来测试我们自己写的Promise是否符合规范。该库需要Promise暴露一个deferred静态方法,为了不影响原本代码,我们单独声明一个Adapter文件:

class Adapter extends ToyPromise {
  static deferred() {
    const result = {};
    result.promise = new Adapter((resolve, reject) => {
      result.resolve = resolve;
      result.reject = reject;
    });

    return result;
  }
}
module.exports = Adapter;

执行命令promises-aplus-tests path/adapter.js(其中path/adapter.js是具体Adapter文件所在的位置),就能看到测试结果。

promises-aplus-tests共有872条测试用例,测试只覆盖Promise/A+规范中要求的部分,所以只要求Promise必须包含then方法,并不关心其他的catch或者all等方法。

如果你下载了toy-promise仓库,则直接执行npm test即可。

如果对本文有什么意见和建议,欢迎讨论和指正!