在我们使用 JavaScript 或者 TypeScript 写代码的时候,多多少少都会用到 Promise
。可以说,Promise
在 JavaScript 或 TypeScript 的代码中无处不在,比如我们熟悉的 axios
和 fetch
,async/await
语法等,都是借助 Promise
来实现的,可见 Promise
是多么的重要。
有编程经验的朋友应该已经很熟悉 Promise
的功能了,但这里我还是简单地介绍一下 Promise
的概念以及 Promise
解决了什么问题。
Promise 的简单介绍
Promise
是用来进行异步编程的。在 Promise
出现之前,传统的异步编程靠的是监听事件和回调函数,比如:
js
const xhr = new XMLHttpRequest();
xhr.addEventListener("load", function(data) {
console.log(data);
});
xhr.open("GET", "http://www.example.com/");
xhr.send();
这种方式有一个致命的缺点,那就是回调地狱,如果我们想在第一个请求成功返回数据之后,再发送第二个请求,那就只能在第一个请求成功的回调函数中发送,以此类推,还想再发送下一个请求时,就只能在上一个请求成功的回调函数中发送:
js
const xhr = new XMLHttpRequest();
xhr.addEventListener("load", function(data) {
console.log(data);
const xhr1 = new XMLHttpRequest();
xhr1.open("GET", "http://www.example.com/");
xhr1.send();
xhr1.addEventListener("load", function(data1) {
console.log(data1)
//... 发送第三个请求
});
});
xhr.open("GET", "http://www.example.com/");
xhr.send();
这种代码不仅看起来很痛苦,写起来也非常痛苦。而 Promise
就是用来解决这个问题的,可以把 Promise
理解成一个容器,里面保存着某个未来才会结束的事件的结果(也就是异步操作的结果),它有三种状态:pending
(进行中)、fulfilled
(已成功)和 rejected
(已失败),状态改变只有两种:从 pending
变为 fulfilled
或从 pending
变为 rejected
,一旦状态改变,就不会再变了。其中,可以分别通过 resolve
函数使得状态变为 fulfilled
,reject
函数使得状态变为 rejected
。
我们使用 Promise
来简单改造一下上述的多个请求方式:
js
function request(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.addEventListener("load", function (data) {
console.log(data);
resolve();
});
xhr.open("GET", url);
xhr.send();
});
}
// 发送第一个请求
request("http://www.example.com/").then(() => {
// 发送第二个请求
return request("http://www.example1.com/");
}).then(() => {
// 发送第三个请求
return request("http://www.example2.com/");
});
可以看到,有了 Promise
的帮助,代码完全脱离了回调地狱的嵌套,以一种流水线的方式来承接发送请求的功能,代码结构上更加清晰。
更多关于
Promise
的介绍可以参考阮一峰老师的《ECMAScript 6入门》中的 Promise 对象。
捉摸不透的 Promise 输出顺序
简单地熟悉了 Promise
的功能之后,我们再来看看这三种 Promise
的输出顺序:
js
Promise.resolve()
.then(() => {
console.log(0);
return 4;
})
.then((res) => {
console.log(res);
});
Promise.resolve()
.then(() => {
console.log(1);
})
.then(() => {
console.log(2);
})
.then(() => {
console.log(3);
})
.then(() => {
console.log(5);
});
// 0 1 4 2 3 5
js
Promise.resolve()
.then(() => {
console.log(0);
return {
then(resolve) {
resolve(4);
},
};
})
.then((res) => {
console.log(res);
});
Promise.resolve()
.then(() => {
console.log(1);
})
.then(() => {
console.log(2);
})
.then(() => {
console.log(3);
})
.then(() => {
console.log(5);
});
// 0 1 2 4 3 5
js
Promise.resolve()
.then(() => {
console.log(0);
return Promise.resolve(4);
})
.then((value) => {
console.log(value);
});
Promise.resolve()
.then(() => {
console.log(1);
})
.then(() => {
console.log(2);
})
.then(() => {
console.log(3);
})
.then(() => {
console.log(5);
});
// 0 1 2 3 4 5
这三种 Promise
的输出顺序不同之处在于 4 的位置,而决定 4 的位置的关键代码在于第一个 then
方法回调函数的返回值。
尤其是第二种,大家可能比较少见,返回的是一个具有 then
方法的对象,这个对象被称之为 thenable
对象,这个对象的 then
方法接收两个参数,resolve
和 reject
,它们都是函数,其含义和 Promise
构造函数中的 resolve
和 reject
一样。所以后面紧跟的链式 then
方法能够接收到 4 这个值。
要想彻底搞明白这三种 Promise
的输出顺序,我们就得来手动实现一遍 Promise
的原理,看看里面到底有啥法宝。
Promise 源码实现
基础功能
万事开头难,为了让大家更有兴趣地往下看,我们先简单地实现 Promise
的基础功能。
先看一个基础功能的例子,如下:
js
const p = new Promise((resolve, reject) => {
resolve('fulfilled');
reject('rejected');
})
p.then(value => {
console.log(value);
}, reason => {
console.log(reason);
})
// fulfilled
我们来分析一下这段代码都做了什么:
Promise
构造函数会传入一个回调函数作为参数,回调函数又会接收两个参数 ---resolve
和reject
,它们都是函数,前文说过,resolve
和reject
是用来改变Promise
实例对象的状态。- 一旦状态改变,就不会再改变了。状态改变之后,会调用
then
方法的两个回调函数中的一个,并且会相对应地接收resolve
或reject
函数的参数值。 fulfilled
状态下会执行then
方法的第一个回调函数,rejected
状态会执行第二个回调函数。
理解了这段代码的执行逻辑之后,我们分两步来实现 Promise
的基础功能,分别是: new Promise
的实现原理和 then
方法的实现原理。
new Promise 的实现原理
js
// 定义状态
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
class SelfPromise {
// 储存状态,初始值是 pending
status = PENDING;
// 成功之后的值
value = null;
// 失败之后的原因
reason = null;
constructor(executor) {
// 将 resolve 和 reject 传给 new Promsie 的回调函数
executor(this.resolve, this.reject);
}
// 箭头函数可以使函数里面的 this 始终指向 Promise 实例对象
resolve = (value) => {
// 只有状态是 pending 的情况下,才改变为 fulfilled 状态
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
}
};
reject = (reason) => {
// 只有状态是 pending 的情况下,才改变为 rejected 状态
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
}
};
}
这里有两个注意点:
resolve
和reject
方法只有是在箭头函数的情况下,才能直接传递给executor
函数作为参数(executor(this.resolve, this.reject)
),这样在外部调用resolve
或reject
函数的时候,它们的this
指向始终是Promise
实例对象。那改为普通函数可不可以呢?也可以,不过就是需要使用bind
方法来稳定resolve
和reject
的this
指向 ---executor(this.resolve.bind(this), this.reject.bind(this))
。resolve
和reject
只有在pending
状态下,才需要改变状态和记录结果,这样就达到了Promise
状态一旦改变就不能再改变的效果。
then 方法的实现原理
js
//... 省略部分代码
class SelfPromise {
//... 省略部分代码
then(onFulfilled, onRejected) {
if (this.status === FULFILLED) {
// 把 resolve 的值传递给 fulfilled 状态的回调函数,并且调用它。
onFulfilled(this.value);
} else if (this.status === REJECTED) {
// 把 reject 的值传递给 rejected 状态的回调函数,并且调用它。
onRejected(this.reason);
}
}
}
then
方法的实现相对来说比较简单,只需要根据状态调用相应的回调函数即可。
基础功能的完整代码
js
// 定义状态
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
class SelfPromise {
// 储存状态,初始值是 pending
status = PENDING;
// 成功之后的值
value = null;
// 失败之后的原因
reason = null;
constructor(executor) {
// 将 resolve 和 reject 传给 new Promsie 的回调函数
executor(this.resolve, this.reject);
}
// 箭头函数可以函数里面的 this 始终指向 Promise 实例对象
resolve = (value) => {
// 只有状态是 pending 的情况下,才改变为 fulfilled 状态
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
}
};
reject = (reason) => {
// 只有状态是 pending 的情况下,才改变为 rejected 状态
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
}
};
then(onFulfilled, onRejected) {
if (this.status === FULFILLED) {
// 把 resolve 的值传递给 fulfilled 状态的回调函数,并且调用它。
onFulfilled(this.value);
} else if (this.status === REJECTED) {
// 把 reject 的值传递给 rejected 状态的回调函数,并且调用它。
onRejected(this.reason);
}
}
}
用一开始的 Promise
基础功能的例子来测试一下:
js
const p = new SelfPromise((resolve, reject) => {
resolve("fulfilled");
reject("rejected");
});
p.then(value => {
console.log(value);
}, reason => {
console.log(reason);
})
// fulfilled
撒花~ 完美!Promise
基础功能的实现原理顺利完成。
处理异步逻辑
基础版的 Promise
有一个很大的缺点,就是处理不了异步的情况。
js
const p = new SelfPromise((resolve, reject) => {
setTimeout(() => {
resolve("fulfilled");
});
});
p.then(value => {
console.log(value);
}, reason => {
console.log(reason);
})
// 不会输出任何信息
由于使用了 setTimeout
执行 resolve
函数,导致 then
方法执行比 resolve
函数要早,所以 then
方法在执行的时候,Promise
的状态是 pending
,不会执行任何回调函数。
基于这种情况,我们需要在 then
方法中添加处理 pending
状态的逻辑。
实现思路是:如果在 then
方法中判断到状态是 pending
,那么就先将两个回调函数保存起来,然后在 Promise
内部的 resolve
或 reject
方法中执行。
js
//... 省略部分代码
class SelfPromise {
+ // 保存 onFulfilled 回调函数
+ onFulfilledCallback = null;
+ // 保存 onRejected 回调函数
+ onRejectedCallback = null;
//... 省略部分代码
resolve = (value) => {
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
+ // 执行 onFulfilled 回调函数
+ this.onFulfilledCallback && this.onFulfilledCallback(value);
}
};
reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
+ // 执行 onRejected 回调函数
+ this.onRejectedCallback && this.onRejectedCallback(reason);
}
};
then(onFulfilled, onRejected) {
if (this.status === FULFILLED) {
onFulfilled(this.value);
} else if (this.status === REJECTED) {
onRejected(this.reason);
+ } else {
+ // pending 状态下保存回调函数
+ this.onFulfilledCallback = onFulfilled;
+ this.onRejectedCallback = onRejected;
}
}
}
再运行一下用例,我们就可以打印出 fulfilled
信息了。
then 方法的多次调用
别忘了,Promise
实例对象的 then
方法是可以多次调用的,而我们现在的代码是无法做到这一点的:
js
const p = new SelfPromise((resolve, reject) => {
setTimeout(() => {
resolve("fulfilled");
});
});
p.then((value) => {
console.log("1", value);
});
p.then((value) => {
console.log("2", value);
});
p.then((value) => {
console.log("3", value);
});
// 3 fulfilled
目前的代码只能输出 3 fulfilled
,需要把 1 fulfilled
和 2 fulfilled
的输出补上。至于为什么只能输出 3 fulfilled
呢?关键在于源码当中 then
方法保存回调函数的方式:
js
class SelfPromise {
//... 省略部分代码
then(onFulfilled, onRejected) {
if (this.status === FULFILLED) {
onFulfilled(this.value);
} else if (this.status === REJECTED) {
onRejected(this.reason);
} else {
// pending 状态下保存回调函数
this.onFulfilledCallback = onFulfilled;
this.onRejectedCallback = onRejected;
}
}
}
回调函数会直接保存在 this.onFulfilledCallback
或 this.onRejectedCallback
中,这样就会导致保存的是最后一个 then
方法的回调函数,所以这里不能直接用两个变量来保存,而是用两个数组来保存所有的回调函数,同时 Promise
内部的 resolve
和 reject
方法也需要循环调用所有的回调函数。
js
//... 省略部分代码
class SelfPromise {
+ // 保存所有的 onFulfilled 回调函数
+ onFulfilledCallbacks = [];
+ // 保存所有的 onRejected 回调函数
+ onRejectedCallbacks = [];
//... 省略部分代码
resolve = (value) => {
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
+ // 执行所有的 onFulfilled 回调函数
+ this.onFulfilledCallbacks.forEach((fn) => fn(value));
}
};
reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
+ // 执行 onRejected 回调函数
+ this.onRejectedCallbacks.forEach((fn) => fn(reason));
}
};
then(onFulfilled, onRejected) {
if (this.status === FULFILLED) {
onFulfilled(this.value);
} else if (this.status === REJECTED) {
onRejected(this.reason);
+ } else {
+ // pending 状态下保存所有的回调函数
+ this.onFulfilledCallbacks.push(onFulfilled);
+ this.onRejectedCallbacks.push(onRejected);
}
}
}
细心的朋友应该发现了这其实是一个观察者模式 ,then
方法的 this.onFulfilledCallbacks.push(onFulfilled)
和 this.onRejectedCallbacks.push(onRejected)
就是在添加订阅者,而 resolve
和 reject
方法就是在通知所有的订阅者。
我们再运行一下用例,得出结果:
md
1 fulfilled
2 fulfilled
3 fulfilled
完美,令人满意的结果!
then 方法的链式调用
Promise
最核心的功能就是 then
方法的链式调用,这也是解决回调地狱的关键所在。就目前我们手动实现的代码来看,是不能够进行 then
方法的链式调用的,因为 then
方法没有任何返回值。
要想实现 then
方法的链式调用,then
方法必须返回 Promise
对象,并且下一个 then
方法的回调函数的参数会依赖上一个 then
方法的回调函数的返回值,这种依赖有两种情况:
-
如果返回的是
Promise
实例对象,那么下一个then
方法的回调函数会接收该实例对象的resolve
或reject
函数传入的值,比如:jsconst p = new Promise((resolve, reject) => { resolve(1); }); p.then((value) => { console.log(value); return new Promise((resolve, reject) => { resolve(2); // reject(2); }); }).then((value) => { console.log("fulfilled", value); }, (err) => { console.log("rejected", err); }); // 1 // fulfilled 2 // 如果调用的是 reject(2),那么返回的是: // 1 // rejected 2
-
如果返回的是
thenable
对象,会和第一种情况一样:jsconst p = new Promise((resolve, reject) => { resolve(1); }); p.then((value) => { console.log(value); return { then(resolve, reject) { resolve(2); // reject(2); } }; }).then((value) => { console.log("fulfilled", value); }, (err) => { console.log("rejected", err); }); // 1 // fulfilled 2 // 如果调用的是 reject(2),那么返回的是: // 1 // rejected 2
-
如果返回的是其他对象或者原始数据类型的值,那么下一个
then
方法的回调函数的参数会直接接收这个值,比如:jsconst p = new Promise((resolve, reject) => { resolve(1); }); p.then((value) => { console.log(value); return { value: 2, }; }).then((value) => { console.log("fulfilled", value); }); // 1 // fulfilled { value: 2 }
了解了 then
方法链式调用的基本情况之后,我们来动手实现一下 then
方法的链式调用。
首先,让 then
方法返回一个 Promise
对象:
js
class SelfPromise {
// ... 省略部分代码
then(onFulfilled, onRejected) {
+ const promise2 = new SelfPromise((resolve, reject) => {
if (this.status === FULFILLED) {
onFulfilled(this.value);
} else if (this.status === REJECTED) {
onRejected(this.reason);
} else {
this.onFulfilledCallbacks.push(onFulfilled);
this.onRejectedCallbacks.push(onRejected);
}
+ });
+ // 返回 Promise 对象
+ return promise2;
}
}
接着,通过 resolve
和 reject
函数来改变 promise2
对象的状态,并且建立上一个 then
方法与下一个 then
方法的依赖关系。
js
class SelfPromise {
// ... 省略部分代码
then(onFulfilled, onRejected) {
const promise2 = new SelfPromise((resolve, reject) => {
if (this.status === FULFILLED) {
+ // 获取上一个 then 方法的 fulfilled 回调函数的返回值
+ const v = onFulfilled(this.value);
+ // 根据返回值,改变 promise2 的状态,并建立与下一个 then 方法的关系
+ resolvePromise(v, resolve, reject);
} else if (this.status === REJECTED) {
+ // 获取上一个 then 方法的 rejected 回调函数的返回值
+ const v = onRejected(this.reason);
+ //根据返回值,改变 promise2 的状态,并建立与下一个 then 方法的关系
+ resolvePromise(v, resolve, reject);
} else {
this.onFulfilledCallbacks.push(onFulfilled);
this.onRejectedCallbacks.push(onRejected);
}
});
return promise2;
}
}
function resolvePromise(value, resolve, reject) {
if (typeof value === "object" || typeof value === "function") {
if (value === null) {
// 如果返回值是 null,
// 直接调用 resolve 函数,promise2 的状态变为 fulfilled,
// 返回值由下一个 then 方法的第一个回调函数接收。
return resolve(value);
}
try {
if (typeof value.then === "function") {
// 如果返回值是 Promise 对象或者 thenable 对象
// 那就只能交给它们的 then 方法来改变 promise2 的状态,以及获取相对应的状态值
// 以下代码等同于 value.then((value) => resolve(value), (err) => reject(err))
value.then(resolve, reject);
} else {
// 如果 then 不是函数,同 null 情况一样的处理逻辑。
resolve(value);
}
} catch (error) {
// 出现异常的情况下,调用 reject 函数
// promise2 的状态变为 rejected,
// 错误信息由下一个 then 方法的第二回调函数接收
reject(error);
}
} else {
// 如果返回值是其他对象或者原始数据类型值,同 null 情况一样的处理逻辑。
resolve(value);
}
}
这里比较难理解的应该是,返回值为 Promise
对象或者 thenable
对象的处理情况 --- value.then(resolve, reject)
,这段代码写完整一点就是 value.then((value) => resolve(value), (err) => reject(err))
,这里 then
方法的回调函数中 value
参数值和 err
参数值就是 Promise
对象或者 thenable
对象内部调用 resolve
或者 reject
函数传入的参数值,再把这些值传递给 promise2
的 resolve
和 reject
函数,从而达到改变 promise2
的状态,下一个 then
方法的回调函数也会被调用并且接收到这些值。
总得来说就是,promise2
的状态完全由返回值(Promise
对象或者 thenable
对象)来控制。就跟以下这段代码一样:
js
const promise2 = new SelfPromise((resolve2, reject2) => {
const value = new SelfPromise((resolve, reject) => {
resolve(1);
});
value.then(
(v) => resolve2(v),
(err) => reject2(err)
);
});
promise2.then((value) => {
console.log(value);
});
// 1
理解清楚上述的逻辑之后,执行一下测试用例。
js
const p = new SelfPromise((resolve, reject) => {
resolve(1);
});
js
p.then((value) => {
console.log(value);
return new SelfPromise((resolve) => {
resolve(2);
});
}).then((value) => {
console.log(value);
});
// 1
// 2
js
p.then((value) => {
console.log(value);
return {
then(resolve) {
resolve(2);
}
};
}).then((value) => {
console.log(value);
});
// 1
// 2
js
p.then((value) => {
console.log(value);
return 2;
};
}).then((value) => {
console.log(value);
});
// 1
// 2
令人满意的结果~
那如果 then
方法是返回自身的 Promise
对象该怎么办?我们来看看原生的 Promise
是怎么处理的:
js
const p = new Promise((resolve, reject) => {
resolve(1);
});
const p1 = p.then(value => {
console.log(value);
return p1;
});
// 1
// Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>
报错,错误信息是:会发生 Promise
循环调用。
所以,我们需要改造一下 SelfPromise
的代码,来模拟这种报错的效果:
js
function resolvePromise(promise2, value, resolve, reject) {
+ // 如果 then 方法返回的是自身 Promise 对象,返回错误信息
+ if (promise2 === value) {
+ return reject(new TypeError('Chaining cycle detected for promise #<Promise>'));
+ }
if (typeof value === "object" || typeof value === "function") {
if (value === null) {
return resolve(value);
}
try {
if (typeof value.then === "function") {
value.then(resolve, reject);
} else {
resolve(value);
}
} catch (error) {
reject(error);
}
} else {
resolve(value);
}
}
class SelfPromise {
// ... 省略部分代码
then(onFulfilled, onRejected) {
const promise2 = new SelfPromise((resolve, reject) => {
if (this.status === FULFILLED) {
const v = onFulfilled(this.value);
+ // 将 promise2 传入进行判断
+ resolvePromise(promise2, v, resolve, reject);
} else if (this.status === REJECTED) {
const v = onRejected(this.reason);
+ // 将 promise2 传入进行判断
+ resolvePromise(promise2, v, resolve, reject);
} else {
this.onFulfilledCallbacks.push(onFulfilled);
this.onRejectedCallbacks.push(onRejected);
}
});
return promise2;
}
}
再测试一下前面的例子,得出结果:
js
// 1
// Uncaught ReferenceError: Cannot access 'p1' before initialization
尴尬,错误信息完全不一样,这里根据提示可以知道,我们在 p1
定义之前就使用了它。实际情况也确实如此,我们是先等 then
方法里面的回调函数执行完毕之后,then
方法再返回 Promise
对象,但我们却在回调函数内先用了这个 Promise
对象,所以才报的这个错误信息。
那怎么办呢?其实只需要把 then
方法的回调函数的同步执行改为异步执行就可以了。对原生 Promise
足够了解的朋友应该知道,then
方法的回调函数是微任务,创建微任务的方式有以下这几种:
- 浏览器环境下有
MutationObserver
。 Promise.then()
- Node 环境下有
process.nextTick
。 queueMicrotask
由于 Promise.then
是我们自己要手动实现的,MutationObserver
和 process.nextTick
又需要在专门的环境下使用,所以这里选择使用 queueMicrotask
来实现微任务的创建。
可以在 MDN 文档上了解更多关于 queueMicrotask 的信息。
我们再来改造一下 SelfPromise
的代码:
js
class SelfPromise {
// ... 省略部分代码
then(onFulfilled, onRejected) {
const promise2 = new SelfPromise((resolve, reject) => {
+ const fulfilledMicrotask = () => {
+ queueMicrotask(() => {
+ const v = onFulfilled(this.value);
+ resolvePromise(promise2, v, resolve, reject);
+ });
+ };
+ const rejectedMicrotask = () => {
+ queueMicrotask(() => {
+ const v = onRejected(this.reason);
+ resolvePromise(promise2, v, resolve, reject);
+ });
+ };
if (this.status === FULFILLED) {
+ // 异步执行 fulfilled 回调函数
+ fulfilledMicrotask();
} else if (this.status === REJECTED) {
+ // 异步执行 rejected 回调函数
+ rejectedMicrotask();
} else {
+ // 添加订阅者(异步执行的回调函数)
+ this.onFulfilledCallbacks.push(fulfilledMicrotask);
+ this.onRejectedCallbacks.push(rejectedMicrotask);
}
});
return promise2;
}
}
测试一下:
js
const p = new SelfPromise((resolve, reject) => {
resolve('1')
})
// 返回自身 Promise 对象
const p1 = p.then(value => {
console.log(value);
return p1;
})
// 接收错误信息
p1.then(value => {
console.log(2);
console.log('fulfilled', value)
}, err => {
console.log(3);
console.log('reject', err.message);
})
// 1
// 3
// reject Chaining cycle detected for promise #<Promise>
效果终于达到了!
thenable 对象的处理补充
通过对 then
方法链式调用的实现,我们可以知道:then
方法的回调函数是异步执行的,属于微任务。
回想一下前文说到的 then
方法返回 Promise
对象或 thenable
对象的处理方式:
js
function resolvePromise(promise2, value, resolve, reject) {
// 部分省略代码...
if (typeof value === "object" || typeof value === "function") {
// 部分省略代码...
try {
if (typeof value.then === "function") {
value.then(resolve, reject);
} else {
resolve(value);
}
} catch (error) {
reject(error);
}
} else {
resolve(value);
}
}
从这段代码上看,Promise
对象和 thenable
对象处理方式是一样的,但是,别忘了,Promise
对象的 then
方法的回调函数是异步执行的,而 thenable
对象的 then
方法 的回调函数可能是同步的也可能是异步的,因为 thenable
对象属于开发人员自定义的对象,是否异步完全由开发人员自己处理。
所以,为了能够让它稳定异步执行,我们需要把它放到 queueMicrotask
的回调函数中执行:
js
function resolvePromise(promise2, value, resolve, reject) {
// 部分省略代码...
if (typeof value === "object" || typeof value === "function") {
// 部分省略代码...
try {
if (typeof value.then === "function") {
+ // 异步执行
+ queueMicrotask(() => {
+ value.then(resolve, reject);
+ });
} else {
resolve(value);
}
} catch (error) {
reject(error);
}
} else {
resolve(value);
}
}
给 thenable
对象添加上了异步执行,是不是就完美了呢?当然不是,刚刚说到了,thenable
对象属于开发人员自定义的对象,所以,我们还需要处理以下这三点:
-
thenable
对象可能被开发人员通过Object.defineProperty()
或new Proxy()
给then
方法设置了一层代理(很多库或者框架的作者都会这么做),为了避免在调用then
方法的时候,触发代理逻辑,所以需要先将函数取出来存放到一个变量上,再通过call
方法进行调用,保持this
指向是thenable
对象。jsfunction resolvePromise(promise2, value, resolve, reject) { // 部分省略代码... if (typeof value === "object" || typeof value === "function") { // 部分省略代码... + // 将函数取出并存到一个常量上 + const then = value.then; try { + if (typeof then === "function") { queueMicrotask(() => { + then.call(value, resolve, reject); }); } else { resolve(value); } } catch (error) { reject(error); } } else { resolve(value); } }
-
thenable
对象可能会同时调用resolve
和reject
回调函数,所以我们需要控制,只要调用其中一个回调函数,就不会再调用另一个回调函数,跟Promise
内部的resolve
和reject
回调函数一样。jsfunction resolvePromise(promise2, value, resolve, reject) { // 部分省略代码... if (typeof value === "object" || typeof value === "function") { // 部分省略代码... + // called 变量控制 thanable 对象只调用 resolve 或 reject 函数一次 + let called = false; // 将函数取出并存到一个常量上 const then = value.then; try { if (typeof then === "function") { queueMicrotask(() => { then.call( value, + (value2) => { + if (called) return; + // 调用了 resolve,called 设为 true,防止再一次调用 reject + called = true; + resolve(value); + }, + (err) => { + if (called) return; + // 调用了 reject,called 设为 true,防止再一次调用 reolve + called = true; + reject(err); + } ); }); } else { resolve(value); } } catch (error) { + if (called) return; + // 错误处理,会调用 reject,called 设为 true,防止再一次调用 reolve + called = true; reject(error); } } else { resolve(value); } }
-
在某些情况下,
thenable
对象中resolve
回调函数很有可能传入的是Promise
对象,比如:jsconst p = new Promsie((resolve, reject) => { resolve(1); }); const thenable = { then(resolve) { resolve(p); } };
所以,要递归调用
resolvePromise
函数处理这种情况。jsfunction resolvePromise(promise2, value, resolve, reject) { // 部分省略代码... if (typeof value === "object" || typeof value === "function") { // 部分省略代码... // called 变量控制 thanable 对象只调用 resolve 或 reject 函数一次 let called = false; // 将函数取出并存到一个常量上 const then = value.then; try { if (typeof then === "function") { queueMicrotask(() => { then.call( value, (value2) => { if (called) return; // 调用了 resolve,called 设为 true,防止再一次调用 reject called = true; + // value2 可能是 Promise 对象,所以需要调用 resolvePromise 函数来进行处理 + resolvePromise(promise2, value2, resolve, reject); }, (err) => { if (called) return; // 调用了 reject,called 设为 true,防止再一次调用 reolve called = true; reject(err); } ); }); } else { resolve(value); } } catch (error) { if (called) return; // 错误处理,会调用 reject,called 设为 true,防止再一次调用 reolve called = true; reject(error); } } else { resolve(value); } }
错误捕获的补充
我们在所有可能发生代码运行错误的地方套上 try...catch
,然后再调用 reject
函数接收错误:
js
// ...省略部分代码
class SelfPromise {
// ...省略部分代码
constructor(executor) {
+ try {
executor(this.resolve, this.reject);
+ } catch (error) {
+ this.reject(error);
+ }
}
// ...省略部分代码
then(onFulfilled, onRejected) {
const promise2 = new SelfPromise((resolve, reject) => {
const fulfilledMicrotask = () => {
queueMicrotask(() => {
+ try {
const v = onFulfilled(this.value);
resolvePromise(promise2, v, resolve, reject);
+ } catch (error) {
+ reject(error);
+ }
});
};
const rejectedMicrotask = () => {
queueMicrotask(() => {
+ try {
const v = onRejected(this.reason);
resolvePromise(promise2, v, resolve, reject);
+ } catch (error) {
+ reject(error);
+ }
});
};
});
return promise2;
}
}
then 方法的值穿透
什么是 then
方法的值穿透?就是在 then
方法链式调用的情况下,如果有一个 then
方法不传入 fulfilled
回调函数,那么会一直传给下一个 then
方法,直到这个 then
方法有 fulfilled
回调函数。比如:
js
const p = new Promise((resolve, reject) => {
resolve(1);
})
p.then().then().then(value => console.log(value));
// 1
不传入 rejected
回调函数的情况也是一样:
js
const p = new Promise((resolve, reject) => {
reject(2);
});
p.then((value) => console.log("fulfilled", value)).then(
(valu1e) => console.log("fulfilled", value1),
(reason) => console.log("rejected", reason)
);
// rejected 2
then
方法的值穿透的原理是什么呢?其实主要靠 then
方法的链式调用来实现的,上面第一种情况的代码等同于:
js
const p = new Promise((resolve, reject) => {
resolve(1);
})
p.then((value) => value)
.then((value) => value)
.then(value => console.log(value));
只不过 Promise
内部帮我们做了这件事:如果 then
的第一个参数不传 fulfilled
回调函数或者是非函数类型的数据,那就将第一个参数的值设置为 (value) => value
;同理第二个参数的值设置为 (reason) => throw reason
,抛出错误,就可以调用 reject
函数接收错误。
接下来,我们自己动手实现一下:
js
// ...省略部分代码
class SelfPromise {
// ...省略部分代码
then(onFulfilled, onRejected) {
+ // onFulfilled 回调函数的默认值,then 方法值传递的原理
+ onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (value) => value;
+ // onRejected 回调函数的默认值,then 方法值传递的原理
+ onRejected = typeof onRejected === "function" ? onRejected : (reason) => { throw reason; }
// ...省略部分代码
}
}
再运行上面的例子,你会发现结果是一模一样的。
静态方法 resolve 和 reject 的实现
静态 resolve 方法的实现原理
我们经常使用 Promise.resolve()
来生成一个状态是 fulfilled
的 Promise
对象,该方法的参数有三种:
-
参数可以是一个
Promise
实例对象。如果参数是
Promise
实例对象,那么Promise.resolve
方法将原封不动地返回这个对象。 -
参数可以是一个
thenable
对象如果参数是
thenable
对象,那么在Promise.resolve
内部会将该对象的then
方法放入微任务队列中执行。比如:jsconst thenable = { then(resolve, reject) { console.log(1); resolve(2); } }; Promise.resolve(thenable).then((value) => { console.log(value); }); console.log(3); // 3 // 1 // 2
-
不传参数或者参数是其他类型的数据,
Promise.resolve
方法返回fulfilled
状态的Promise
实例对象。
基于以上的特点,我们来实现一下它的原理:
js
// ...省略部分代码
class SelfPromise {
// ...省略部分代码
static resolve(param) {
// 如果参数是 Promise 实例对象,原封不动地返回这个对象
if (param instanceof SelfPromise) {
return param;
}
return new SelfPromise((resolve, reject) => {
if (param && typeof param.then === "function") {
// 如果参数是 thenable 对象,放入微任务队列中执行
queueMicrotask(() => {
param.then(resolve, reject);
});
} else {
// 其他情况直接调用 resolve 函数,返回 fulfilled 状态的 Promise 对象
resolve(param);
}
});
}
}
静态 reject 方法的实现原理
Promise.reject
方法也是一个 Promise
对象,状态是 rejected
,但它的参数类型并没有 Promise.resolve
方法那么复杂,只有一种情况:无论传入什么类型的数据,都只会返回 rejected
状态的 Promise
实例对象。
实现原理也比较简单:
js
// ...省略部分代码
class SelfPromise {
// ...省略部分代码
static reject(param) {
return new SelfPromise((resolve, reject) => {
reject(param);
});
}
}
Promise 实例对象方法的实现
Promise 实例对象的方法除了 then
方法,还有 catch
方法和 finally
方法,接下来,我们也手动实现一下这两个方法。
catch 方法实现原理
catch
方法是用于指定发生错误时的回调函数,它其实就是对 then
方法的调用,想想我们之前是通过 then
方法的第二个参数来接收 rejected
状态的错误:
js
const p = new SelfPromise((resolve, reject) => {
reject(1);
})
p.then((value) => {
console.log("fulfilled", value);
}, (reason) => {
console.log("rejected", reason);
});
// rejected 1
所以, catch
方法等同于 then(null, onRejected)
或 then(undefined, onRejected)
,因此实现原理也很明了:
js
// ...省略部分代码
class SelfPromise {
// ...省略部分代码
catch(onRejected) {
return this.then(null, onRejected);
}
}
finally 方法实现原理
finally
方法用于在 Promise
对象的状态是 fulfilled
还是 rejected
的情况下,都会执行的操作。注意,finally
方法的回调函数不接受任何参数,同时,finally
方法也会返回 Promise
对象。
finally 中文翻译过来是最终的意思,所以,finally
方法的回调函数无论返回什么值,都不会传递给后面链式调用的 then
方法的回调函数。并且,finally
方法自带值穿透特性,会将前面 then
方法回调函数返回的值自动传给它后面的 then
方法或者 catch
方法的回调函数。比如:
js
Promise.reject(1)
.finally((reason) => {
console.log("finally", reason);
return 2;
})
.then((value) => {
console.log("then", value);
})
.catch((reason) => {
console.log("catch", reason);
});
// finally undefined
// catch 1
finally
本质上也是 then
方法的特例,由于 finally
方法的回调函数跟前面 Promise
对象的状态无关,所以,就等同于需要在 then
方法中的两个回调函数里调用 finally
方法的回调函数,并且基于 finally
自身的一些特性,还需要借助静态方法 resolve
来实现其原理:
js
// ...省略部分代码
class SelfPromise {
// ...省略部分代码
finally(callback) {
return this.then(
// 值穿透以及 callback() 返回值不会传递给后面 then 方法的原理
(value) => SelfPromise.resolve(callback()).then(() => value),
(reason) => SelfPromise.resolve(callback()).then(() => { throw reason; })
);
}
}
静态方法 all,race,allSettled 和 any 的实现
Promise.all 的实现原理
Promise.all
方法接收一个具有 Iterator
接口的数据作为参数,参数中的每一个元素都是 Promise
对象,如果不是,就会用 Promise.resolve
方法将它转换为 Promise
对象。
Promise.all
方法返回的也是一个新的 Promise
对象,当所有元素的状态都是 fulfilled
时,返回的新 Promise
对象的状态才是 fulfilled
,否则就是 rejected
。
如果新 Promise
对象的状态是 fulfilled
,那么用 then
方法接收的结果是一个数组,数组中结果的顺序就是 Promise.all
方法参数的顺序,比如:
js
Promise.all([
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3),
]).then((res) => {
console.log(res);
});
// [1, 2, 3]
原理实现思路:
- 首先要判断参数的
Symbol.iterator
属性是否是函数,如果是,这表明参数是具有Iterator
接口的数据,如果不是直接返回错误。 - 统一遍历具有
Iterator
接口的数据的方法,因为需要考虑到Set
,Map
,String
等类型数据。 - 当每个
Promise
对象元素的状态变为fulfilled
时,会将其结果存到对应索引的数组中。
js
// ...省略部分代码
class SelfPromise {
// ...省略部分代码
static all(promiseIterator) {
return new SelfPromise((resolve, reject) => {
// 判断参数是否是具有 `Iterator` 接口的数据
if (
promiseIterator &&
typeof promiseIterator[Symbol.iterator] === "function"
) {
const res = []; // 结果数组
let countRes = 0; // 记录数组中结果的个数
const len = promiseIterator.length || promiseIterator.size;
// 保存对应索引的结果
function saveRes(value, index) {
res[index] = value;
if (++countRes === len) {
resolve(res);
}
}
// 返回迭代器对象
const iterator = promiseIterator[Symbol.iterator]();
// 遍历具有迭代器的数据结构,并且记录索引值
for (
let i = 0, iteratorRes = iterator.next();
iteratorRes.done !== true;
i++, iteratorRes = iterator.next()
) {
SelfPromise.resolve(iteratorRes.value).then((value) => {
// 在对应索引位置上保存结果
saveRes(value, i);
}, reject);
}
} else {
reject(new TypeError("Arguments is not iterable"));
}
});
}
}
Promise.race 的实现原理
Promise.race
方法接收的参数和返回值同 Promise.all
一样,它的特点是:哪个 Promise
实例对象的状态改变得快,Promise.race
方法最后就是什么状态,并且那个率先改变状态的 Promise
实例对象的返回值,会传递给 Promise.race
方法返回值的 then
方法或 catch
方法。
举个例子:
js
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject(2);
}, 2000);
});
Promise.race([p1, p2]).then((res) => {
console.log(res);
});
// 一秒过后,输出 1,状态为 fulfilled
实现原理跟 Promise.all
方法差不多,只不过不再需要把结果保存到数组上了。
js
// ...省略部分代码
class SelfPromise {
// ...省略部分代码
static race(promiseIterator) {
return new SelfPromise((resolve, reject) => {
if (
promiseIterator &&
typeof promiseIterator[Symbol.iterator] === "function"
) {
// 返回迭代器对象
const iterator = promiseIterator[Symbol.iterator]();
// 遍历具有迭代器的数据结构
for (
let iteratorRes = iterator.next();
iteratorRes.done !== true;
iteratorRes = iterator.next()
) {
// 哪个 Promise 对象状态改变得快,race 方法最后就是什么状态
SelfPromise.resolve(iteratorRes.value).then(
resolve,
reject
);
}
} else {
reject(new TypeError("Arguments is not iterable"));
}
});
}
}
Promise.allSettled 的实现原理
Promise.allSettled
方法接收的参数同 Promise.all
一样,不过 Promise.allSettled
方法只有在所有 Promise
对象都发生状态改变了(无论是 fulfilled
还是 rejected
),返回的新 Promise
实例对象状态才会改变,并且状态总是为 fulfilled
。
实现原理和 Promise.all
方法基本一样,只不过在每个 Promise
对象上的 rejected
状态的回调函数处理不同。
js
// ...省略部分代码
class SelfPromise {
// ...省略部分代码
static allSettled(promiseIterator) {
return new SelfPromise((resolve, reject) => {
if (
promiseIterator &&
typeof promiseIterator[Symbol.iterator] === "function"
) {
const res = [];
let countRes = 0;
const len = promiseIterator.length || promiseIterator.size;
function saveRes(value, index) {
res[index] = value;
if (++countRes === len) {
resolve(res);
}
}
// 返回迭代器对象
const iterator = promiseIterator[Symbol.iterator]();
// 遍历具有迭代器的数据结构,并且记录索引值
for (
let i = 0, iteratorRes = iterator.next();
iteratorRes.done !== true;
i++, iteratorRes = iterator.next()
) {
SelfPromise.resolve(iteratorRes.value)
.then((value) => {
saveRes({ status: "fullfilled", value }, i);
})
.catch((reason) => {
saveRes({ status: "rejected", reason }, i);
});
}
} else {
reject(new TypeError("Arguments is not iterable"));
}
});
}
}
Promise.any 的实现原理
Promise.any
方法接收的参数同 Promise.all
一样,只要其中一个 Promise
实例对象的状态变成 fulfilled
,那么Promise.any
方法返回的新 Promise
实例对象的状态就是 fulfilled
,如果所有 Promise
实例对象都变成 rejected
状态,返回的新 Promise
实例对象的状态才会变成 rejected
状态。
在实现原理上,有一点是跟 Promise.all
方法反过来的,那就是每个 Promise
实例对象在 rejected
状态才保存对应索引位置上的结果。
js
// ...省略部分代码
class SelfPromise {
// ...省略部分代码
static any(promiseIterator) {
return new SelfPromise((resolve, reject) => {
if (
promiseIterator &&
typeof promiseIterator[Symbol.iterator] === "function"
) {
const res = [];
let countRes = 0;
const len = promiseIterator.length || promiseIterator.size;
function saveRes(reason, index) {
res[index] = reason;
if (++countRes === len) {
const err = new AggregateError(
res,
"All promises were rejected"
);
reject(err);
}
}
// 返回迭代器对象
const iterator = promiseIterator[Symbol.iterator]();
// 遍历具有迭代器的数据结构,并且记录索引值
for (
let i = 0, iteratorRes = iterator.next();
iteratorRes.done !== true;
i++, iteratorRes = iterator.next()
) {
SelfPromise.resolve(iteratorRes.value).then(
resolve,
(reason) => {
// 在对应索引位置上保存结果
saveRes(reason, i);
}
);
}
} else {
reject(new TypeError("Arguments is not iterable"));
}
});
}
}
总结
完整版的 Promise
实现原理我已经放到 Github 上了 --- self-promise,里面的测试也有前面说到的捉摸不透的三种 Promise
的输出顺序。
总得来说,实现 Promise
的原理关键在于:
Promise
运用了观察者模式,then
方法用于添加订阅者,resolve
和reject
函数用于通知所有订阅者。then
方法的回调函数是异步执行的,属于微任务,所以对于thenable
对象中then
方法放在Promise
内部执行也是异步的。then
链式调用的功能在于then
方法的返回值是Promise
对象,对于then
方法回调函数的返回值类型要有不同的处理方式。- 静态方法
resolve
和reject
就是封装了创建Promise
对象的创建过程;实例方法catch
和finally
就是对then
方法的二次封装;静态方法all
,race
,allSettled
,any
也是利用了then
方法的机制。