前段时间我用两篇文章深入讲解了异步的概念和Event Loop的底层原理,然后还讲了一种自己实现异步的发布订阅模式:
setTimeout和setImmediate到底谁先执行,本文让你彻底理解Event Loop
从发布订阅模式入手读懂Node.js的EventEmitter源码
本文会讲解另一种更现代的异步实现方案:Promise
。Promise几乎是面试必考点,所以我们不能仅仅会用,还得知道他的底层原理,学习他原理的最好方法就是自己也实现一个Promise。所以本文会自己实现一个遵循Promise/A+
规范的Promise。实现之后,我们还要用Promise/A+
官方的测试工具来测试下我们的实现是否正确,这个工具总共有872个测试用例,全部通过才算是符合Promise/A+
规范,下面是他们的链接:
Promise/A+
规范: https://github.com/promises-aplus/promises-spec
Promise/A+
测试工具: https://github.com/promises-aplus/promises-tests
本文的完整代码托管在GitHub上: https://github.com/dennis-jiang/Front-End-Knowledges/blob/master/Examples/JavaScript/Promise/MyPromise.js
Promise用法
Promise的基本用法,网上有很多,我这里简单提一下,我还是用三个相互依赖的网络请求做例子,假如我们有三个网络请求,请求2必须依赖请求1的结果,请求3必须依赖请求2的结果,如果用回调的话会有三层,会陷入“回调地狱”,用Promise就清晰多了:
const request = require("request");
// 我们先用Promise包装下三个网络请求
// 请求成功时resolve这个Promise
const request1 = function() {
const promise = new Promise((resolve) => {
request('https://www.baidu.com', function (error, response) {
if (!error && response.statusCode == 200) {
resolve('request1 success');
}
});
});
return promise;
}
const request2 = function() {
const promise = new Promise((resolve) => {
request('https://www.baidu.com', function (error, response) {
if (!error && response.statusCode == 200) {
resolve('request2 success');
}
});
});
return promise;
}
const request3 = function() {
const promise = new Promise((resolve) => {
request('https://www.baidu.com', function (error, response) {
if (!error && response.statusCode == 200) {
resolve('request3 success');
}
});
});
return promise;
}
// 先发起request1,等他resolve后再发起request2,
// 然后是request3
request1().then((data) => {
console.log(data);
return request2();
})
.then((data) => {
console.log(data);
return request3();
})
.then((data) => {
console.log(data);
})
上面的例子里面,then
是可以链式调用的,后面的then
可以拿到前面resolve
出来的数据,我们控制台可以看到三个success依次打出来:
Promises/A+规范
通过上面的例子,其实我们已经知道了一个promise长什么样子,Promises/A+规范其实就是对这个长相进一步进行了规范。下面我会对这个规范进行一些讲解。
术语
promise
:是一个拥有then
方法的对象或函数,其行为符合本规范
thenable
:是一个定义了then
方法的对象或函数。这个主要是用来兼容一些老的Promise实现,只要一个Promise实现是thenable,也就是拥有then
方法的,就可以跟Promises/A+兼容。
value
:指reslove
出来的值,可以是任何合法的JS值(包括undefined
, thenable 和 promise等)
exception
:异常,在Promise里面用throw
抛出来的值
reason
:拒绝原因,是reject
里面传的参数,表示reject
的原因
Promise状态
Promise总共有三个状态:
pending
: 一个promise在resolve或者reject前就处于这个状态。fulfilled
: 一个promise被resolve后就处于fulfilled
状态,这个状态不能再改变,而且必须拥有一个不可变的值(value
)。rejected
: 一个promise被reject后就处于rejected
状态,这个状态也不能再改变,而且必须拥有一个不可变的拒绝原因(reason
)。
注意这里的不可变指的是===
,也就是说,如果value
或者reason
是对象,只要保证引用不变就行,规范没有强制要求里面的属性也不变。Promise状态其实很简单,画张图就是:
then方法
一个promise必须拥有一个then
方法来访问他的值或者拒绝原因。then
方法有两个参数:
promise.then(onFulfilled, onRejected)
参数可选
onFulfilled
和 onRejected
都是可选参数。
- 如果
onFulfilled
不是函数,其必须被忽略 - 如果
onRejected
不是函数,其必须被忽略
onFulfilled
特性
如果 onFulfilled
是函数:
- 当
promise
执行结束后其必须被调用,其第一个参数为promise
的终值value
- 在
promise
执行结束前其不可被调用 - 其调用次数不可超过一次
onRejected
特性
如果 onRejected
是函数:
- 当
promise
被拒绝执行后其必须被调用,其第一个参数为promise
的据因reason
- 在
promise
被拒绝执行前其不可被调用 - 其调用次数不可超过一次
多次调用
then
方法可以被同一个 promise
调用多次
- 当
promise
成功执行时,所有onFulfilled
需按照其注册顺序依次回调 - 当
promise
被拒绝执行时,所有的onRejected
需按照其注册顺序依次回调
返回
then
方法必须返回一个 promise
对象。
promise2 = promise1.then(onFulfilled, onRejected);
- 如果
onFulfilled
或者onRejected
返回一个值x
,则运行 Promise 解决过程:[[Resolve]](promise2, x)
- 如果
onFulfilled
或者onRejected
抛出一个异常e
,则promise2
必须拒绝执行,并返回拒因e
- 如果
onFulfilled
不是函数且promise1
成功执行,promise2
必须成功执行并返回相同的值 - 如果
onRejected
不是函数且promise1
拒绝执行,promise2
必须拒绝执行并返回相同的据因
规范里面还有很大一部分是讲解Promise 解决过程的,光看规范,很空洞,前面这些规范已经可以指导我们开始写一个自己的Promise了,Promise 解决过程会在我们后面写到了再详细讲解。
自己写一个Promise
我们自己要写一个Promise,肯定需要知道有哪些工作需要做,我们先从Promise的使用来窥探下需要做啥:
- 新建Promise需要使用
new
关键字,那他肯定是作为面向对象的方式调用的,Promise是一个类。关于JS的面向对象更详细的解释可以看这篇文章。- 我们
new Promise(fn)
的时候需要传一个函数进去,说明Promise的参数是一个函数- 构造函数传进去的
fn
会收到resolve
和reject
两个函数,用来表示Promise成功和失败,说明构造函数里面还需要resolve
和reject
这两个函数,这两个函数的作用是改变Promise的状态。- 根据规范,promise有
pending
,fulfilled
,rejected
三个状态,初始状态为pending
,调用resolve
会将其改为fulfilled
,调用reject
会改为rejected
。- promise实例对象建好后可以调用
then
方法,而且是可以链式调用then
方法,说明then
是一个实例方法。链式调用的实现这篇有详细解释,我这里不再赘述。简单的说就是then
方法也必须返回一个带then
方法的对象,可以是this或者新的promise实例。
构造函数
为了更好的兼容性,本文就不用ES6了。
// 先定义三个常量表示状态
var PENDING = 'pending';
var FULFILLED = 'fulfilled';
var REJECTED = 'rejected';
function MyPromise(fn) {
this.status = PENDING; // 初始状态为pending
this.value = null; // 初始化value
this.reason = null; // 初始化reason
}
resolve
和reject
方法
根据规范,resolve
方法是将状态改为fulfilled,reject
是将状态改为rejected。
// 这两个方法直接写在构造函数里面
function MyPromise(fn) {
// ...省略前面代码...
// 存一下this,以便resolve和reject里面访问
var that = this;
// resolve方法参数是value
function resolve(value) {
if(that.status === PENDING) {
that.status = FULFILLED;
that.value = value;
}
}
// reject方法参数是reason
function reject(reason) {
if(that.status === PENDING) {
that.status = REJECTED;
that.reason = reason;
}
}
}
调用构造函数参数
最后将resolve
和reject
作为参数调用传进来的参数,记得加上try
,如果捕获到错误就reject
。
function MyPromise(fn) {
// ...省略前面代码...
try {
fn(resolve, reject);
} catch (error) {
reject(error);
}
}
then
方法
根据我们前面的分析,then
方法可以链式调用,所以他是实例方法,而且规范中的API是promise.then(onFulfilled, onRejected)
,我们先把架子搭出来:
MyPromise.prototype.then = function(onFulfilled, onRejected) {
}
那then
方法里面应该干什么呢,其实规范也告诉我们了,先检查onFulfilled
和onRejected
是不是函数,如果不是函数就忽略他们,所谓“忽略”并不是什么都不干,对于onFulfilled
来说“忽略”就是将value
原封不动的返回,对于onRejected
来说就是返回reason
,onRejected
因为是错误分支,我们返回reason
应该throw一个Error:
MyPromise.prototype.then = function(onFulfilled, onRejected) {
// 如果onFulfilled不是函数,给一个默认函数,返回value
var realOnFulfilled = onFulfilled;
if(typeof realOnFulfilled !== 'function') {
realOnFulfilled = function (value) {
return value;
}
}
// 如果onRejected不是函数,给一个默认函数,返回reason的Error
var realOnRejected = onRejected;
if(typeof realOnRejected !== 'function') {
realOnRejected = function (reason) {
if(reason instanceof Error) {
throw reason;
} else {
throw new Error(reason)
}
}
}
}
参数检查完后就该干点真正的事情了,想想我们使用Promise的时候,如果promise操作成功了就会调用then
里面的onFulfilled
,如果他失败了,就会调用onRejected
。对应我们的代码就应该检查下promise的status,如果是FULFILLED
,就调用onFulfilled
,如果是REJECTED
,就调用onRejected
:
MyPromise.prototype.then = function(onFulfilled, onRejected) {
// ...省略前面代码...
if(this.status === FULFILLED) {
onFulfilled(this.value)
}
if(this.status === REJECTED) {
onRejected(this.reason);
}
}
再想一下,我们新建一个promise的时候可能是直接这样用的:
new Promise(fn).then(onFulfilled, onRejected);
上面代码then
是在实例对象一创建好就调用了,这时候fn
里面的异步操作可能还没结束呢,也就是说他的status
还是PENDING
,这怎么办呢,这时候我们肯定不能立即调onFulfilled
或者onRejected
的,因为fn
到底成功还是失败还不知道呢。那什么时候知道fn
成功还是失败呢?答案是fn
里面主动调resolve
或者reject
的时候。所以如果这时候status
状态还是PENDING
,我们应该将onFulfilled
和onRejected
两个回调存起来,等到fn
有了结论,resolve
或者reject
的时候再来调用对应的代码。因为后面then
还有链式调用,会有多个onFulfilled
和onRejected
,我这里用两个数组将他们存起来,等resolve
或者reject
的时候将数组里面的全部方法拿出来执行一遍:
// 构造函数
function MyPromise(fn) {
// ...省略其他代码...
// 构造函数里面添加两个数组存储成功和失败的回调
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
function resolve(value) {
if(that.status === PENDING) {
// ...省略其他代码...
// resolve里面将所有成功的回调拿出来执行
that.onFulfilledCallbacks.forEach(callback => {
callback(that.value);
});