A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

本帖最后由 懒,羊羊 于 2018-7-6 09:11 编辑

     今天,我教大家写个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的状态变化,依次类推,这样就构成了一个函数调用链。
如果面试官问了其中的细节,就靠你自己去领悟了,亲!

3 个回复

倒序浏览
回复 使用道具 举报
回复 使用道具 举报
赞一个
回复 使用道具 举报
您需要登录后才可以回帖 登录 | 加入黑马