前言
手写Promise一直是前端童鞋非常头疼的问题,也是面试的高频题。网上有很多手写Promise的博客,但大部分都存在或多或少的问题。
下面我们根据A+规范,手写一个Promise
基础结构
在此部分,先把Promise的基础结构写出来。
直接上代码
// 规范2.1:A promise must be in one of three states: pending, fulfilled, or rejected.
// 三个状态:PENDING、FULFILLED、REJECTED
const PENDING = Symbol();
const FULFILLED = Symbol();
const REJECTED = Symbol();
// 根据规范2.2.1到2.2.3
class _Promise {
constructor(executor) {
// 默认状态为 PENDING
this.status = PENDING;
// 存放成功状态的值,默认为 undefined
this.value = undefined;
// 存放失败状态的值,默认为 undefined
this.reason = undefined;
// 成功时,调用此方法
let resolve = (value) => {
// 状态为 PENDING 时才可以更新状态,防止 executor 中调用了两次 resovle/reject 方法
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
}
};
// 失败时,调用此方法
let reject = (reason) => {
// 状态为 PENDING 时才可以更新状态,防止 executor 中调用了两次 resovle/reject 方法
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
}
};
try {
// 立即执行,将 resolve 和 reject 函数传给使用者
executor(resolve, reject);
} catch (error) {
// 发生异常时执行失败逻辑
reject(error);
}
}
// 包含一个 then 方法,并接收两个参数 onFulfilled、onRejected
then(onFulfilled, onRejected) {
if (this.status === FULFILLED) {
onFulfilled(this.value);
}
if (this.status === REJECTED) {
onRejected(this.reason);
}
}
}
export default _Promise;
接下来我们用测试代码测一下
const promise = new _Promise((resolve, reject) => {
resolve('成功');
setTimeout(() => {
console.log('settimeout1');
}, 0);
})
.then(
(data) => {
console.log('success', data);
setTimeout(() => {
console.log('settimeout2');
}, 0);
},
(err) => {
console.log('faild', err);
}
)
.then((data) => {
console.log('success2', data);
});
控制台打印
可以看到,在executor方法中的异步行为在最后才执行
而且如果把resolve方法放到setTimeout中,会无法执行
这当然是不妥的。
接下来我们优化一下异步
executor方法中的异步
在上一小节中,我们将resolve的结果值存放到了this.value里。
优化后的代码如下:
// 规范2.1:A promise must be in one of three states: pending, fulfilled, or rejected.
// 三个状态:PENDING、FULFILLED、REJECTED
const PENDING = Symbol();
const FULFILLED = Symbol();
const REJECTED = Symbol();
class _Promise {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
// 存放成功的回调
this.onResolvedCallbacks = [];
// 存放失败的回调
this.onRejectedCallbacks = [];
// 这里使用数组,是因为如果多次调用then,会把方法都放到数组中。
// 但是目前这个版本还不支持then的链式调用
let resolve = (value) => {
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
// 依次将对应的函数执行
// 在此版本中,这个数组实际上长度最多只为1
this.onResolvedCallbacks.forEach(fn => fn());
}
}
let reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
// 依次将对应的函数执行
// 在此版本中,这个数组实际上长度最多只为1
this.onRejectedCallbacks.forEach(fn => fn());
}
}
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
then(onFulfilled, onRejected) {
if (this.status === FULFILLED) {
onFulfilled(this.value)
}
if (this.status === REJECTED) {
onRejected(this.reason)
}
// 上面两个分支是:支持resolve函数执行的时候,如果不在异步行为里执行resolve的话,会立即执行onFulfilled方法
if (this.status === PENDING) {
// 如果promise的状态是 pending,需要将 onFulfilled 和 onRejected 函数存放起来,等待状态确定后,再依次将对应的函数执行
this.onResolvedCallbacks.push(() => {
onFulfilled(this.value)
});
// 如果promise的状态是 pending,需要将 onFulfilled 和 onRejected 函数存放起来,等待状态确定后,再依次将对应的函数执行
this.onRejectedCallbacks.push(() => {
onRejected(this.reason);
})
}
}
}
我们用测试方法测一下:
const promise = new _Promise((resolve, reject) => {
setTimeout(() => {
console.log('settimeout1');
resolve('成功');
}, 0);
})
.then(
(data) => {
console.log('success', data);
setTimeout(() => {
console.log('settimeout2');
}, 0);
return data;
},
(err) => {
console.log('faild', err);
}
)
.then((data) => {
console.log('success2', data);
});
控制台结果:
可以看到,异步顺序是正确的,先执行settimeout1,再执行success
但是不支持链式的then调用,也不支持在then中返回一个新的Promise
支持链式调用的Promise
接下来我们将完整实现一个支持链式调用的Promis
// 规范2.1:A promise must be in one of three states: pending, fulfilled, or rejected.
// 三个状态:PENDING、FULFILLED、REJECTED
const PENDING = Symbol();
const FULFILLED = Symbol();
const REJECTED = Symbol();
class _Promise {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
// 存放成功的回调
this.onResolvedCallbacks = [];
// 存放失败的回调
this.onRejectedCallbacks = [];
// 这里使用数组,是因为如果多次调用then,会把方法都放到数组中。
// 但是目前这个版本还不支持then的链式调用
let resolve = (value) => {
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
// 依次将对应的函数执行
// 在此版本中,这个数组实际上长度最多只为1
this.onResolvedCallbacks.forEach(fn => fn());
}
}
let reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
// 依次将对应的函数执行
// 在此版本中,这个数组实际上长度最多只为1
this.onRejectedCallbacks.forEach(fn => fn());
}
}
try {
// 立即执行executor方法
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
// 这里就是最关键的then方法
then(onFulfilled, onRejected) {
// 克隆this,因为之后的this就不是原promise的this了
const self = this;
// 判断两个传入的方法是不是funcion,如果不是,那么给一个function的初始值
onnFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function'?onRejected:reason => { throw new Error(reason instanceof Error ? reason.message:reason) }
// 返回一个新的promise,剩下的逻辑都在这个新的promise里进行
return new _Promise((resolve, reject) => {
if (this.status === PENDING) {
// 如果promise的状态是 pending,需要将 onFulfilled 和 onRejected 函数存放起来,等待状态确定后,再依次将对应的函数执行
self.onResolvedCallbacks.push(() => {
// 使用settimeout模拟微任务
setTimeout((0 => {
// self.value是之前存在value里的值
const result = onFulfilled(self.value);
// 这里要考虑两种情况,如果onFulfilled返回的是Promise,则执行then
// 如果返回的是一个值,那么直接把值交给resolve就行
result instanceof _Promise ? result.then(resolve, reject) : resolve(result);
}, 0)
onFulfilled(self.value)
});
// 如果promise的状态是 pending,需要将 onFulfilled 和 onRejected 函数存放起来,等待状态确定后,再依次将对应的函数执行
// reject也要进行一样的事
self.onRejectedCallbacks.push(() => {
setTimeout(() => {
const result = onRejected(self.reason);
// 不同点:此时是reject
result instanceof _Promise ? result.then(resolve, reject) : reject(result);
}, 0)
})
}
// 如果不是PENDING状态,也需要判断是不是promise的返回值
if (self.status === FULFILLED) {
setTimeout(() => {
const result = onFulfilled(self.value);
result instanceof _Promise ? result.then(resolve, reject) : resolve(result);
});
}
if (self.status === REJECTED) {
setTimeout(() => {
const result = onRejected(self.reason);
result instanceof _Promise ? result.then(resolve, reject) : reject(result);
})
}
})
// 到这里,最难的then方法已经写完了
}
}
额外补充
catch、静态resolve、静态reject方法
catch方法的一般用法是new _Promise(() => {...}).then(() => {...}).catch(e => {...})
所以它是一个和then同级的方法,它实现起来非常简单:
class _Promise{
...
catch(onRejected) {
return this.then(null, onRejected);
}
}
静态resolve、静态reject的用法:_Promise.resolve(() => {})
这样可以直接返回一个_Promise
这块的实现,参考then中返回_Promise的那一段,就能实现
reject类似
class _Promise{
...
static resolve(value) {
if (value instanceof Promise) {
// 如果是Promise实例,直接返回
return value;
} else {
// 如果不是Promise实例,返回一个新的Promise对象,状态为FULFILLED
return new Promise((resolve, reject) => resolve(value));
}
}
static reject(reason) {
return new Promise((resolve, reject) => {
reject(reason);
})
}
}
优化setTimeout变成微任务
最后再说一个关于微任务的
setTimeout毕竟是个宏任务,我们可以用MutationObserver来模拟一个微任务,只要将下面的nextTick方法替换setTimeout方法即可
function nextTick(fn) {
if (process !== undefined && typeof process.nextTick === "function") {
return process.nextTick(fn);
} else {
// 实现浏览器上的nextTick
var counter = 1;
var observer = new MutationObserver(fn);
var textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true,
});
counter += 1;
textNode.data = String(counter);
}
}
这个方法的原理不难看懂,就是在dom里创建了一个textNode,用MutationObserver监控这个node的变化。在执行nextTick方法的时候手动修改这个textNode,触发MutationObserver的callback,这个callback就会在微任务队列中执行。
注意MutationObserver的兼容性。
总结
我个人感觉完整理解Promise的源码还是比较考验代码功底的,一开始建议把源码放在编译器里一点一点调试着看,如果实在不知道怎么下手,也可以把代码背下来,慢慢咀嚼。实际上,背下来之后,人脑对这个东西会有一个缓慢的理解过程,到了某一天会感觉恍然大悟。