目前为止,我在项目中大量使用到了ES6的Promise,最常见的场景有是发送一个网络请求,在成功得到响应之后渲染对应页面或者元素,失败之后提示错误。
http.get(url).then(res => { render(res) }, err => { showError(err) })
以上代码中http.get(url)返回的就是一个Promise,在promise上调用then方法,它接收两个参数,第一个参数代表请求成功之后的回调函数,第二个参数代表请求失败时的回调函数。
Before Promise
在ES6 Promise没有诞生之前,我曾在古老的JSP项目中使用过原生的XHR,也使用过jQuery的$.ajax用来实现和上边同样的事儿。当场景足够简单时,看不出Promise的优越性在哪儿。但是每当遇到多个请求互相依赖,需要嵌套,或者次序调用时,都会发现代码非常难看,也很难捕获和处理异常。更致命的问题是,我们将回调函数的执行控制权交给了发起异步的函数,而发起异步的函数可能完全不受我们自己控制,所以没有办法保证对应的回调函数会被正确执行,进而导致bug。
Promise Introduction
而ES6 Promise的出现,解决了以上问题。Promise作为一个第三方担保者的角色出现,它承诺了失败或者成功时一定会执行对应的函数。它具有三种状态: Pending,Fullfilled,Rejected,同一时刻只可能处在一种状态之下;状态转化方向是Pending->Fullfilled和Pending->Reject两种,当Pending->Fullfilled发生时,保证onFullfilled回调函数(then方法的第一个参数)被执行,当Pending->Rejected发生时,保证onRejected回调函数(then方法的第二个参数)被执行。更厉害的是,它还支持链式调用和值穿透,then函数执行后继续返回一个Promise, 包装了上一个Promise的执行结果,这就可以解决上述多个异步动作互相依赖,嵌套,次序调用的问题。
以上我描述的特性并不完整,Promise有多种规范,其中ES6 Promise遵循的是Promise A+规范。接下来,回到本文主题,分两步来介绍如何DIY Promise.
实现简单版Promise
第一步,我们先抛开A+规范,实现一个最简单的Promise。
首先来设计test case:
it("resolve", () => {
new MyPromise((resolve) => {
resolve(1);
}).then((data) => {
expect(data).toEqual(1);
});
});
it("reject", () => {
const onFullFilled = jest.fn();
new MyPromise((resolve, reject) => {
reject(0);
resolve(1);
}).then(onFullFilled, (data) => {
expect(data).toEqual(0);
});
expect(onFullFilled).not.toHaveBeenCalled();
});
我设计了两个case, 分别针对resolve和reject的情况。其中case reject还需要测试在reject之后,onRejected毁掉是否还会被执行。
直接先来看第一个版本的代码吧:
export const PENDING = "PENDING";
export const FULFILLED = "FULFILLED";
export const REJECTED = "REJECTED";
class MyPromise{
private status= PENDING;
private value;
private reason;
constructor(excutor){
try{
excutor(this.resolve, this.reject)
}catch(error){
this.reject(error)
}
}
private resolve(value){
if(status !== PENDING)return;
this.value = value;
this.status = FULFILLED;
}
private reject(reason){
if(status !== PENDING)return;
this.reason = reason;
this.status = REJECTED;
}
public then(onFullfilled, onRejected){
if(this.status === FULFILLED){
onFullfilled(this.value)
}
if(this.status === REJECTED){
onRejected(this.reason)
}
}
}
以上最简版已经初步刻画了Promise的核心功能。但是,有一种case没有覆盖到:
it("simple resolve", () => {
const promise = new MyPromise((resolve) => {
setTimeout(() => {
resolve(1);
}, 0);
});
expect(promise).resolves.toBe(1);
});
如果Promise体中含有异步代码,而resove, reject的执行正好在异步代码中会出现什么情况呢? 很显然then会先执行,然而执行then的时候异步代码块中的resolve/reject还未被执行,此时Promise的状态还是PENDING,进而导致onFullfilled/onRejected也不会被执行。
为了解决这个问题,我们需要使用两个数组分别存储onFullfilleds和onRejecteds, 当遇执行then时如果还处在PENDING的状态,那么先把他们存在数组中,当resolve或reject触发的时候,依次执行他们。
export const PENDING = "PENDING";
export const FULFILLED = "FULFILLED";
export const REJECTED = "REJECTED";
class MyPromise{
private status= PENDING;
private value;
private reason;
private onResolvedCallbacks = [];
private onRejectedCallbacks = [];
constructor(excutor){
try{
excutor(this.resolve, this.reject)
}catch(error){
this.reject(error)
}
}
private resolve(value){
if(status !== PENDING)return;
this.value = value;
this.status = FULFILLED;
this.onResolvedCallbacks.foreach(cb => cb(this.value))
}
private reject(reason){
if(status !== PENDING)return;
this.reason = reason;
this.status = REJECTED;
this.onRejectedCallbacks.foreach(cb => cb(this.reason))
}
public then(onFullfilled, onRejected){
if(this.status === FULFILLED){
onFullfilled(this.value)
}
if(this.status === REJECTED){
onRejected(this.reason)
}
if(this.status === PENDING){
this.onResolvedCallbacks.push(onFullfilled);
this.onRejectedCallbacks.push(onRejected)
}
}
}
一开始,我针对这段代码有两个疑问:
- 为什么要是用数组存回调?以下case可以解答:
it("multiple then", () => {
let output = [];
const promise = new MyPromise((resolve) => {
setTimeout(() => {
resolve(1);
}, 0);
});
// 可能会在同一个promise上多次调用then
promise.then((data) => output.push(data + 1));
promise.then((data) => output.push(data + 2));
promise.then((data) => output.push(data + 3));
new MyPromise((resolve) => {
resolve("a");
}).then(() => {
expect(output).toEqual([2, 3, 4]);
});
- 这样写之后会不会造成非异步的情况下onFullfilled/onReject被提前执行?
答案是不会,因为只有在异步的情况下,才会出现执行then的时候状态还是PENDING。
至此为止我们已经完成了一个简版的Promise。
实现符合A+规范的Promise
第二步,我们在上面的基础上,实现符合A+规范的Promise。
同样,先来设计test cases:
describe("chain", () => {
it("should throw cycle error", () => {
const promise = new MyPromise((resolve) => {
resolve(1);
}).then(
(data) => {
return promise;
},
(error) => {
expect(error).toEqual(
new TypeError("Chaining cycle detected for promise #<Promise>")
);
}
);
});
it("if x is simple value", () => {
new MyPromise((resolve) => {
resolve(1);
})
.then(() => {
return 2;
})
.then()
.then((data) => {
expect(data).toEqual(2);
});
});
it("if x is a thenable but then is a simple value", () => {
const p = new MyPromise((resolve) => {
resolve(1);
}).then(() => {
return { then: 2 };
});
expect(p).resolves.toEqual({ then: 2 });
});
it("if x is a real promise and it resolved", () => {
new MyPromise((resolve) => {
resolve(1);
})
.then(() => {
return new MyPromise((resolve) => {
resolve(2);
});
})
.then((data) => {
expect(data).toBe(2);
});
});
it("if x is a real promise and it rejected", () => {
new MyPromise((resolve) => {
resolve(1);
})
.then(() => {
return new MyPromise((resolve, reject) => {
reject(2);
});
})
.then(null, (data) => {
expect(data).toBe(2);
});
});
it("if x is a nested promise", () => {
new MyPromise((resolve) => {
resolve(1);
})
.then(() => {
return new MyPromise((resolve) => {
resolve(2);
}).then(() => {
return new MyPromise((resolve) => resolve(3));
});
})
.then((data) => {
expect(data).toBe(3);
});
});
it("if x is promise, resolve and reject should be mutex", () => {
const onFullFilled = jest.fn();
new MyPromise((resolve, reject) => {
resolve(1);
})
.then((data) => {
return {
then: (resolve, reject) => {
reject(3);
resolve(2);
}
};
})
.then(onFullFilled, (data) => {
expect(data).tobe(3);
});
expect(onFullFilled).not.toHaveBeenCalled();
});
it("if reject in nested promise x", () => {
new MyPromise((resolve, reject) => {
resolve(1);
})
.then(() => {
return new MyPromise((resolve, reject) => {
reject(2);
}).then(null, () => {
return new MyPromise((resolve, reject) => {
reject(3);
});
});
})
.then(
() => {},
(data) => {
expect(data).toEqual(3);
}
);
});
});
然后再来看代码实现:
export const PENDING = "PENDING";
export const FULFILLED = "FULFILLED";
export const REJECTED = "REJECTED";
export class MyPromise {
constructor(executor) {
try{
excutor(this.resolve, this.reject)
}catch(error){
this.reject(error)
}
}
private resolve(value){
if(status !== PENDING)return;
this.value = value;
this.status = FULFILLED;
this.onResolvedCallbacks.foreach(cb => cb(this.value))
}
private reject(reason){
if(status !== PENDING)return;
this.reason = reason;
this.status = REJECTED;
this.onRejectedCallbacks.foreach(cb => cb(this.reason))
}
private resolvePromise(promise2, x, resolve, reject) {
// 不能自己等待自己执行完
if (promise2 === x) {
return reject(
new TypeError("Chaining cycle detected for promise #<Promise>")
);
}
// 如果x是一个promise, 用来标记它是否执行完resolve或者reject
// 其实不做标记也不会发生Bug, 只是会产生没必要的调用
let called;
if ((typeof x === "object" && x !== null) || typeof x === "function") {
const then = x.then;
if (typeof then === "function") {
try {
then.call(
x,
(resolvedData) => {
if (called) return;
this.resolvePromise(promise2, resolvedData, resolve, reject);
called = true;
},
(rejectedData) => {
if (called) return;
reject(rejectedData);
called = true;
}
);
} catch (error) {
if (called) return;
reject(error);
called = true;
}
} else {
resolve(x);
}
} else {
resolve(x);
}
}
// 包含一个 then 方法,并接收两个参数 onFulfilled、onRejected
public then(onFulfilled, onRejected) {
if (typeof onFulfilled !== "function") {
onFulfilled = (v) => v;
}
if (typeof onRejected !== "function") {
onRejected = (err) => {
throw Error(err);
};
}
const promise2 = new MyPromise((resolve, reject) => {
if (this.status === FULFILLED) {
try {
const x = onFulfilled(this.value);
this.resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}
if (this.status === REJECTED) {
try {
const x = onRejected(this.reason);
this.resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}
if (this.status === PENDING) {
this.onResolvedCallbacks.push(onFulfilled);
this.onRejectedCallbacks.push(onRejected);
}
});
return promise2;
}
}
总结
我在自己尝试实现Promise的时候,没有搞清楚ES6 Promise和Promise A+是有区别的,ES6 Promise在其之上扩展了一些功能,比如Promise.resolve, Promise.reject, Promise.all, Promise.race。还有一个我纠结了很久的区别,那就是ES6 Promise只要不调用then,那么promise的状态始终是pending, 但是Promise A+不管then有没有被调用,只要resolve/reject被执行了,状态都会变化。
在我自己尝试实现的时候,我尝试着按照ES6 Promise去实现,但是发现不知如何做,于是我才发现有个Promise A+规范,然后按照文章 中的解析,再尝试自己构建测试用例然后改写出来的。
这启发我在学习一个东西的原理的时候,应该先有一个大局观,而不是一开始就拘泥于细节。
同时,这次我尝试应用“测试驱动开发”的思想,整体做下来感觉到思路清晰,效率更高。
网友评论