Promise 探索
发布于 7 年前 作者 XueSeason 3713 次浏览 来自 分享

从回调地狱说起 Callback Hell

在 Node 中,绝大部分操作都是异步的方式,例如读取一个文件内容:

fs.readFile('./config/pass.txt', (err, data) => {
  if (err) throw err
  console.log(data)
})

这里我们通过传入一个回调函数作为异步完成的后续操作。

如果我们异步读取多个文件,等到所有文件读取完毕执行特定操作呢?
代码修改如下:

fs.readFile('./config/pass.txt', (err, data) => {
  if (err) throw err
  fs.readFile('./config/pass1.txt', (err, data1) => {
    if (err) throw err
    fs.readFile('./config/pass2.txt', (err, data2) => {
      if (err) throw err
      fs.readFile('./config/pass3.txt', (err, data3) => {
        if (err) throw err
        console.log('success');
      })
    })
  })
})

这里还是简单地敲套三层,想象下业务突然复杂,一不小心嵌套五层以上,这画面太美。。。

救世主 Promise

Promise 的音译是普罗米修斯,是希腊神话故事中的英雄,名字的意思是“先知”。

我们可以把上面 Node 读取文件的操作改造如下:

function asyncReadFile (filePath, options) {
  return new Promise((resolve, reject) => {
    fs.readFile(filePath, options, (err, data) => {
      if (err) reject(err)
      resolve(data)
    })
  })
}

现在来看看 Promise 是如何解决 Callback Hell 的:

asyncReadFile('./config/pass.text').then(data => {
  return asyncReadFile('./config/pass1.text')
}).then(data => {
  return asyncReadFile('./config/pass2.text')
}).then(data => {
  return asyncReadFile('./config/pass3.text')
}).then(data => {
  console.log('success')
}).catch(err => {
  console.log(err)
})

对于先知的我们,故事的安排在脑中是即时出现的,而非实际上的异步发生。因此,作为创世的编程者而言,这种与瞬间思考所同步的代码更符合现实世界下的思考方式。因此,更易读更易于理解。

Promise 是一种对异步操作的封装,在异步操作执行成功或者失败时执行指定方法。将横向的异步调用转换为纵向,因此更符合人类的思维方式。

一个 Promise 对象具备三种生命状态:pending、fulfilled 和 rejected。只能从最初的 pending 到 fulfilled 或者 rejected,而且状态的改变是不可逆的。

我们先简单看下 Promise 的工作原理。

Promise 大致的工作流程是:

  • 创建 Promise 对象 => 进入等待处理阶段 Pending
  • 处理完成后,切换到 Fulfilled 状态/ Rejected 状态
  • 根据状态,执行 then 方法/执行 catch 方法 内的回调函数
  • then 方法返回新的 Promise,此时就支持链式调用

这里创建一个 Promise 对象,Promise 内部维系着 resolve 和 reject 方法,resolve 会让 Promise 对象进入 Fulfilled 状态,并将 resolve 方法的第一个参数传给后续 then 所指定的 onFulfilled 函数。reject 方法同理,只不过是切换到 Rejected 状态,并将参数传给 catch 所指定的 onRejected 函数。

一步步打造心中的 Promise

基础实现

先抛开 rejected,实现一个 Promise 的调用链的简单代码如下:

function Promise (fn) {
  var deferreds = []

  this.then = function (onFulfilled) {
    deferreds.push(onFulfilled)
  }

  function resolve (value) {
    deferreds.forEach(deferred => {
      deferred(value)
    })
  }

  fn(resolve)
}

深入理解上面代码逻辑:

  • then 方法将 onFulfilled 函数压入 deferreds 队列中。
  • 将 resolve 传给创建 Promise 时传入的函数,resolve 的作用是将 deferreds 中的 onFulfilled 函数队列逐一执行。

但是这段代码暴露出一个严重的问题,如果 Promise 执行的是同步代码,resolve 是早于 then 方法的执行,这样造成一个问题:then 还没有及时把 onFulfilled 函数压入队列,此时 deferreds 还是空数组,resolve 执行后,后续注册到 deferreds 数组内的 onFulfilled 函数将不再执行。

这里我们可以把 deferreds 数组视为水桶,onFulfilled 视为饮用水,resolve 视为开关。then 操作就是将饮用水一点点地注入到水桶中。想想我们还没将水加到水桶中(执行 then 操作)就打开开关(执行 resolve),这肯定是接不到水的。

解决的办法就是将 resolve 函数的执行改为异步。

异步

Promises/A+ 规范明确要求回调需要通过异步方式执行,保证一致可靠的执行顺序。通过 setTimeout 方法,我们可以轻松实现:

function resolve (value) {
  setTimeout(() => {
    pending.forEach(deferred => {
      deferred(value)
    })
  }, 0)
}

这样我们就可以把 resolve 执行放到下一个时钟周期。

引入状态

按照 Promise 规范,我们需要引入三种互斥的状态:pending、fulfilled、rejected。

执行 resolve 会将 pending 状态切换到 fulfilled,在此之后添加到 then 到函数都会立即被调用。

现在我们的代码如下:

function Promise (fn) {
  const deferreds = []
  var state = 'pending'
  var value = null

  this.then = function (onFulfilled) {
    if (state === 'pending') {
      deferreds.push(onFulfilled)
      return this
    }
    onFulfilled(value)
    return this
  }

  function resolve (_value) {
    state = 'fulfilled'
    value = _value
    setTimeout(() => {
      deferreds.forEach(deferred => {
        deferred(value)
      })
    }, 0)
  }

  fn(resolve)
}

有了上面的基础,我们可以简单地调用 Promise:

asyncReadFile('./README.md', 'utf-8').then(data => {
  console.log(data)
})

为了串行 Promise,我们在 then 中返回 this,并设置一个 value 来保存传给 resolve 的值。

asyncReadFile('./README.md', 'utf-8').then(data => {
  console.log(data)
  return asyncReadFile('./package.json', 'utf-8')
}).then(data => {
  console.log(data)
})

像上面这样调用,虽然可以通过,但是两次输出的 data 是相同的值,并不是真正意义上的链式调用。

串行 Promise

只要 then 方法每次调用都返回一个 Promise 对象,前一个 Promise 对象持有后一个 Promise 对象的 resolve 方法,这样串行就变得非常简单了。

这里需要对 then 方法进行改造:

this.then = function (onFulfilled) {
  return new Promise(resolve => {
    handle({ onFulfilled, resolve })
  })
}

function handle (deferred) {
  if (state === 'pending') {
    deferreds.push(deferred)
    return
  }
  const ret = deferred.onFulfilled(value)
  deferred.resolve(ret)
}

这里完成的主要任务是:

  • then 方法中返回一个新的 Promise 对象,这样每次执行 then 方法,都返回一个 Promise 对象,让链式调用成为可能。
  • 新创建的 Promise 对象调用上一级 Promise 的 handle 方法,传递自身的 resolve 方法和当前的 onFulfilled 函数。

handle 相比之前的 then 多了一行 deferred.resolve(ret),这一步是链式调用的关键点。此刻的 resolve 是下一级 Promise 的方法,上一级 Promise 执行这段方法调用,就开启了链式调用。

我们继续重构前面的 Promise 代码,这里主要修改的是 resolve 方法。

function Promise (fn) {
  const deferreds = []
  var state = 'pending'
  var value = null

  this.then = function (onFulfilled) {
    // then 方法永远会返回一个 Promise 对象
    return new Promise(resolve => {
      // handle 为上一级 Promise 的方法
      handle({ onFulfilled, resolve })
    })
  }

  function handle (deferred) {
    if (state === 'pending') {
      // then 方法将 deferred 传入时,先压入到 deferreds 中
      deferreds.push(deferred)
      return
    }
    // 执行 Bridge Promise 前一个 Promise 对象的 then 方法的 onFulfilled 函数
    const ret = deferred.onFulfilled(value)
    // resolve 执行 deferreds 中的 onFulfilled 方法,即下一个 Bridge Promise 的 then 中的回调函数
    deferred.resolve(ret)
  }

  function resolve (_value) {
    // 如果是 promise 对象
    if (_value && (typeof _value === 'object' || typeof _value === 'function')) {
      const then = _value.then
      if (typeof then === 'function') {
        // 将 resolve 延迟到 promise 执行完毕后调用,切换 Bridge Promise 的状态。
        then.call(_value, resolve)
        return
      }
    }

    // 如果是其它值
    state = 'fulfilled'
    value = _value
    setTimeout(() => {
      deferreds.forEach(deferred => {
        handle(deferred)
      })
    }, 0)
  }

  fn(resolve)
}

Promise 具体流程

  1. 实例化一个最初的 Promise 对象,设置最初的状态为 pending
  2. 通过 then 方法,创建一个新的 Promise 对象,由于上一级 Promise 暂时处于 pending 状态,当前 then 方法的 onFulfilled 函数和新 Promise 的 resolve 方法放入到上一级 Promise 的 deferreds 数组中。
  3. 这样就形成这样一个画面:第一个 Promise 被实例化,调用 then 方法。then 会返回一个新的 Promise 对象,在上一个 then 方法的基础上继续通过新 Promise 的 then,形成一条调用链。 每一个被创建出来的新 Promise 的 resolve 都将传给上一级的 Promise 的 deferreds 数组来维护
  4. 在第一个 Promise 对象的回调函数中执行异步操作,完成后调用 Promise 的 resolve 方法。
  5. resolve 允许传入一个参数,该参数的值通过 Promise 内部的 value 变量维护。resolve 会把 Promise 的状态修改为 fulfilled,然后异步调用 handle 依次处理 deferreds 数组中的每一个 deferred。
  6. 此时第一个 Promise 的状态在上一步骤中被改为 fulfilled,于是 handle 主要完成的工作是,执行 deferred 的 onFulfilled 函数,并调用下一个 Promise 的 resolve 方法。
  7. 下一个 Promise 的 resovle 在上一级被执行成功后,同样会将状态切换到 fulfilled ,重复步骤 6 直到结束。

这样 Promise 的核心逻辑,基本被我们实现了。至于 rejected 和 异常处理 就交给大家来思考吧(其实就是懒!)。

结束语:真的很难想象就这么几十行代码,竟然有如此强大的威力,理解 Promise 并不难,需要敬佩的是创造强大 Promise 魔法的第一批程序员。

5 回复

我最近在看bluebird源码,看得头都大了。没看懂…

Promise 的音译是普罗米修斯 😅

@dlyt 可以不用看bluebird的源码,因为它除了promise,还有很多其他的函数,导致他的结构会比较复杂,最好可以看 then/promise,去github搜一下就有了

@cctv1005s 好的,我去看看。

promise就是函数队列

回到顶部