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

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

今天,我教大家写个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:
ManitoYu/my-promise 一年前版本
ManitoYu/my-promise-2 不会忘版本

2 个回复

正序浏览
回复 使用道具 举报

ヾ(◍°∇°◍)ノ゙
回复 使用道具 举报
您需要登录后才可以回帖 登录 | 加入黑马