💡JS-带你手写简易版Promise(含jest测试)

171 阅读10分钟

这篇文章我们用 js 来实现一个简易版的 promise。promise 有很多的 api 语法,很多细节,让人学了很难记住。而学习源码,能让我们拨开迷雾,直接抵达其本质。

Promise 状态和值

promise 有三个状态:

  • pending
  • fulfilled
  • rejected

一般情况下,刚开始创建的 promise 的状态是 pending ,也就是待定(不确定)的意思。状态确定之后,会变成 fulfilled 或 rejected 其中一种,一旦状态确定了,就不能改变了。

并且 promise 也有自己的值,其把状态改变函数的参数作为 promise 的值。

下面实现下。新建文件夹 MyPromise,然后创建 index.js :

const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
class MyPromise {
	status = "pending";
	value = undefined;

	constructor(executor) {
		executor(this.resolve.bind(this), this.reject.bind(this));
		return this;
	}

	resolve(value) {
		if (this.status === PENDING) {
			this.status = FULFILLED;
			this.value = value;
		}
	}

	reject(reason) {
		if (this.status === PENDING) {
			this.status = REJECTED;
			this.value = reason;
		}
	}
}

module.exports = MyPromise;

上面的代码实现了一个基本的 Promise 类,包含状态管理和 resolve、reject 方法。

jest测试:

测试我们就不用简单的手动测试,借用 jest 测试框架,体验下自动化测试的感觉😄

**Jest **是一个由 Facebook 开发和维护的 JavaScript 测试框架,广泛用于测试 React 应用程序,但也可以用于测试任何 JavaScript 代码。

要使用 Jest,首先需要安装它:

npm init -y
npm install --save-dev jest

npm init -y之后,MyPromise 文件夹下面会多一个 package.json。在 package.json 中添加一个测试脚本:

"scripts": {
  "test": "jest"
}

创建个测试文件看看 jest 语法长什么样子。demo.test.js (通常以 .test.js 或 .spec.js 结尾):

test('adds 1 + 2 to equal 3', () => {
  expect(1 + 2).toBe(3);
});

然后打开终端,执行npm run test

测试通过!!

下面我们正式写 promise 的测试代码: promise.test.js:

const MyPromise = require(".");

describe("test promise status and value", () => {
	test("test resolve", () => {
		const myPromise = new MyPromise((resolve, reject) => {
			resolve(1);
		});
		expect(myPromise.status).toBe("fulfilled");
		expect(myPromise.value).toBe(1);
	});

	test("test reject", () => {
		const myPromise = new MyPromise((resolve, reject) => {
			reject(1);
		});
		expect(myPromise.status).toBe("rejected");
		expect(myPromise.value).toBe(1);
	});

	test("test resolve and reject at the same time", () => {
		const myPromise = new MyPromise((resolve, reject) => {
			resolve(1);
			reject(2);
		});
		expect(myPromise.status).toBe("fulfilled");
		expect(myPromise.value).toBe(1);
	});
});

代码中写了三个测试用例,用来测试 promise 的 status 以及 value。

测试结果:

jest 测试看起来有点懵?看不懂?没关系,看个大概就好,把重点放在 promise 的实现上

promise 的 then

then 的性质可就多了,下面一个一个来

then 的参数

then 用来读取 promise的值,并接收两个函数作为参数。如果 promise 的状态是 fulfilled,第一个参数就会被调用,并且传入 promise 的值。如果 promise的状态是 rejected,第二个参数就会被调用

class MyPromise {
	status = "pending";
	value = undefined;

	constructor(executor) {
		executor(this.resolve.bind(this), this.reject.bind(this));
		return this;
	}

	resolve(value) {
		if (this.status === PENDING) {
			this.status = FULFILLED;
			this.value = value;
		}
	}

	reject(reason) {
		if (this.status === PENDING) {
			this.status = REJECTED;
			this.value = reason;
		}
	}

  // 添加一个then
	then(onFulfilled, onRejected) {
		if (this.status === FULFILLED) {
			onFulfilled(this.value);
		} else if (this.status === REJECTED) {
			onRejected(this.value);
		}
	}
}

thne 的返回值

then 的返回值是一个新 promise,并且新 promise 的状态和值与onFulfilledonRejected的返回值有关系:

  • onFulfilled/onRejected 可以返回任意值,如果返回的是非 promise 值,那么生成的新 promise 的状态都是fulfilled,并且它的值和 onFulfilled 的返回值一致
  • 如果 onFulfilled/onRejected 返回的是 promise 对象,那么生成的新 promise 的状态和返回的 promise 的状态一致,且值也是一致的
class MyPromise {
    //...
  	then(onFulfilled, onRejected) {
		return new MyPromise((resolve, reject) => {
			if (this.status === FULFILLED) {
				this.resolvePromise(promise, onFulfilled(this.value), resolve, reject);
			} else if (this.status === REJECTED) {
				this.resolvePromise(promise, onRejected(this.value), resolve, reject);
			}
		});
	}

	handlePromise(x, resolve, reject) {
		if (x instanceof MyPromise) {
			x.then(
				(res) => {
					return this.handlePromise(res, resolve, reject);
				},
				(err) => {
					reject(err);
				}
			);
		} else {
			resolve(x);
		}
	}
}

写个测试脚本测试下,先测试 then 的参数

describe("test promise then", () => {
	test("test then on fulfilled promise", () => {
		const myPromise = new MyPromise((resolve, reject) => {
			resolve(1);
		});

		const spyOnFulfilled = jest.fn();
		const spyOnRejected = jest.fn();
		myPromise.then(spyOnFulfilled, spyOnRejected);
		expect(spyOnFulfilled).toHaveBeenCalledWith(1);
		expect(spyOnRejected).not.toHaveBeenCalled();
	});

	test("test then on rejected promise", () => {
		const myPromise = new MyPromise((resolve, reject) => {
			reject(1);
		});

		const spyOnFulfilled = jest.fn();
		const spyOnRejected = jest.fn();
		myPromise.then(spyOnFulfilled, spyOnRejected);
		expect(spyOnFulfilled).not.toHaveBeenCalled();
		expect(spyOnRejected).toHaveBeenCalledWith(1);
	});
}

jest.fn是 jest 中 mock 的函数,用来检测函数是否被调用

测试结果:

再测试下 then 的返回值

describe("test return value of then", () => {
	test("test return value of then", () => {
		const myPromise = new MyPromise((resolve, reject) => {
			resolve(1);
		});

		const spyOnFulfilled = jest.fn((res) => {
			return res + 1;
		});
		const spyOnRejected = jest.fn();
		const newPromise = myPromise.then(spyOnFulfilled, spyOnRejected);

		expect(newPromise.status).toBe("fulfilled");
		expect(newPromise.value).toBe(2);
	});

	test("test return fulfilled promise of then", () => {
		const myPromise = new MyPromise((resolve, reject) => {
			resolve(1);
		});

		const spyOnFulfilled = jest.fn((res) => {
			return new MyPromise((resolve, reject) => {
				resolve(res + 1);
			});
		});
		const spyOnRejected = jest.fn();
		const newPromise = myPromise.then(spyOnFulfilled, spyOnRejected);

		expect(newPromise.status).toBe("fulfilled");
		expect(newPromise.value).toBe(2);
	});

	test("test return rejected promise of then", () => {
		const myPromise = new MyPromise((resolve, reject) => {
			resolve(1);
		});

		const spyOnFulfilled = jest.fn((res) => {
			return new MyPromise((resolve, reject) => {
				reject(res + 1);
			});
		});
		const spyOnRejected = jest.fn();
		const newPromise = myPromise.then(spyOnFulfilled, spyOnRejected);

		expect(newPromise.status).toBe("rejected");
		expect(newPromise.value).toBe(2);
	});
});

测试结果:

在 then 中抛出一个错误

如果想使 then 中返回一个失败的 promise,还有一个办法,就是在 then 中 抛出一个错误。要想捕获 then 中抛出错误,还需要在 then 中处理一下:

class MyPromise{
  	then(onFulfilled, onRejected) {
		return new MyPromise((resolve, reject) => {
			try {
			} catch (error) {}
			if (this.status === FULFILLED) {
				try {
					const res = onFulfilled(this.value);
					this.handlePromise(res, resolve, reject);
				} catch (error) {
					reject(error);
				}
			} else if (this.status === REJECTED) {
				try {
					const res = onRejected(this.value);
					this.handlePromise(res, resolve, reject);
				} catch (error) {
					reject(error);
				}
			}
		});
	}
}

在调用onFulfilled/onRejected的时候,外部包裹了 try...catch,如果抛出错误,可以直接返回一个失败的 promise

测试代码:

describe("test throw error in then", () => {
	test("test throw error in then", () => {
		const myPromise = new MyPromise((resolve, reject) => {
			resolve(1);
		});

		let newPromise = myPromise.then(
			(res) => {
				throw "error";
			},
			(err) => {
				console.log(err);
			}
		);

		expect(newPromise.status).toBe("rejected");
		expect(newPromise.value).toBe("error");
	});
});

测试结果:

then 的参数省略

then 的参数其实没必要一定要穿两个,可以只传一 onFulfilled,或者什么都不传。支持这个特性,是因为 then 中给了默认值

then(onFulfilled, onRejected) {
  // 给默认值
		onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (value) => value;
		onRejected =
			typeof onRejected === "function"
				? onRejected
				: (reason) => {
						throw reason;
				  };
		return new MyPromise((resolve, reject) => {
			try {
			} catch (error) {}
			if (this.status === FULFILLED) {
				try {
					const res = onFulfilled(this.value);
					this.handlePromise(res, resolve, reject);
				} catch (error) {
					reject(error);
				}
			} else if (this.status === REJECTED) {
				try {
					const res = onRejected(this.value);
					this.handlePromise(res, resolve, reject);
				} catch (error) {
					reject(error);
				}
			}
		});
	}

then 的等待性

很多时候,我们并不是在状态确定的 promise 上调用 then,而是在 pending 的 promise 上调用的,向这样:

new MyPromise((resolve, reject)=>{
  setTimeout(resolve,1000,1);
}).then(res=>{
  console.log('onfulfilled',res)
})

上面的代码创建了一个 1 秒之后,状态才落定的 promise 对象,而 then 是同步读取的。目前 then 中并没有对 pending 状态的判断处理,所以上面 then 中的函数并不会被执行。

按道理,then 中的函数是需要被执行,而且是 1 秒之后才执行,这样才能拿到 promise 的值。代码怎么写呢?

class MyPromise {

  onFulfilledCallbacks = [];
  onRejectedCallbacks = [];

  resolve(value) {
    if (this.status === PENDING) {
      this.status = FULFILLED;
      this.value = value;

      this.onFulfilledCallbacks.forEach((callback) => {
        callback(value);
      });
    }
  }

  reject(reason) {
    if (this.status === PENDING) {
      this.status = REJECTED;
      this.value = reason;

      this.onRejectedCallbacks.forEach((callback) => {
        callback(reason);
      });
    }
  }

  return new MyPromise((resolve, reject) => {
    // 状态成功时候的处理函数
    const handleResolve = () => {
      try {
        const res = onFulfilled(this.value);
        this.handlePromise(res, resolve, reject);
      } catch (error) {
        reject(error);
      }
    };

    // 状态失败时候的处理函数
    const handleReject = () => {
      try {
        const res = onRejected(this.value);
        this.handlePromise(res, resolve, reject);
      } catch (error) {
        reject(error);
      }
    };
    if (this.status === PENDING) {
      this.onFulfilledCallbacks.push(() => {
        handleResolve();
      });

      this.onRejectedCallbacks.push(() => {
        handleReject();
      });
    } else if (this.status === FULFILLED) {
      handleResolve();
    } else if (this.status === REJECTED) {
      handleReject();
    }
  });

}

代码用了一个巧妙的方式,当 promise 的状态是 pending 的时候,会将 then 函数传入的onFulfilled/onRejected给保存起来,等到状态确定的时候,才调用对应的函数。

完美解决😄

测试代码:


	test("test pending", (done) => {
		jest.useFakeTimers();
		const onFulfilled = jest.fn();
		const onRejected = jest.fn();
		const myPromise = new MyPromise((resolve, reject) => {
			setTimeout(() => {
				resolve(1);
			}, 1000);
		}).then(onFulfilled, onRejected);

		expect(myPromise.status).toBe("pending");

		setTimeout(() => {
			expect(onFulfilled).toHaveBeenCalledWith(1);
			expect(onRejected).not.toHaveBeenCalled();
			done();
		}, 1000);

		jest.advanceTimersByTime(1000);
		jest.useRealTimers();
	});

测试结果:

这里的测试代码有一个小技巧,为了不让测试结果出来真的被阻塞 1 秒,我使用了 jest 的 fakeTime,让测试用例在 2ms 就能判断出结果

then 的异步性

真实的 then 是异步执行的,我们现在代码是同步的,现在改一改:

	then(onFulfilled, onRejected) {
		onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (value) => value;
		onRejected =
			typeof onRejected === "function"
				? onRejected
				: (reason) => {
						throw reason;
				  };
		return new MyPromise((resolve, reject) => {
			const handleResolve = () => {
				queueMicrotask(() => {
					try {
						const res = onFulfilled(this.value);
						this.handlePromise(res, resolve, reject);
					} catch (error) {
						reject(error);
					}
				});
			};

			const handleReject = () => {
				queueMicrotask(() => {
					try {
						const res = onRejected(this.value);
						this.handlePromise(res, resolve, reject);
					} catch (error) {
						reject(error);
					}
				});
			};
			if (this.status === PENDING) {
				this.onFulfilledCallbacks.push(() => {
					handleResolve();
				});

				this.onRejectedCallbacks.push(() => {
					handleReject();
				});
			} else if (this.status === FULFILLED) {
				handleResolve();
			} else if (this.status === REJECTED) {
				handleReject();
			}
		});
	}

我使用了queueMicrotask来实现异步的微任务效果

改完之后,测试代码出现了问题:

因为之前的 Promise 的测试写法是同步的写法,所以没等到结果出来,断言就开始了,所以会错:

test("test then on fulfilled promise", () => {
		const myPromise = new MyPromise((resolve, reject) => {
			resolve(1);
		});

		const spyOnFulfilled = jest.fn();
		const spyOnRejected = jest.fn();
		myPromise.then(spyOnFulfilled, spyOnRejected);
    // 同步断言!
		expect(spyOnFulfilled).toHaveBeenCalledWith(1);
		expect(spyOnRejected).not.toHaveBeenCalled();
	});

所有判断 then 的返回值的代码,都需要改改:

describe("test promise then", () => {
	test("test then on fulfilled promise", () => {
		const myPromise = new MyPromise((resolve, reject) => {
			resolve(1);
		});

		const spyOnFulfilled = jest.fn();
		const spyOnRejected = jest.fn();
    // 异步断言
		myPromise.then(spyOnFulfilled, spyOnRejected).then(() => {
			expect(spyOnFulfilled).toHaveBeenCalledWith(1);
			expect(spyOnRejected).not.toHaveBeenCalled();
		});
	});
});
describe("test return value of then", () => {
	test("test return value of then", () => {
		const myPromise = new MyPromise((resolve, reject) => {
			resolve(1);
		});

		const spyOnFulfilled = jest.fn((res) => {
			return res + 1;
		});
		const spyOnRejected = jest.fn();
		const newPromise = myPromise.then(spyOnFulfilled, spyOnRejected);

    // 异步断言
		newPromise.then(() => {
			expect(newPromise.status).toBe("fulfilled");
			expect(newPromise.value).toBe(2);
		});
	});
});

上面只给了部分代码,就不全写出来了

其实,修改测试代码本身,不就说明 then 的异步性生效了吗,其异步性还要单独写测试代码吗?

还是写一下吧😄。我打算通过函数执行的顺序来证明其异步性:

describe("test async of then", (done) => {
	test("test async of then", async () => {
		const myPromise = new MyPromise((resolve, reject) => {
			resolve(1);
		});
		const fn1 = jest.fn();
		const fn2 = jest.fn();
		const fn3 = jest.fn();

		fn1();

		const newPromise = myPromise.then(
			() =>fn2(),
			() => {}
		);

		fn3();

		newPromise.then(() => {
			const order1 = fn1.mock.invocationCallOrder[0];
			const order2 = fn2.mock.invocationCallOrder[0];
			const order3 = fn3.mock.invocationCallOrder[0];

			expect(order1).toBeLessThan(order2);
			expect(order3).toBeLessThan(order2);
			done();
		});
	});
});

测试结果:

promise 的 catch

**<font style="color:rgb(62, 175, 124);">catch</font>** 有点像 **<font style="color:rgb(62, 175, 124);">try...catch</font>**的写法,**<font style="color:rgb(62, 175, 124);">catch</font>** 中的代码,在 **<font style="color:rgb(62, 175, 124);">try</font>** 中抛出错误之后,就会被执行。而 **<font style="color:rgb(62, 175, 124);">promise</font>**中的 **<font style="color:rgb(62, 175, 124);">catch</font>**回调函数 ,在链式调用过程中抛出了错误,就会被执行。

class MyPromise{
  catch(onRejected){
    return this.then(null, onRejected);
  }
}

其实 catch 就是 then 函数的语法糖

测试代码:

test("test catch in fulfilled promise", async () => {
		const myPromise = new MyPromise((resolve, reject) => {
			resolve(1);
		});

		const spyOnCatch = jest.fn(() => {
			// 该断言不会被执行
			expect(res).toBe(1);
		});

		const res = await myPromise.catch(() => {});
		expect(spyOnCatch).not.toHaveBeenCalled();
		expect(res).toBe(1);
		// 判断断言执行的数量为2
		expect.assertions(2);
	});

	test("test catch in rejected promise", async () => {
		const myPromise = new MyPromise((resolve, reject) => {
			reject(1);
		});

		const spyOnCatch = jest.fn((res) => {
			// 该断言会被执行
			expect(res).toBe(1);
			return "ERROR";
		});

		const res = await myPromise.catch(spyOnCatch);
		expect(spyOnCatch).toHaveBeenCalled();
		expect(res).toBe("ERROR");
		// 判断断言执行的数量为3
		expect.assertions(3);
	});

测试结果:

promise 的 finally

finally 中回调函数,无论 promise 是失败还是成功,都会执行,不过 finally 中拿不到 promise 的值。 finally 后面还可以继续 then(因为 finally 也会返回 promise), 并且会原封不动的将状态和值传递给后面的 then

透露下,finally 也是 then 的语法糖,思考下,如何用 then 函数实现这一特性


class MyPromise {
  finally(onFinally) {
    onFinally = (res) => {
      onFinally();
      return res;
    };
    return this.then(onFinally, onFinally);
  }
}

测试代码:

test("test finally in fulfilled promise", async () => {
		const myPromise = new MyPromise((resolve, reject) => {
			resolve(1);
		});

		const spyOnFinally = jest.fn();

		const res = await myPromise.finally(spyOnFinally);
		expect(spyOnFinally).toHaveBeenCalled();
		expect(res).toBe(1);
	});

	test("test finally in rejected promise", async () => {
		const myPromise = new MyPromise((resolve, reject) => {
			reject(1);
		});
		const spyOnFinally = jest.fn();
		const res = await myPromise.then().finally(spyOnFinally);
		expect(spyOnFinally).toHaveBeenCalled();
		expect(res).toBe(1);
	});

测试结果:

完整代码

const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
class MyPromise {
	status = "pending";
	value = undefined;
	onFulfilledCallbacks = [];
	onRejectedCallbacks = [];
	constructor(executor) {
		executor(this.resolve.bind(this), this.reject.bind(this));
		return this;
	}

	resolve(value) {
		if (this.status === PENDING) {
			this.status = FULFILLED;
			this.value = value;

			this.onFulfilledCallbacks.forEach((callback) => {
				callback(value);
			});
		}
	}

	reject(reason) {
		if (this.status === PENDING) {
			this.status = REJECTED;
			this.value = reason;

			this.onRejectedCallbacks.forEach((callback) => {
				callback(reason);
			});
		}
	}

	then(onFulfilled, onRejected) {
		onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (value) => value;
		onRejected =
			typeof onRejected === "function"
				? onRejected
				: (reason) => {
						throw reason;
				  };
		return new MyPromise((resolve, reject) => {
			const handleResolve = () => {
				queueMicrotask(() => {
					try {
						const res = onFulfilled(this.value);
						this.handlePromise(res, resolve, reject);
					} catch (error) {
						reject(error);
					}
				});
			};

			const handleReject = () => {
				queueMicrotask(() => {
					try {
						const res = onRejected(this.value);
						this.handlePromise(res, resolve, reject);
					} catch (error) {
						reject(error);
					}
				});
			};
			if (this.status === PENDING) {
				this.onFulfilledCallbacks.push(() => {
					handleResolve();
				});

				this.onRejectedCallbacks.push(() => {
					handleReject();
				});
			} else if (this.status === FULFILLED) {
				handleResolve();
			} else if (this.status === REJECTED) {
				handleReject();
			}
		});
	}

	handlePromise(x, resolve, reject) {
		if (x instanceof MyPromise) {
			x.then(
				(res) => {
					return this.handlePromise(res, resolve, reject);
				},
				(err) => {
					reject(err);
				}
			);
		} else {
			resolve(x);
		}
	}

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

	finally(onFinally) {
		onFinally = (res) => {
			onFinally();
			return res;
		};
		return this.then(onFinally, onFinally);
	}
}

module.exports = MyPromise;

代码地址: github.com/zenoskongfu…

所有的测试均已通过:

这个版本实现的功能是基础的,方便初学者学习的,所以省略了很多边界条件,与真实Promise 实用性上有些差距。

但是其核心功能已经全部实现,且核心实现也是贴近真实 Promise 的,大家可以放心食用

总结

到这里 then 以及 promise 相关的实例方法都介绍地差不多了。

下篇文章来实现 promise 的静态方法:

  • Promise.resolve(value) —— 使用给定 value ,直接创建一个 resolved 的 promise。
  • Promise.reject(error) —— 使用给定 error,直接创建一个 rejected 的 promise。
  • Promise.all(promises) —— 等待所有 promise 都 resolve 时,返回存放它们结果的数组。如果给定的任意一个 promise 为 reject,那么它就会变成 Promise.all 的 error,所有其他 promise 的结果都会被忽略。
  • Promise.race(promises) —— 等待第一个 settle 的 promise,并将其 result/error 作为结果返回。
  • Promise.any(promises)(ES2021 新增方法)—— 等待第一个 fulfilled 的 promise,并将其结果作为结果返回。如果所有 promise 都 rejected,Promise.any 则会抛出 AggregateError 错误类型的 error 实例。
  • Promise.allSettled(promises)(ES2020 新增方法)—— 等待所有 promise 都 settle 时,并以包含以下内容的对象数组的形式返回它们的结果:
    • status: "fulfilled" 或 "rejected"
    • value(如果 fulfilled)或 reason(如果 rejected)。