Open JTangming opened 5 years ago
想要手写一个 Promise 则需要了解 Promise/A+ 规范并遵循这个规范。
我们是这样来使用 promise:
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {resolve('success');}, 1000);
})
const p2 = p1.then(val => {
console.log(val)
})
通过 Promise/A+ 规范和对其 API 的熟知,实现一个 Promise 的具体思路是:
一个简易版的 Promise 如下:
// 声明一下三种状态值
const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';
class Promise {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
// 通过发布订阅来执行 then
this.onResolvedCallbacks = [];
this.onRejectedCallbacks= [];
let resolve = (value) => {
if(this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
this.onResolvedCallbacks.forEach(fn => fn());
}
}
let reject = (reason) => {
if(this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn());
}
}
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
then(onFulfilled, onRejected) {
// 兼容一下 executor 同步使用的情况
this.status === FULFILLED && onFulfilled(this.value);
this.status === REJECTED && onRejected(this.reason);
if (this.status === PENDING) {
// 这里通过发布订阅的方式,将 onFulfilled 和 onRejected 函数存放到具体状态的 callback 中
this.onResolvedCallbacks.push(() => {
onFulfilled(this.value)
});
this.onRejectedCallbacks.push(()=> {
onRejected(this.reason);
})
}
}
}
链式调用即当 Promise 的 then 函数中 return 了一个值,不管是什么值,我们都能在下一个 then 中获取到,这就是所谓的then 的链式调用。
回到第一个例子,我们改造一下使用链式写法:
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {resolve('success');}, 1000);
})
const p2 = p1.then(val => {
console.log('p2 success:', val);
// 或者 return 一个 promise/非 promise 的值
throw new Error('失败了')
}).then(
data => {
console.log('success again', data);
},
ex => {
console.log('fail again', data);
}
);
结合 Promise/A+ 规范,咱们需要做到:
大致的伪代码如下:
// status ...
const resolvePromise = (newPromise, ret, resolve, reject) => {
// 如果是非基础类型的数据,另外判断处理
if ((typeof ret === 'object' && ret != null) || typeof ret === 'function') {
try {
// 如果上一个 then 的参数方法返回的是一个 promise,则等待 promise 执行完毕,再暴露执行结果
let then = ret.then;
if (typeof then === 'function') {
// 返回的 promise 执行后,将成功或者失败的结果暴露给当前 then 的 fulfilled or rejected
then.call(ret, succ => {
resolve(succ);
}, fail => {
reject(fail);
});
} else {
// 如果是引用类型(非 promise 的对象),则直接 resolve
resolve(ret);
}
} catch (ex) {
reject(ex);
}
} else {
// 如果上一个 then 参数中的函数返回值 ret 是个基本类型的值则直接 resolve
resolve(ret)
}
}
class Promise {
constructor(executor) {
// this.xxxs ...
let resolve = (value) => {
if(this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
this.onResolvedCallbacks.forEach(fn=>fn());
}
}
let reject = (reason) => {
if(this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach(fn=>fn());
}
}
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
then(onFulfilled, onRejected) {
// 兼容 onFufilled,onRejected 不传值
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : () => {};
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
// then 最终返回一个新的 promise 实例
let newPromise = new Promise((resolve, reject) => {
// ...
if (this.status === PENDING) {
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
let ret = onFulfilled(this.value);
resolvePromise(newPromise, ret, resolve, reject);
} catch (e) {
reject(e)
}
}, 0);
});
this.onRejectedCallbacks.push(()=> {
setTimeout(() => {
try {
let ret = onRejected(this.reason);
resolvePromise(newPromise, ret, resolve, reject)
} catch (e) {
reject(e)
}
}, 0);
});
}
});
return newPromise;
}
}
注,使用 setTimeout 模拟了微任务
常用的静态方法有 Promise.resolve(),Promise.reject(),可实现如下:
static resolve/*or reject**/(data) {
return new Promise((resolve, reject) => {
resolve(data); // or reject(data);
})
}
实现思路是:
Promise.all = function(promises) {
return new Promise((resolve, reject) => {
let resultArr = [];
let orderIndex = 0;
const processResultByKey = (value, index) => {
resultArr[index] = value;
if (++orderIndex === promises.length) {
resolve(resultArr);
}
}
for (let i = 0; i < promises.length; i++) {
let value = promises[i];
if (value && typeof value.then === 'function') {
value.then((value) => {
processResultByKey(value, i);
}, reject);
} else { // 如果并发的数组并不是一个 promise,则直接加到记录结果的数组中即可
processResultByKey(value, i);
}
}
});
}
Promise.race = function(promises) {
return new Promise((resolve, reject) => {
for (let i = 0; i < promises.length; i++) {
let val = promises[i];
if (val && typeof val.then === 'function') {
val.then(resolve, reject);
} else {
resolve(val)
}
}
});
}
从零扒一扒 promise
在开发过程中,很多时候都在熟练的使用 Promise/A+ 规范,然而有时在使用的时候发现并不是很了解它的底层实现,下面扒一扒它的实现。
Promises/A+规范
术语
异步回调
Promise 解决的就是异步任务处理问题,简单举例如下(假设有一个异步任务 asyncJob1,它执行完成后要执行 asyncJob2):
这样做的问题是 callback 的控制权在 asyncJob1 里面了,并且若有多个异步任务将会有回调地狱的问题,如:
控制反转
稍稍把代码修改一下:
那么调用的方式就是:
写代码的时候,「同步」的写法语义更容易理解,即”干完某件事后,再处理另外一件事”,通过 then 方法来实现链式调用:
可按照如下例子来调用,这样看起来就更有序了。
带着上面的思路,接下来实现一个简版的 Promise。
Promise simple demo
上面的例子,都是函数执行完成后同步执行回调,看下面的例子:
于是上面的回调实现就可以变成:
Promise 支持多个回调
有时在某件事完成之后,可以同时做其他的多件事情,为此修改 Promise,增加回调队列:
于是在 job1 后可以添加多个回调
这样之后可能还不够,因为如果另外一个回调是异步处理的话,可能就没法得到结果了,比如:
可以在 then 中增加一个判断,如果已经 resolve 过了,则直接执行回调:这样处理后上面的'done2'就可以输出了
Promise 作用域安全性
以上的 Promise 返回后,外部可以直接访问 then、resolve 这两个方法,然而外部应该只关心 then,resolve 方法不应该暴露出去,防止外部调用 resolve 修改了 Promise 的状态。代码修整如下:
以上只列出了修改的代码,可以看出这个改动很小,其实就是给 then 封装多了一层,调用的方式就变成如下:
Promise 链式调用
截到目前为止,promise 原型还不能实现链式调用,比如这样调用的话,第二个 then 就会报错
链式调用是promise很重要的特性,为了实现链式调用,我们要实现:
先来看看代码实现:
执行以下代码,我们能得到:
we are all done! monkey with job1 with job2
的输出Promise 错误分支
以上的 Promise 都是只有成功的 resolve 调用,在使用的 Promise 都能接受 2 个回调:resolve、reject。
为了实现可以 reject,需要引入一个 promise 的状态,记录它是被 resolve 还是 reject 过。
为了简单起见,reject的代码和resolve差不多,可以抽取一下减少多余的代码。
Promise 融入异步
在上面的所有调用中,resolve 或 reject 里的回调调用都是同步的,这取决于回调的实现。如果回调本身是同步的,就可能会出问题。
比如按上面的 promise 的代码,把 job 的调用中的 setTimeout 去掉,就会得不到结果。
调用时机
resolve 和 reject 只有在执行环境堆栈仅包含平台代码时才可被调用 注1
注1 这里的平台代码指的是引擎、环境以及 promise 的实施代码。实践中要确保 resolve 和 reject 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。
这个事件队列可以采用“宏任务(macro - task)”机制或者“微任务(micro - task)”机制来实现。
由于 promise 的实施代码本身就是平台代码(译者注:即都是 JavaScript),故代码自身在处理在处理程序时可能已经包含一个任务调度队列。
所以我们要确保这些调用都是异步的,这里只是简单地用 setTimeout 来示意处理,这样之后像上面的调用也有结果输出了。
以上仅仅是简版的 Promise,离我们平常用的promise还差很远,仅仅给自己带来的一些思考。