12

一步一步手写完美符合PromiseA+规范的Promise

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzA4Nzg0MDM5Nw%3D%3D&%3Bmid=2247488902&%3Bidx=1&%3Bsn=56d8440f9cd415d5f6762cd840503685
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

关注  高级前端进阶 ,回复“ 加群

加入我们一起学习,天天进步

作者:晴天酱85223

来源:https://juejin.cn/post/6900834452901986312

前言

Promise作为异步编程的一种解决方案,已经变得十分常用。而手写Promise也是面试中的高频题,今天我们就来一步一步完成一个完美符合PromiseA+规范的Promise吧

准备工作

PromiseA+规范翻译

node 版本 v12.10.0

Chrome 版本 71+

我们使用 promises-aplus-tests (版本 2.1.2) 来测试 我们写的 PromiseZ

全局安装

npm install promises-aplus-tests -g

在PromiseZ (例如 index.js) 中添加

PromiseZ.deferred = function() {
let defer = {};
defer.promise = new PromiseZ((resolve, reject) => {
defer.resolve = resolve;
defer.reject = reject;
});
return defer;
}
module.exports = PromiseZ;

执行脚本

promises-aplus-tests index.js

在手写过程中,如果遇到某些地方不理解的情况,可以根据控制台中爆红的提示,在promises-aplus-tests 定位相应的测试用例,便于加深理解

1· 基本使用

首先呢,先来看看比较常用的写法

new Promise((resolve, reject) => {
queueMicrotask(() => {
resolve('resolved');
}, 2000);
}).then(res => {
console.log(res);
})
// 输出结果 resolved

我们知道 Promise有三种状态: pending, fulfilled, rejected

状态只能由 pending 转为 fulfilled 或者 rejected, 且状态不可逆。

Promise作为构造函数时,会将一个函数作为它的参数传入

并且Promise是一个含有 then方法的函数

基于此,先写一个最基本的

const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

function PromiseZ(fn) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined
this.onFulfilledCallback; // 需要在then方法里赋值
this.onRejectedCallback; // 需要在then方法里赋值
const me = this;
function resolve(value) {
if (me.status === PENDING) {
me.status = FULFILLED;
me.value = value;
me.onFulfilledCallback && me.onFulfilledCallback(value);
}
}
function reject(reason) {
if (me.status === PENDING) {
me.status = REJECTED;
me.reason = reason;
me.onRejectedCallback && me.onRejectedCallback(reason);
}
}
try {
fn(resolve, reject);
} catch (e) {
reject(e);
}
}

PromiseZ.prototype.then = function (onFulfilled, onRejected) {
const me = this;
const onFulfilledCallback = typeof onFulfilled === 'function' ? onFulfilled : value => value;
const onRejectedCallback = typeof onRejected === 'function' ? onRejected : reason => { throw reason };
if (me.status === FULFILLED) {
onFulfilledCallback(me.value);
} else if (me.status === REJECTED) {
onRejectedCallback(me.reason);
} else {
me.onFulfilledCallback = onFulfilledCallback;
me.onRejectedCallback = onRejectedCallback;
}
}

解析:当PromiseZ的参数fn是同步执行resolve或者reject时,在调用.then时,状态已经不再是pending,则直接调用 onFulfilledCallback 或者 onRejectedCallback即可;

当fn是异步执行resolve或者reject时,调用.then时状态还处于 pending。需要将onFulfilledCallback、onRejectedCallback赋值到this,通过resolve/reject来执行回调。

2.多次调用.then

我们知道,Promise是可以多次调用then方法的,例如

let p = new Promise((res) => {
queueMicrotask(() => {
res(10);
}, 1000)
});
p.then(v => {
console.log(v + 1);
});
p.then(v => {
console.log(v + 2);
});
// 输出结果 11 12

所以 this.onFulfilledCallback 和 this.onRejectedCallback 应当是个数组结构,接收多个then内传入的方法;

function PromiseZ(fn) {
/** 省略 **/
- this.onFulfilledCallback;
- this.onRejectedCallback;
+ this.onFulfilledCallbacks = [];
+ this.onRejectedCallbacks = [];
function resolve(value) {
if (me.status === PENDING) {
me.status = FULFILLED;
me.value = value;
- me.onFulfilledCallback && me.onFulfilledCallback(value);
+ me.onFulfilledCallbacks.forEach(cb => cb(value));
}
}
function reject(reason) {
if (me.status === PENDING) {
me.status = REJECTED;
me.reason = reason;
- me.onRejectedCallback && me.onRejectedCallback(reason);
+ me.onRejectedCallbacks.forEach(cb => cb(reason));
}
}
/** 省略 **/
}

PromiseZ.prototype.then = function (onFulfilled, onRejected) {
/** 省略 **/
else {
- me.onFulfilledCallback = onFulfilledCallback;
- me.onRejectedCallback = onRejectedCallback;
+ me.onFulfilledCallbacks.push(onFulfilledCallback);
+ me.onFulfilledCallbacks.push(onFulfilledCallback);
}
}

这下我们可以一个Promise多次调用then方法了

3.onFulfilled 和 onRejected 应该是微任务

Promise A+ 规范里有以上这么一条规范,该怎么理解呢,我们来看看下面这两个栗子。

console.log('start');
new Promise(resolve => {
resolve('resolved');
}).then(() => {
console.log('then');
});
console.log('end');
// 输出顺序为 start resolved end then

然而使用我们的PromiseZ的输出顺序 是  start resolved  then end

原因是在执行 then方法时 状态已经变为 FULFILLED/REJECTED,我们立刻执行了onFulfilledCallback/onRejectedCallback,导致整个执行顺序并不符合PromiseA+规范。

再看一个栗子

let resolve1;
console.log('start');
new Promise(resolve => {
console.log('pending');
resolve1 = resolve;
}).then(() => {
console.log('then');
});
resolve1();
console.log('end');
// 输出顺序为 start pending end then

使用我们的PromiseZ的输出顺序为 start pending then end 在执行 then方法时, 状态为pending,所以将我们立刻执行了onFulfilledCallback 推入数组队列中。当执行resolve1 后,状态发生变更,立刻将队列中的所有方法都执行,导致不符合预期。

为解决以上问题,我们使用queueMicrotask来实现微任务。queueMicrotask 这个Api还比较新,也可以使用setTimeout来模拟

function PromiseZ(fn) {
/** 省略 **/
function resolve(value) {
if (me.status === PENDING) {
me.status = FULFILLED;
me.value = value;
+ queueMicrotask(() => {
me.onFulfilledCallbacks.forEach(cb => cb(value));
+ });

}
}
function reject(reason) {
if (me.status === PENDING) {
me.status = REJECTED;
me.reason = reason;
+ queueMicrotask(() => {
me.onRejectedCallbacks.forEach(cb => cb(reason));
+ });
}
}
}

PromiseZ.prototype.then = function (onFulfilled, onRejected) {
/** 省略 **/
if (me.status === FULFILLED) {
+ queueMicrotask(() => {
onFulfilledCallback(me.value);
+ });
} else if (me.status === REJECTED) {
+ queueMicrotask(() => {
onRejectedCallback(me.reason);
+ });
} else {
/** 省略 **/
}
}

4.链式调用

new Promise().then(dothing1).then(dothing2) 这种调用已经非常常见了,本质上是每次执行then方法后都返回一个新的Promise(注:是新的Promise,不再是初始的那个)

let p = new Promise(res => res(2));
let then = p.then(v => v);
then instanceof Promise // true
then === p // false

对then方法进行重写,让其返回一个新的PromiseZ: promise2

PromiseZ.prototype.then = function (onFulfilled, onRejected) {
/** 省略 **/
+ let promise2 = new PromiseZ((resolve, reject) => {
if (me.status === FULFILLED) {
queueMicrotask(() => {
+ try {
- onFulfilledCallback(me.value);
+ let x = onFulfilledCallback(me.value);
+ resolve(x);
+ } catch(e) {
+ reject(e);
+ }
});
} else if (me.status === REJECTED) {
queueMicrotask(() => {
+ try {
- onRejectedCallback(me.reason);
+ let x = onRejectedCallback(me.reason);
+ resolve(x); // 这里使用resolve而不是reject
+ } catch(e) {
+ reject(e);
+ }
});
} else {
- me.onFulfilledCallbacks.push(onFulfilledCallback);
- me.onFulfilledCallbacks.push(onFulfilledCallback);
+ me.onFulfilledCallbacks.push((value) => {
+ try {
+ let x = onFulfilledCallback(value);
+ resolve(x);
+ } catch (e) {
+ reject(e);
+ }
+ });
+ me.onRejectedCallbacks.push((reason) => {
+ try {
+ let x = onRejectedCallback(reason);
+ resolve(x); // 这里使用resolve而不是reject
+ } catch(e) {
+ reject(e);
+ }
+ });
}
+ })
+ return promise2;
}

这里使用resolve而不是reject 是因为当我们在then方法中的onRejected 接收到了上一个错误,说明我们对预期的错误进行了处理,进行下一层传递时应该执行下一个then的onFulfilled,除非在执行本次resolve时又出现了其他错误

测试一下

console.log('start');
new PromiseZ(res => {
queueMicrotask(() => {
console.log('resolve');
res(10);
}, 3000)
}).then(v => {
console.log('then1');
return v + 3;
}).then(v => {
console.log('then2');
console.log(v);
})
console.log('end');
// 输出 start end resolve then1 then2 13
// 符合预期

5. x是一个Promise

在上一个环节,我们定义了一个变量x用来接收 onFulfilledCallback/onRejectedCallback 的结果。提供的测试用例也都不是PromiseZ类型的。

如果x也是一个PromiseZ的话,那么promise2的状态就要取决于 x 的状态

例如

console.log('start');
new Promise((res) => {
console.log('promise1 pending');
queueMicrotask(() => {
console.log('promise1 resolve');
res(1);
}, 2000);
}).then(v => {
console.log(`then1: ${v}`);
return new Promise(res => {
console.log(`promise2 pending: ${v}`);
queueMicrotask(() => {
console.log(`promise2 resolve: ${v}`);
res(v + 3);
}, 2000);
})
}).then(v => {
console.log(`then2: ${v}`);
});
console.log('end');
// 输出结果
start
promise1 pending
end
promise1 resolve
then1: 1
promise2 pending: 1
promise2 resolve: 1
then2: 4

我们这里定义一个resolvePromise桥梁函数,用于对x与promise2的状态进行连接 resolve(promise2, x, resolve, reject); 其中resolve、reject都是由promise2提供的,可以理解为 当x的状态变为FULFILLED/REJECTED时,再来调用resolve/reject来改变promise2的状态

PromiseZ.prototype.then = function (onFulfilled, onRejected) {
/** 省略 **/
let promise2 = new PromiseZ((resolve, reject) => {
/** 省略 **/
- resolve(x);
+ resolvePromise(promise2, x, resolve, reject);
/** 省略 **/

});
}
function resolvePromise(promise2, x, resolve, reject) {
if (x instanceof PromiseZ) {
try {
let then = x.then;
// 递归调用
then.call(x, y => {
resolvePromise(promise2, y, resolve, reject);
}, r => {
reject(r);
});
} catch (e) {
reject(e);
}
} else {
resolve(x);
}
}

递归调用:当x的状态变为FULFILLED,resolve的结果 y 可能又是一个PromiseZ,promise2的状态又再次依赖于y...... 所以我们需要对此进行递归调用;

6. x 是一个 thenable

首先,Promise规范给出的的 thenable定义

'thenable' 是一个定义then方法的对象或者函数

我们先来举几个栗子

new Promise(res => res(10)).then(v => {
return {
other: v,
then: v + 2
}
}).then(ans => {
console.log(ans);
});

new Promise(res => res(10)).then(v => {
return {
other: v,
then: () => {
return v + 2;
}
}
}).then(ans => {
console.log(ans);
});

new Promise(res => res(10)).then(v => {
return {
other: v,
then: (res, rej) => {
res(v + 2);
}
}
}).then(ans => {
console.log(ans);
});

来猜测一下上面三个then方法的输出结果,下面是正确的返回结果

// 第一个
{
other: 10,
then: 12
}
// 第二个
// 不会打印,即不会then方法里的代码(Promise状态一直在pending)
// 第三个
12

综上,Promise对thenable做特殊处理,将其也当做一个Promise来进行处理

function resolvePromise(promise2, x, resolve, reject) {
- if (x instanceof PromiseZ) {
+ if (typeof x === 'object' && x || typeof x === 'function') {
try {
let then = x.then;
+ if (type of then === 'function')
then.call(x, y => {
resolvePromise(promise2, y, resolve, reject);
}, r => {
reject(r);
});
+ } else {
+ resolve(x);
+ }
} catch (e) {
reject(e);
}
} else {
/** 省略 **/
}
}

【x 是一个thenable】 实际上是包含了【x是一个Promise】的情况

到这里,我们已经实现了Promise的大部分功能,但是要想完全符合Promise规范,还得继续调整一下

7. x === promise2

在运行测试用例时,发现当 x === promise2时,产生了循环引用。来看个简单的测试用例

let promise = new PromiseZ(res => res()).then(function () {
return promise;
});

当产生了循环引用时, 直接reject出一个TypeError

function resolvePromise(promise2, x, resolve, reject) {
+ if (x === promise2) {
+ reject(new TypeError('chaining cycle'));
+ } else if (typeof x === 'object' && x || typeof x === 'function') {
/** 省略 **/
} else {
resolve(x);
}
}

8. thenable中只能resolve/reject一次

在前面我们就提过,Promise的状态是不可逆的,在执行完resolve或者reject之后,再次执行resolve或者reject应该被忽略掉,在PromiseZ中我们已经加入了这样逻辑(判断状态)。同样的,在thenable中,我们也应该遵守这种规定。看下面的测试用例。

new Promise(res => res()).then(() => {
return {
then: function (onFulfilled) {
// 第一个onFulfilled
onFulfilled({
then: function (onFulfilled) {
queueMicrotask(function () {
onFulfilled('onFulfilled1');
}, 0);
}
});
// 第二个onFulfilled
onFulfilled('onFulfilled2');
}
};
}).then(value => {
console.log(value);
});
// 正确输出 onFulfilled1

然而在我们的PromiseZ中确会打印 onFulfilled2 因为在执行第一个onFulfilled后返回了一个thenable,在该thenable中是异步执行 onFulfilled,所以当前PromiseZ的状态依旧处于 pending,因此便继续执行第二个onFulfilled了。所以我们需要增加一个标识符 called ,从而忽略之后的调用

function resolvePromise(promise2, x, resolve, reject) {
if (x === promise2) {
reject(new TypeError('chaining cycle'))
} else if (typeof x === 'object' && x || typeof x === 'function') {
+ let called
try {
let then = x.then;
if (typeof then === 'function') {
then.call(x, y => {
+ if (called) return;
+ called = true;
resolvePromise(promise2, y, resolve, reject);
}, r => {
+ if (called) return;
+ called = true;
reject(r);
});
} else {
+ if (called) return;
+ called = true;
resolve(x);
}
} catch (e) {
+ if (called) return;
+ called = true;
reject(e);
}
} else {
resolve(x);
}
}

到这里,一个完美符合PromiseA+ 规范的 PromiseZ就完成啦

参考链接

Promise的源码实现(完美符合Promise/A+规范)

ECMAScript 6 入门 Promise

PromiseA+

[翻译] We have a problem with promises

The End

如果你觉得这篇内容对你挺有启发,我想请你帮我三个小忙:

1、点个  「在看」 ,让更多的人也能看到这篇内容

2、关注官网  https://muyiy.cn ,让我们成为长期关系

3、关注公众号「高级前端进阶」,公众号后台回复  「加群」 ,加入我们一起学习并送你精心整理的高级前端面试题。 mQzEb2b.jpg!mobile 》》面试官都在用的题库,快来看看《《

“在看”吗?在看就点一下吧

IZZNZ3R.gif!mobile

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK