异步处理在程序开发世界里很常见,异步可以简单描述为:现在发起的请求需要在未来某个时间点得到答复。在早期的前端开发世界里,我们会看到代码中充斥着回调函数片段,请求响应处理、定时器等操作都需借助回调函数来完成异步操作。
假如我们希望某个函数具有等待的效果,在等待完后,打印传入的参数。代码片段示例如下:
function wait(fn, time) {setTimeout(() => {fn('Yeah');}, time);}wait(function(message) {console.log(message);}, 1000);
通常在一秒后会打印出 “Yeah”。从本例来看,回调函数能够很好解决异步处理问题,但在有些时候,回调函数会使得代码可读性急剧降低,比如以下代码:
f1(function() {const t1 = 'f1';console.log(t1);f2(function() {const t2 = `${t1},f2`;console.log(t2);f3(function() {const t3 = `${t2},f3`;console.log(f3);});});});
如此嵌套,代码可读性会急剧降低,正如术语:回调地狱。
有什么办法可以解决这个问题吗?有,那就是 Promise。我们使用 Promise 改写上面的代码,就像这样:
f1().then(() => {const t1 = 'f1';console.log(t1);return t1;}).then(t1 => {return f2().then(function() {const t2 = `${t1},f2`;console.log(t2);return t2;});}).then(t2 => {return f3().then(function() {const t3 = `${t2},f3`;console.log(f3);return t3;});});
这样我们就将一层一层嵌套调用的形式改写成了平级调用,提高了代码可读性。Promise 是如何实现的呢?我们要从一种设计模式说起。
阅读前可先回顾一下观察者模式与发布-订阅模式的区别
观察者模式即是先收集所有依赖,待到需要执行时,从依赖中取出函数并依次执行。这种思路在事件处理中得到充分运用,针对某个 DOM 元素,绑定事件的过程相当于声明依赖,待到事件触发时,则将这些依赖取出依次执行。
在 Promise 中,我们知道任务的处理完成状态分为 resolve 和 reject,针对于这两种状态,我们事先需要将各自依赖存储,待到异步任务执行完再取出这些依赖函数依次执行。我们先写一部分片段:
type FuncType = (...args: any[]) => any;export class HePromise {private resolves: FuncType[] = [];constructor(executor) {}then(resolveFunc: FuncType) {this.resolves.push(resolveFunc);}}
可以看到 Promise 依赖的收集是在 then 函数中完成,当 Promise 示例每调用一次 then,便将 then 中传入的第一个函数放入依赖数组中。不过我们知道 Promise 的初始化是在实例化时完成,因此在构造器中需要执行 Promise 实例化传入的函数,代码改造后如下:
type FuncType = (...args: any[]) => any;type ExecutorFunc = (resolveFunc: FuncType, rejectFunc?: FuncType) => any;export class HePromise {private resolves: FuncType[] = [];constructor(executor: ExecutorFunc) {const { resolve } = this;executor(resolve);}private resolve = (resolvedVal: any) => {const { resolves } = this;while (resolves.length) {const cb = resolves.shift();if (cb) cb(resolvedVal);}};then(resolveFunc: FuncType) {this.resolves.push(resolveFunc);}}
完整执行过程是:当执行 new HePromise()
时,constructor
函数会执行,不过这里需要注意的是,我们暂时只考虑异步操作,忽略了同步的情况。异步情况下 executor
函数会在未来某个时间点执行,而从初始化到这个时间点之间,正是 then
函数执行收集依赖的过程。
除此之外,我们还需要考虑 reject
的操作,这个操作与 resolve
表征类似,我们直接添加到上面代码中,添加后完整代码示例如下:
type FuncType = (...args: any[]) => any;type ExecutorFunc = (resolveFunc: FuncType, rejectFunc?: FuncType) => any;export class HePromise {private resolves: FuncType[] = [];private rejects: FuncType[] = [];constructor(executor: ExecutorFunc) {const { resolve, reject } = this;executor(resolve, reject);}private resolve = (resolvedVal: any) => {const { resolves } = this;while (resolves.length) {const cb = resolves.shift();if (cb) cb(resolvedVal);}};private reject = (rejectedVal: any) => {const { rejects } = this;while (rejects.length) {const cb = rejects.shift();if (cb) cb(rejectedVal);}};then(resolveFunc: FuncType, rejectFunc?: FuncType) {this.resolves.push(resolveFunc);if (rejectFunc) this.rejects.push(rejectFunc);}}
接下来,我们编写测试代码,本项目中使用 jest 进行测试,测试代码编写如下:
describe('test HePromise', () => {it('basic usage', done => {const p = new HePromise(resolve => {setTimeout(() => {resolve(1);}, 1000);});try {p.then(data => {expect(data).toBe(1);done();});} catch (error) {done(error);}});});
执行测试,测试通过,完成 Promise 初始版本封装。执行流程总结如下:
executor
;executor
内部,将各任务放入宏/微任务队列中(宏/微任务请参看 事件循环 );then
和 catch
中可收集到 resolve
、reject
依赖,并将该依赖存放到对应队列中;executor
中的 resolve
或 reject
,取出对应队列中的依赖依次执行。英文原文:Promise/A+
Promise 的状态
一个 Promise 的当前状态必须为以下三种状态中的一种:等待态(Pending)、**执行态(Fulfilled)**和 拒绝态(Rejected)。
等待态(Pending)处于等待态时,promise 需满足以下条件:
可以迁移至执行态或拒绝态
执行态(Fulfilled)处于执行态时,promise 需满足以下条件:
不能迁移至其他任何状态
必须拥有一个不可变的终值
拒绝态(Rejected)处于拒绝态时,promise 需满足以下条件:
不能迁移至其他任何状态
必须拥有一个不可变的据因
这里的不可变指的是恒等(即可用 === 判断相等),而不是意味着更深层次的不可变(译者注:盖指当 value 或 reason 不是基本值时,只要求其引用地址相等,但属性值可被更改)。
then 方法
一个 promise 必须提供一个 then 方法以访问其当前值、终值和据因。
promise 的 then 方法接受两个参数:
promise.then(onFulfilled, onRejected);
其中,onFulfilled 和 onRejected 都是可选参数。
......
我们为 HePromise 添加状态,根据规范约定,在代码中添加状态枚举值,如下:
enum STATUS {PENDING = 'pending',FULFILLED = 'fulfilled',REJECTED = 'rejected',}
在执行 resolve
前,需要检测当前状态是否为 pending
,如果是则可以继续执行,否则无法执行 resolve
,在执行 resolve
时,将状态置为 fulfilled
。reject
方法中同理先检测状态是否为 pending
,如果是则继续执行并将状态置为 rejected
。
改进后,代码示例如下:
type FuncType = (...args: any[]) => any;type ExecutorFunc = (resolveFunc: FuncType, rejectFunc?: FuncType) => any;enum STATUS {PENDING = 'pending',FULFILLED = 'fulfilled',REJECTED = 'rejected',}export class HePromise {private status = STATUS.PENDING;private resolves: FuncType[] = [];private rejects: FuncType[] = [];constructor(executor: ExecutorFunc) {const { resolve, reject } = this;executor(resolve, reject);}private resolve = (resolvedVal: any) => {const { resolves, status } = this;if (status !== STATUS.PENDING) return;this.status = STATUS.FULFILLED;while (resolves.length) {const cb = resolves.shift();if (cb) cb(resolvedVal);}};private reject = (rejectedVal: any) => {const { rejects, status } = this;if (status !== STATUS.PENDING) return;this.status = STATUS.REJECTED;while (rejects.length) {const cb = rejects.shift();if (cb) cb(rejectedVal);}};then(resolveFunc: FuncType, rejectFunc?: FuncType) {this.resolves.push(resolveFunc);if (rejectFunc) this.rejects.push(rejectFunc);}}
根据 Promise A+ 规范,每次 then 返回的值也需要满足 thenable,也就是说我们需要将 resolve 返回值使用 promise 包裹,在本例中就是需要将返回值包装为新的 HePromise 对象。 开发之前我们不妨先来看看 Promise 链式调用的示例:
const p = new Promise(resolve => resolve(1));p.then(r1 => {console.log(r1);return 2;}).then(r2 => {console.log(r2);return 3;}).then(r3 => {console.log(r3);});
可以发现,每次 then 函数调用完,都返回了一个新的数字,令人不解的是,这个数据居然也拥有了 then 函数,可以依次调用。这里需要做的处理时,需要将传入的 resolve 与 reject 函数封装然后放入待执行队列中。简言之,当返回值为一个 Promise 时,需要执行 promise.then 方法,否则直接执行 resolve。改进后的 then 方法如下:
then(resolveFunc: FuncType, rejectFunc?: FuncType) {return new HePromise((resolve, reject) => {const resolvedFn = (val: any) => {try {let resolvedVal = resolveFunc(val);resolvedVal instanceof HePromise? resolvedVal.then(resolve, reject): resolve(resolvedVal);} catch (error) {if (reject) reject(error);}};this.resolves.push(resolvedFn);if (rejectFunc) this.rejects.push(rejectFunc);})}
可以看到,then 方法调用时,会返回新的 HePromise 对象,该对象中主要做了这样几件事情:
同理将 reject 处理补全,整体代码示例:
then(resolveFunc: FuncType, rejectFunc?: FuncType) {return new HePromise((resolve, reject) => {const resolvedFn = (val: any) => {try {const resolvedVal = resolveFunc(val);resolvedVal instanceof HePromise? resolvedVal.then(resolve, reject): resolve(resolvedVal);} catch (error) {if (reject) reject(error);}};this.resolves.push(resolvedFn);const rejectedFn = (val: any) => {if (rejectFunc) {try {const rejectedVal = rejectFunc(val);rejectedVal instanceof HePromise? rejectedVal.then(resolve, reject): resolve(rejectedVal);} catch (error) {if (reject) reject(error);}}};if (rejectFunc) this.rejects.push(rejectedFn);});}
完成编码后,编写测试代码:
it('chain invoke usage', done => {const p = new HePromise(resolve => {setTimeout(() => {resolve(11);}, 1000);});try {p.then(data => {expect(data).toBe(11);return 'hello';}).then(data => {expect(data).toBe('hello');return 'world';}).then(data => {expect(data).toBe('world');done();});} catch (error) {done(error);}});
执行测试,可以看到测试用例通过。
不过需要注意的是,根据 Promise A+ 规范,需要对 then 参数进行处理,如果参数不是函数,则需要忽略并继续往下执行,示例如下:
typeof resolveFunc !== 'function' ? (resolveFunc = value => value) : null;typeof rejectFunc !== 'function'? (rejectFunc = reason => {throw new Error(reason instanceof Error ? reason.message : reason);}): null;
与此同时,如果在执行过程中,Promise 状态值已发生变化,则需要根据不同状态直接进行相应,例如,如果是 pending
,则将任务放入对应队列中,如果为 fulfilled
,直接调用 resolve,如果为 rejected
则直接调用 reject。可以使用 switch 语句进行策略处理,如下:
switch (this.status) {case STATUS.PENDING:this.resolves.push(resolvedFn);this.rejects.push(rejectedFn);break;case STATUS.FULFILLED:resolvedFn(this.value);break;case STATUS.REJECTED:rejectedFn(this.value);break;}
此处 this.value
是上次执行完后得到的值,起到暂存的目的。补充以上代码后,完整代码示例如下:
type FuncType = (...args: any[]) => any;type ExecutorFunc = (resolveFunc: FuncType, rejectFunc?: FuncType) => any;enum STATUS {PENDING = 'pending',FULFILLED = 'fulfilled',REJECTED = 'rejected',}export class HePromise {private status = STATUS.PENDING;private value = undefined;private resolves: FuncType[] = [];private rejects: FuncType[] = [];constructor(executor: ExecutorFunc) {const { resolve, reject } = this;executor(resolve, reject);}private resolve = (resolvedVal: any) => {const { resolves, status } = this;if (status !== STATUS.PENDING) return;this.status = STATUS.FULFILLED;this.value = resolvedVal;while (resolves.length) {const cb = resolves.shift();if (cb) cb(resolvedVal);}};private reject = (rejectedVal: any) => {const { rejects, status } = this;if (status !== STATUS.PENDING) return;this.status = STATUS.REJECTED;this.value = rejectedVal;while (rejects.length) {const cb = rejects.shift();if (cb) cb(rejectedVal);}};then(resolveFunc: FuncType, rejectFunc?: FuncType): HePromise {typeof resolveFunc !== 'function' ? (resolveFunc = value => value) : null;typeof rejectFunc !== 'function'? (rejectFunc = reason => {throw new Error(reason instanceof Error ? reason.message : reason);}): null;return new HePromise((resolve, reject) => {const resolvedFn = (val: any) => {try {const resolvedVal = resolveFunc(val);resolvedVal instanceof HePromise? resolvedVal.then(resolve, reject): resolve(resolvedVal);} catch (error) {if (reject) reject(error);}};this.resolves.push(resolvedFn);const rejectedFn = (val: any) => {if (rejectFunc) {try {const rejectedVal = rejectFunc(val);rejectedVal instanceof HePromise? rejectedVal.then(resolve, reject): resolve(rejectedVal);} catch (error) {if (reject) reject(error);}}};switch (this.status) {case STATUS.PENDING:this.resolves.push(resolvedFn);this.rejects.push(rejectedFn);break;case STATUS.FULFILLED:resolvedFn(this.value);break;case STATUS.REJECTED:rejectedFn(this.value);break;}});}}
以上情况我们遗漏了一个点,就是同步任务,我们可以看到以上示例中,初始化 HePromise 中的 resolve 都是在未来进行的,如果同步执行 resolve,则以上代码会出现问题。我们的方案是,将初始处理默认放入宏任务队列中,也就是使用 setTimeout
包裹 resolve,这样一来,就能保证即使是同步任务,也可以保证在同步收集完任务以后在执行 executor 中的 resolve 和 reject。示例如下:
export class HePromise {private resolve = (resolvedVal: any) => {setTimeout(() => {const { resolves, status } = this;if (status !== STATUS.PENDING) return;this.status = STATUS.FULFILLED;this.value = resolvedVal;while (resolves.length) {const cb = resolves.shift();if (cb) cb(resolvedVal);}});};}
同理可实现 reject 逻辑。编写测试代码,如下:
it('sync task', done => {const p = new HePromise(resolve => {resolve(123);});p.then(res => {expect(res).toBe(123);done();});});
Promise 中还包括 catch、finally、Promise.resolve、Promise.reject、Promise.all、Promise.race,接下来我们分别来实现。
其实我们可以理解是 then 方法的一个变体,就是 then 方法省略了 resolve 参数,实现如下:
catch(rejectFnnc) {return this.then(undefined, rejectFnnc)}
该方法保证 Promise 不管是 fulfilled 还是 reject 都会执行,都会执行指定的回调函数。在 finally 之后,还可以继续 then。并且会将值原封不动的传递给后面的 then 函数。针对这个机制也有很多理解,糙版的处理如下:
finally(cb) {return this.then(value => {cb();return value;},reason => {cb();throw reason})}
不过,如果 Promise 在 finally 前返回了一个 reject 状态的 promise,想上面这样编写是无法满足要求的。
finally 对自身返回的 promise 的决议影响有限,它可以将上一个 resolve 改为 reject,也可以将上一个 reject 改为另一个 reject,但不能把上一个 reject 改为 resolve。
这样一来,我们可以将 callback 使用 Promise.resolve 包裹一下,保证后续的 resolve 状态。如下:
finally(cb) {return this.then(value => HePromise.resolve(cb()).then(() => value),reason => HePromise.resolve(cb()).then(() => { throw reason }))}
调用该静态方法其实就是将值 promise 化,如果传入值本身就是 promise 示例,则直接返回,否则创建新的 promise 示例并返回,示例如下:
static resolve(val) {if(val instanceof HePromise) return valreturn new HePromise(resolve => resolve(val))}
编写测试代码如下:
it('HePromise.resolve', done => {HePromise.resolve(1).then(res => {expect(res).toBe(1);done();});});
该方法的原理同 resolve
,直接贴出代码
static reject(val) {return new HePromise((resolve, reject) => reject(val))}
编写测试代码如下:
it('HePromise.reject & catch', done => {HePromise.reject(1).then(res => {expect(res).toBe(1);done();},error => {expect(error).toBe(1);done();},);});
或者通过 catch 的方式,如下:
it('HePromise.reject & catch', done => {HePromise.reject(1).then(res => {expect(res).toBe(1);done();}).catch(error => {expect(error.message).toEqual('1');done();});});
执行测试,测试通过。
就是将传入数组中的值 promise 化,然后保证每个任务都处理后,最终 resolve。示例如下:
class HePromise {static all(promises: any[]) {let index = 0;const result: any[] = [];const pLen = promises.length;return new HePromise((resolve, reject) => {promises.forEach(p => {HePromise.resolve(p).then(val => {index++;result.push(val);if (index === pLen) {resolve(result);}},err => {if (reject) reject(err);},);});});}}
编写测试用例如下:
it('HePromise.all', done => {HePromise.all([1, 2, 3]).then(res => {expect(res).toEqual([1, 2, 3]);done();});});
执行测试,测试通过。
就是将传入数组中的值 promise 化,只要其中一个任务完成,即可 resolve。示例如下:
class HePromise {static race(promises: any[]): HePromise {return new HePromise((resolve, reject) => {promises.forEach(p => {HePromise.resolve(p).then(val => {resolve(val);},err => {if (reject) reject(err);},);});});}}
编写测试用例:
it('HePromise.race', done => {HePromise.race([11, 22, 33]).then(res => {expect(res).toBe(11);done();});});
执行测试,测试通过。
整体测试代码情况如下:
type FuncType = (...args: any[]) => any;type ExecutorFunc = (resolveFunc: FuncType, rejectFunc?: FuncType) => any;enum STATUS {PENDING = 'pending',FULFILLED = 'fulfilled',REJECTED = 'rejected',}export class HePromise {private status = STATUS.PENDING;private value = undefined;private resolves: FuncType[] = [];private rejects: FuncType[] = [];static resolve(val: any): HePromise {if (val instanceof HePromise) return val;return new HePromise(resolve => resolve(val));}static reject(val: any): HePromise {return new HePromise((resolve, reject) => reject && reject(val));}static all(promises: any[]): HePromise {let index = 0;const result: any[] = [];const pLen = promises.length;return new HePromise((resolve, reject) => {promises.forEach(p => {HePromise.resolve(p).then(val => {index++;result.push(val);if (index === pLen) {resolve(result);}},err => {if (reject) reject(err);},);});});}static race(promises: any[]): HePromise {return new HePromise((resolve, reject) => {promises.forEach(p => {HePromise.resolve(p).then(val => {resolve(val);},err => {if (reject) reject(err);},);});});}constructor(executor: ExecutorFunc) {const { resolve, reject } = this;executor(resolve, reject);}private resolve = (resolvedVal: any) => {const { resolves, status } = this;if (status !== STATUS.PENDING) return;this.status = STATUS.FULFILLED;this.value = resolvedVal;while (resolves.length) {const cb = resolves.shift();if (cb) cb(resolvedVal);}};private reject = (rejectedVal: any) => {const { rejects, status } = this;if (status !== STATUS.PENDING) return;this.status = STATUS.REJECTED;this.value = rejectedVal;while (rejects.length) {const cb = rejects.shift();if (cb) cb(rejectedVal);}};then(resolveFunc?: FuncType, rejectFunc?: FuncType): HePromise {typeof resolveFunc !== 'function' ? (resolveFunc = value => value) : null;typeof rejectFunc !== 'function'? this.rejects.length < 1? (rejectFunc = reason => {throw new Error(reason instanceof Error ? reason.message : reason);}): null: null;return new HePromise((resolve, reject) => {const resolvedFn = (val: any) => {try {const resolvedVal = resolveFunc && resolveFunc(val);resolvedVal instanceof HePromise? resolvedVal.then(resolve, reject): resolve(resolvedVal);} catch (error) {if (reject) reject(error);}};this.resolves.push(resolvedFn);const rejectedFn = (val: any) => {if (rejectFunc) {try {const rejectedVal = rejectFunc(val);rejectedVal instanceof HePromise? rejectedVal.then(resolve, reject): resolve(rejectedVal);} catch (error) {if (reject) reject(error);}}};switch (this.status) {case STATUS.PENDING:this.resolves.push(resolvedFn);this.rejects.push(rejectedFn);break;case STATUS.FULFILLED:resolvedFn(this.value);break;case STATUS.REJECTED:rejectedFn(this.value);break;}});}catch(rejectFnnc: FuncType) {return this.then(undefined, rejectFnnc);}finally(cb) {return this.then(value => HePromise.resolve(cb()).then(() => value),reason =>HePromise.resolve(cb()).then(() => {throw reason;}),);}}