一位不愿意透露姓名的顶级摸鱼工程师曾经说过,学习 Promise 最好的方式就是先阅读它的规范定义。那么哪里可以找到 Promise 的标准定义呢?
答案是 Promises/A+ 规范。
假设你已经打开了上述的规范定义的页面并尝试开始阅读(不要因为是英文的就偷偷关掉,相信自己,你可以的),规范在开篇描述了 Promise 的定义,与之交互的方法,然后强调了规范的稳定性。关于稳定性,换言之就是:我们可能会修订这份规范,但是保证改动微小且向下兼容,所以放心地学吧,这就是权威标准,五十年之后你再去谷歌 Promise,出来的规范还是这篇 ?。
好的,让我们回到规范。从开篇的介绍看,到底什么是 Promise ?
A promise represents the eventual result of an asynchronous operation.
Promise 表示一个异步操作的最终结果。
划重点!!这里其实引出了 JavaScript 引入 Promise 的动机:异步。
学习一门新技术,最好的方式是先了解它是如何诞生的,以及它所解决的问题是什么。Promise 跟我们说的异步编程有什么联系呢?Promise 到底解决了什么问题?
要回答这些问题,我们需要先回顾下没有 Promise 之前,异步编程存在什么问题?
异步编程
JavaScript 的异步编程跟浏览器的事件循环息息相关,网上有很多的文章或专栏介绍了浏览器的事件循环机制,如果你还不了解,可以先阅读下面的文章,
假设你已经了解了事件循环,接下来我们来看异步编程存在什么问题?
由于 Web 页面的单线程架构,决定了 JavaScript 的异步编程模型是基于消息队列(Message Queue)和事件循环(Event Loop)的,就像下面这样,
我们的异步任务的回调函数会被放入消息队列,然后等待主线程上的同步任务执行完成,执行栈为空时,由事件循环机制调度进执行栈继续执行。
这导致了 JavaScript 异步编程的一大特点:异步回调,比如网络请求,
// 成功的异步回调函数
function resolve(response) {
console.log(response);
}
// 失败的异步回调函数
function reject(error) {
console.log(error);
}
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => resolve(xhr.response);
xhr.ontimeout = (e) => reject(e);
xhr.onerror = (e) => reject(e);
xhr.open("Get", "http://xxx");
xhr.send();
复制代码
虽然可以通过简单的封装使得异步回调的方式变得优雅,比如,
$.ajax({
url: "https://xxx",
method: "GET",
fail: () => {},
success: () => {},
});
复制代码
但是仍然没有办法解决业务复杂后的“回调地狱”的问题,比如多个依赖请求,
$.ajax({
success: function (res1) {
$.ajax({
success: function (res2) {
$.ajax({
success: function (res3) {
// do something...
},
});
},
});
},
});
复制代码
这种线性的嵌套回调使得异步代码变得难以理解和维护,也给人很大的心智负担。
所以我们需要一种技术,来解决异步编程风格的问题,这就是 Promise 的动机。
了解 Promise 背景和动机有利于我们理解规范,现在让我们重新回到规范的定义。
规范
Promise A+ 规范首先定义了 Promise 的一些相关术语和状态。
Terminology,术语
- “promise” ,一个拥有
then
方法的对象或函数,其行为符合本规范 - “thenable”,一个定义了
then
方法的对象或函数 - “value”,任何 JavaScript 合法值(包括
undefined
,thenable
和promise
) - “exception”,使用
throw
语句抛出的一个值 - “reason”,表示一个
promise
的拒绝原因
State,状态
promise 的当前状态必须为以下三种状态之一:Pending
, Fulfilled
, Rejected
- 处于 Pending 时,promise 可以迁移至 Fullfilled 或 Rejected
- 处于 Fulfilled 时,promise 必须拥有一个不可变的终值且不能迁移至其他状态
- 处于 Rejected 时,promise 必须拥有一个不可变的拒绝原因且不能迁移至其他状态
所以 Promise 内部其实维护了一个类似下图所示的状态机,
Promise 在创建时处于 Pending(等待态),之后可以变为 Fulfilled(执行态)或者 Rejected(拒绝态),一个承诺要么被兑现,要么被拒绝,这一过程是不可逆的。
定义了相关的术语和状态后,是对 then
方法执行过程的详细描述。
Then
一个 promise 必须提供一个 then
方法以访问其当前值、终值和拒绝原因。
then
方法接受两个参数,
promise.then(onFulfilled, onRejected);
复制代码
- onFulfilled,在 promise 执行结束后调用,第一个参数为 promise 的终值
- onRejected,在 promise 被拒绝执行后调用,第一个参数为 promise 的拒绝原因
对于这两个回调参数和 then
的调用及返回值,有如下的一些规则,
-
onFulfilled 和 onRejected 都是可选参数。
-
onFulfilled 和 onRejected 必须作为函数被调用,调用的
this
应用默认绑定规则,也就是在严格环境下,this
等于undefined
,非严格模式下是全局对象(浏览器中就是window
)。关于this
的绑定规则如果不了解的可以参考我之前的一篇文章 《可能是最好的 this 解析了…》,里面有非常详细地介绍。 -
onFulfilled 和 onRejected 只有在执行环境堆栈仅包含平台代码时才可被调用。由于 promise 的实施代码本身就是平台代码(JavaScript),这个规则可以这么理解:就是要确保这两个回调在 then 方法被调用的那一轮事件循环之后异步执行。这不就是微任务的执行顺序吗?所以 promise 的实现原理是基于微任务队列的。
-
then
方法可以被同一个 promise 调用多次,而且所有的成功或拒绝的回调需按照其注册顺序依次回调。所以 promise 的实现需要支持链式调用,可以先想一下怎么支持链式调用,稍后我们会有对应的实现。 -
then
方法必须返回一个 promise 对象。
针对第 5 点,还有如下几条扩展定义,我们将返回值与 promise 的解决过程结合起来,
promise2 = promise1.then(onFulfilled, onRejected);
复制代码
then
的两个回调参数可能会抛出异常或返回一个值,
5.1 如果 onFulfilled 或者 onRejected 抛出一个异常 e
,那么返回的 promise2 必须拒绝执行,并返回拒绝的原因 e
。
5.2 如果 onFulfilled 或者 onRejected 返回了一个值 x
,会执行 promise 的解决过程
- 如果
x
和返回的 promise2 相等,也就是 promise2 和x
指向同一对象时,以TypeError
作为拒绝的原因拒绝执行 promise2 - 如果
x
是 promise,会判断x
的状态。如果是等待态,保持;如果是执行态,用相同的值执行 promise2;如果是拒绝态,用相同的拒绝原因拒绝 promise2 - 如果
x
是对象或者函数,将x.then
赋值给then
;如果取x.then
的值时抛出错误e
,则以e
为拒绝原因拒绝 promise2。如果then
是函数,将x
作为函数的this
,并传递两个回调函数 resolvePromise, rejectPromise 作为参数调用函数
读到这里,相信你跟我一样已经迫不及待想要实现一个 Promise 了,既然了解了原理和定义,我们就来手写一个 Promise 吧。
手写 Promise
const PENDING = "PENDING";
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";
function resolve(value) {
return value;
}
function reject(err) {
throw err;
}
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(
new TypeError("Chaining cycle detected for promise #<Promise>")
);
}
let called;
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(promise2, 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);
}
}
class Promise {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
this.resolveCallbacks = [];
this.rejectCallbacks = [];
let resolve = (value) => {
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
this.resolveCallbacks.forEach((fn) => fn());
}
};
let reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
this.rejectCallbacks.forEach((fn) => fn());
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : resolve;
onRejected = typeof onRejected === "function" ? onRejected : reject;
let promise2 = new Promise((resolve, reject) => {
if (this.status === FULFILLED) {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
}
if (this.status === REJECTED) {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
}
if (this.status === PENDING) {
this.resolveCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
this.rejectCallbacks.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
}
});
return promise2;
}
}
复制代码
小结
我们从 Promise A+ 规范作为切入点,先探索了 Promise 诞生的背景和动机,了解了异步编程的发展历史,然后回到规范精读了其中对于相关术语,状态及执行过程的定义,最后尝试了简版的 Promise 实现。最新的 《JavaScript高级程序设计(第4版)》 中,将 Promise 翻译为 “承诺”,作为现代 JavaScript 异步编程的方案,Promise 通过回调函数延迟绑定、回调函数返回值穿透和错误“冒泡”等技术解决了多层嵌套的问题,规范了对异步任务的处理结果(成功或失败)的统一处理。
参考链接
- Promises/A+ Spec
- 消息队列和事件循环:页面是怎么“活”起来的?
- Promise – JavaScript | MDN
- ECMAScript® 2022 Language Specification – TC39
- 面试官:“你能手写一个 Promise 吗”
写在最后
本文首发于我的 博客,才疏学浅,难免有错误,文章有误之处还望不吝指正!
如果有疑问或者发现错误,可以在相应的 issues 进行提问或勘误
如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励
(完)