今天,我教大家写个Promise。玩掘金的同学可能上个星期会发现前端模块有很多Promise的文章,当然,我不会跟风写个类似的文章,我只是整理一下Promise的设计思路罢了。
为什么题目要强调“不会忘记”呢?因为之前一次面试。面试官正好问到了Promise的原理,对于这种面试题,出现几率很低。我要是不会还好,我一年前研究过3天,然而面试的时候忘得一干二净,一点印象都没有,有点可气。
说明什么?我并没有真正懂Promise。如果想不忘记,就要知道为什么Promise可以解决异步问题,为什么创造它的人知道要这样去设计它?知道思想了,实现自然可以推理出来。
我说的并不会都对,只是基于我对函数式的认知推理一下。
首先理清一下我们想要什么:
需要一种类型表达一个异步计算,这个计算会得到一个值
于是,我创造了一个类型:
[JavaScript] 纯文本查看 复制代码
Promise<A>
玩过函数式的同学,都应该知道flatMap吧。用来干嘛的?动态构建计算!
它的签名是这样的:
A、B表示两种类型,F表示一种容器类型,在本文可以用Promise替换。也就是说,如果F是Promise的话,签名就是这样了:
[JavaScript] 纯文本查看 复制代码
flatMap<A, B>(a: F<A>, f: A => F<B>): F<B>
如果我们把flatMap作为Promise的一个实例方法:
[JavaScript] 纯文本查看 复制代码
class Promise<A> {
flatMap<B>(f: A => Promise<B>): Promise<B>
}
推演到这里,有没有发现flatMap和Promise的一个什么方法很像?then!
这就是为什么可以不停地then构建运算的原因。明显借用了函数式编程中Monad(单子)。
如果你看到某个同学写出了这样的代码:
[JavaScript] 纯文本查看 复制代码
promise1.then(a =>
promise2.then(b =>
promise3.then(c => ...)
)
)
你心里一定会默默叼他,特么会不会用Promise啊?然而,我告诉你,他这样写反而是更贴近函数式的写法。再看这种写法:
[JavaScript] 纯文本查看 复制代码
promise1
.then(a => promise2)
.then(b => promise3)
.then(c => ...)
这就是我们常说的正常人写法,但这种写法跟上面“新手写法”相比,反而有个缺点。a、b、c的位于三个不同的函数作用域,如果想相互访问变量值,反而变得异常麻烦。要么用全局变量记录值,要么作为回调返回值传给下一个promise。这就是为什么会有async/await语法糖的原因,其实是为了改造第一种写法:
[JavaScript] 纯文本查看 复制代码
async () => {
const a = await promise1
const b = await promise2
const c = await promise3
}
现在a、b、c在同一个作用域了,而且也没了该死的回调。Good!
上面只是题外话。有了Promise<A>、then,现在可以随意描述一个异步过程了。我们会发现,现在我们完全不知道实现是怎么样的,只是一直在描述问题。这就是声明式编程之美。
函数式和命令式编程很大的一个不同就是,描述和计算分离。我们只是写了一个最简单的Monad罢了,然而它现在不表达任何意义。怎么才能用它表示异步呢?归根结底就是,then怎么实现呢?我们知道then现在长这个样:
[JavaScript] 纯文本查看 复制代码
then(onFulfilled, onRejcted) {
return new Promise()
}
onFulfilled和onRejcted都表示下一个计算,也就是需要等待触发。那么什么时候触发?当然是上一次计算结束啦。所以我们设计了一次计算的几种状态:
[JavaScript] 纯文本查看 复制代码
const State = {
Pending: 0, // 等待
Fulfilled: 1, // 满足
Rejected: 2 // 拒绝
}
我们还需要当前计算记录下一次计算的回调函数,便于当计算有了结果后,触发下次计算。
[JavaScript] 纯文本查看 复制代码
class Promise {
constructor() {
this.state = State.Pending
this.value = undefined
this.pending = []
}
}
现在的Promise有了三个成员,用于表示状态的state、用于记录值的value、用于记录下次计算的pending队列。好了,then现在是这样的:
[JavaScript] 纯文本查看 复制代码
then(onFulfilled, onRejected) {
const promise = new Promise()
// 把计算推到pending队列
this.pending.push([promise, onFulfilled, onRejected])
return promise
}
如果当前Promise状态不再是pending了,也就是计算有了结果,接下来就是调度队列了。这也是resolve要做的,伪代码如下:
[JavaScript] 纯文本查看 复制代码
resolve(promise, val) {
// 如果计算的返回值是个promise,则需要的等待promise状态冻结。
if (isPromise(val)) {
return val.then(resolve.bind(null, promise), reject.bind(null, promise))
}
// 设置状态
promise.state = Promise.Fulfilled
// 设置值
promise.value = val
// 调度队列
schedule(promise)
}
所谓调度队列,就是把当前Promise的值喂给下一次计算。伪代码如下:
[JavaScript] 纯文本查看 复制代码
schedule(promise) {
for (let i = 0; i < promise.pending.length; i++) {
const pending = promise.pending[i]
// 继续resolve队列中的promise,把值喂给onFulfilled或onRejcted
resolve(pending[0], pending[1](promise.value))
}
}
核心代码其实就这么多了,当然,没有考虑onRejected分支,也没有考虑各种容错,更详细的代码见源代码。其实promise的核心方法就是内部的resolve,用于衔接运算!
现在,我想用一句话描述Promise,也就是面试官问你,Promise原理的时候,你该怎么说。
Promise表达的是一个异步计算,每次then会返回一个新的Promise,同时向之前的Promise注册onFulfilled和onRejected方法,如果之前Promise状态发生了变化,不再是pending了,便会把计算值逐个喂给队列中注册的函数,触发下一个promise的状态变化,依次类推,这样就构成了一个函数调用链。
如果面试官问了其中的细节,就靠你自己去领悟了,亲!