Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

剖析 Promise 内部机制 #14

Open
XueSeason opened this issue Jul 21, 2016 · 5 comments
Open

剖析 Promise 内部机制 #14

XueSeason opened this issue Jul 21, 2016 · 5 comments

Comments

@XueSeason
Copy link
Member

XueSeason commented Jul 21, 2016

从回调地狱说起 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 魔法的第一批程序员。

@RaineySpace
Copy link
Member

最近你挖的坑比较多啊

@XueSeason
Copy link
Member Author

@XueRainey 我也不想挖坑,工作事太多了。这些都是夹缝中抽出的时间。

@RaineySpace
Copy link
Member

@XueSeason 我都周末有时间会写点东西,平常代码都来不及写。。。

@Nick930826
Copy link
Member

我周末不加班就陪老婆了 只有碎片时间看东西 没时间写啊

@XueSeason XueSeason changed the title 开源库 Q 的 Promise 源码解析 开源库 Q 的 Promise 内部机制 Jul 22, 2016
@XueSeason XueSeason changed the title 开源库 Q 的 Promise 内部机制 剖析 Promise 内部机制 Jul 22, 2016
@XueSeason
Copy link
Member Author

终于完成的差不多了。只有深入理解才知道,简单的几十行 Promise 代码,能说的还有很多。时间仓促,基本就这样把核心讲完吧。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants