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