chunpu / blog

personal blog render by jekyll
MIT License
51 stars 8 forks source link

可视化诠释 Promise 执行过程和实现原理 #95

Open chunpu opened 5 years ago

chunpu commented 5 years ago

Promise 的可视化

为了让女票理解 Promise, 做了一个可视化展现整个 Promise 状态变化的动态效果

可视化在线 demo

其中: 延迟时间, 状态是 resolved / rejected, 以及挂 .then 的数量都是随机

Javascript 异步的历史

了解 Promise 是感受前端近几年变化的最好方式

还记得2014年, 前端还是宇宙初始一片混沌

大家都用着最原始的写法, 甚至分不清为什么 setTimeout 取到的 i 会不正常

后端大家弄懂了异步, 还约定了 callback(err, value) 第一个值是错误

再后来, 前端被分为五岳剑派

2014年 Promise 从 whatwg 的 dom 规范中删除, 被分配到 es2015 中去了

现在, Promise 一统江湖~

Promise 并非是 javascript 初创, E语言, C++, python 都有比 javascript 早的 Promise 概念

为什么要有 Promise?

我们写的简单逻辑大多是简单写法

function sum(a, b) {
  return a + b
}

var c = sum(1, 2)

但现实生活是我们的很多操作是很复杂的, 可能是发一个请求, 可能是按一个按钮, 可能是读一下硬盘

这些操作并非瞬间完成, 比如发请求, 慢的时候要花好几秒

此时我们就发明了 callback

function async_sum(a, b, callback) {
  callback(a + b)
}

async_sum(1, 2, c => {
  console.log(c)
})

这种写法也叫 CPS, CPS 的全称是 (Continuation-Passing Style)

CPS 就是 放弃 return 语句, 返回值始终通过一个 callback 来传递

thunk 也是一个典型的 CPS 设计

很显然, 把callback的写法变成同步的串行的是我们的终极目标, async/await 已经帮我们解决了此问题, 大幅提升代码可读性, js 的异步问题终于大一统了

四年后的我再次教女票实现 Promise, 更加被 Promise 的整个设计所惊艳, 可以说 Promise 真正意义上解决了现实问题

Promise 现场教学开始了!

then 约定

最初的 Promise 其实叫 thenable, 现代的 Promise 也是 thenable 的扩展

thenable 是这样约定的, 一个 thenable 对象, 可以这样获取它的值

thenable.then(value => {
  console.log(value)
})

当然, 仅仅这么简单肯定是不够的, 我们来看看现代的 Promise 长什么样

定义 Promise 类

class Promise {
  constructor (executor) {}

  then (onResolved, onRejected) {}

  catch (onRejected) {}

  static resolve (value) {}

  static reject (reason) {}

  static all () {}

  static race () {}
}

class 是另一个我非常钦佩的设计, 一个 class, 就能说清楚自己的一片天

Promise 这个类, 无非是一个构造器, 两个方法, 两个属性, 几个静态函数

Promise 的状态

有人说 Promise 是状态机, 无可厚非, Promise 约定了三种状态

状态只有两种变化

构造函数

var promise = new Promise((resolve, reject) => {
  // do your thing
})

一个初始化的 promise 用 Promise 类来产生, 构造器的参数是一个叫 executor 的函数, 里面提供了两个方法来修改 promise 的状态

constructor (executor) {
  this.value = undefined
  this.status = 'pending'
  executor(value => {
    this.value = value
    this.status = 'resolved'
  }, reason => {
    this.value = reason
    this.status = 'rejected'
  })
}

实现 .then

Promise 的核心在于 .then, 它通过一个 handler 来接收值, 这也是 thenable 的约定

then (onResolved, onRejected) {
  if (this.status === 'resolved') {
    onResolved(this.value)
  } else if (this.status === 'rejected') {
    onRejected(this.value)
  }
}

不过这样有很多问题

  1. 万一 promise 还在 pending 的时候就挂了 then 呢?

    解决办法:

    我们想到 jQuery(function() {}) 支持始终在 document 加载成功后执行, 不管我们写的时候 document 加载了没有, 因此 then 也用类似的思路也行

  2. then 的值应该可以不断传下去

    我们再优化一下 then 函数

    then (onResolved, onRejected) {
      var handler
      if (status === 'resolved') {
        handler = onResolved
      } else if (status === 'rejected') {
        handler = onRejected
      }
      this.value = handler(this.value)
      this.status = 'resolved'
    }
  3. 我们还想到, then 其实是可以无限 then 下去的, 这时候我们第一反应依然是 jQuery, jQuery 的链式调用非常好用, 秘诀是返回了 this

    那 Promise 的 then 可不可以也直接返回 this 来实现链式调用呢?

    我们先试试假如 then 返回 this

    var promise1 = Promise.resolve(1)
    var promise2 = promise1.then(x => x++)
    setTimeout(() => {
      promise1.then(x => {
        console.log(x) // x 返回了 2, 但我们希望是 1
      })
    })

    最后的 then 获取到的值已经被 promise2 的返回值改变了, 这显然不是我们想要的

then 始终返回一个新的 Promise

为了简单理解可以把 then 理解为生孩子, 每次 .then 都会生出一个新的 promise

Promise 就是 传宗接待, 开枝散叶!

王氏一家不会因为生了一个孩子名字叫 "小明", 而把妈妈的名字也改成 "小明"

但妈妈每生出一个孩子都可以随父辈王氏的姓

也就是说 Promise 的链式操作和 jQuery 的链式操作截然相反, jQuery 链的自己, Promise 链的是子子孙孙

Promise 可以不断 .then 同一个 promise, 我们突然惊醒, Promise 不仅仅是一个链, 它甚至可以是一片森林!

Promise 只有在 pending 的时候可以被修改, 其他状态下都是不可改变的

如果理解了这个, 我们会发现之前的 then 实现都是错的, 我们只能推倒重来!

then 有两个 handler, onResolvedonRejected

这个 handler 概念很像 dom 中的 onclick, 我们可以同样把 resolved 和 rejected 看成是两个状态

因此我们定义一个 triggerHandler 专门用来触发自己的 handler

再定义一个 setStatus 用来设置 Promise 自己的状态和值, 同时, 我们要在设置新状态之后通知自己的孩子们

triggerHandler (status, parentValue) {
  // private
  var handler
  if (status === 'resolved') {
    handler = this.onResolved
  } else if (status === 'rejected') {
    handler = this.onRejected
  }
  this.setStatus('resolved', handler(parentValue))
}

setStatus (status, value) {
  // private
  this.status = status
  this.value = value
  this.children.forEach(child => {
    child.triggerHandler(status, value)
  })
}

Promise 的值可以是另一个 Promise

我们知道, Promise 的 then 是可以不断执行异步函数的, 就好像一个任务清单那样

但按照目前的实现, then 之后只能链式同步任务了, 显然不能满足我们的需求

再次优化 Promise 的 setStatus, 使其可以支持 Promise 类型的值, 这样我们可以把新的 Promise 任务塞进自己的孩子

setStatus (status, value) {
  // private
  if (value && value.then) {
    value.then(realValue => {
      this.setStatus('resolved', realValue)
    }, reason => {
      this.setStatus('rejected', reason)
    })
  } else {
    this.status = status
    this.value = value
    this.children.forEach(child => {
      child.triggerHandler(status, value)
    })
  }
}

Promise 中的 nextTick

Promise/A+ 规范要求 handler 执行必须是异步的, 具体可以参见标准 3.1 条

Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called

我们这里用 setTimeout 简单实现一个跨平台的 nextTick

function nextTick(func) {
  setTimeout(func)
}

使用 nextTick 包裹 triggerHandler

triggerHandler (status, parentValue) {
  // private
  nextTick(() => {
    var handler
    if (status === 'resolved') {
      handler = this.onResolved
    } else if (status === 'rejected') {
      handler = this.onRejected
    }
    this.setStatus('resolved', handler(parentValue))
  })
}

如果你是好奇宝宝, 刨根问底的问为啥一定要 nextTick ?

那欢迎阅读我另一篇博客为什么 Promise 的 onFulfilled 和 onRejected 必须是异步的?

完整的实现

要注意, 一个规范总是非常详细的, 里面有各种各样的细节, 但这对我们理解 Promise 的设计并没有帮助, 反而会带来一些干扰

此处贴上边指导女票边写的 Promise 实现

function nextTick(func) {
  setTimeout(func)
}

class Promise {
  constructor (executor) {
    this.value = undefined
    this.status = 'pending'
    this.children = []
    executor(value => {
      this.setStatus('resolved', value)
    }, reason => {
      this.setStatus('rejected', reason)
    })
  }

  then (onResolved, onRejected) {
    var child = new Promise(() => {})
    this.children.push(child)
    child.parent = this
    Promise.onChange()
    Object.assign(child, {
      onResolved: onResolved || (value => value),
      onRejected: onRejected || (reason => Promise.reject(reason))
    })
    if (this.status !== 'pending') {
      child.triggerHandler(this.status, this.value)
    }
    return child
  }

  catch (onRejected) {
    return this.then(null, onRejected)
  }

  triggerHandler (status, parentValue) {
    // private
    nextTick(() => {
      var handler
      if (status === 'resolved') {
        handler = this.onResolved
      } else if (status === 'rejected') {
        handler = this.onRejected
      }
      this.setStatus('resolved', handler(parentValue))
    })
  }

  setStatus (status, value) {
    // private
    if (value && value.then) {
      value.then(realValue => {
        this.setStatus('resolved', realValue)
      }, reason => {
        this.setStatus('rejected', reason)
      })
    } else {
      this.status = status
      this.value = value
      Promise.onChange()
      this.children.forEach(child => {
        child.triggerHandler(status, value)
      })
    }
  }

  static resolve (value) {
    return new Promise(resolve => {
      resolve(value)
    })
  }

  static reject (reason) {
    return new Promise((resolve, reject) => {
      reject(reason)
    })
  }

  static all () { /* TODO */ }

  static race () { /* TODO */ }
}

更完整更严谨的 Promise 可以参照我之前写过一个通过官方800多个测试 的 Promise 项目 min-promise

yolio2003 commented 5 years ago

好了好了知道你有女票了。