写一个符合 Promises/A+ 规范并可配合 ES7 async/await 使用的 Promise

原文地址javascript


从历史的进程来看,Javascript 异步操做的基本单位已经从 callback 转换到 Promise。除了在特殊场景使用 streamRxJs 等更加抽象和高级的异步数据获取和操做流程方式外,如今几乎任何成熟的异步操做库,都会实现或者引用 Promise 做为 API 的返回单位。主流的 Javascript 引擎也基本原生实现了 Promise。php

在 Promise 远未流行之前,Javascript 的异步操做基本都在使用以 callback 为主的异步接口。鼠标键盘事件,页面渲染,网络请求,文件请求等等异步操做的回调函数都是用 callback 来处理。随着异步使用场景范围的扩大,出现了大量工程化和富应用的的交互和操做,使得应用不足以用 callback 来面对越发复杂的需求,慢慢出现了许多优雅和先进的异步解决方案:EventEmitterPromiseWeb WorkerGeneratorAsync/Awaithtml

目前 Javascript 在客户端和服务器端的应用当中,只有 Promise 被普遍接受并使用。追根溯源,Promise 概念的提出是在 1976 年,Javascript 最先的 Promise 实现是在 2007 年,到如今 2016 年,Promise/A+ 规范和 ECMAscript 规范提出的 API 也足够稳定。then, reject, all, spread, race, finally 都是工程师开发中常常用到的 Promise API。不少人刚接触 Promise 概念的时候看下 API,看几篇博客或者看几篇最佳实践就觉得理解程度够了,可是对 Promise 内部的异步机制不明了,使得在开发过程当中遇到很多坑或者懵逼。java

本文旨在让读者能深刻了解 Promise 内部执行机制,熟悉和掌握 Promise 的操做流。若有兴趣,能够继续往下读。react

Promise 只是一个 Event Loop 中的 microtask

深刻了解过 Promise 的人都知道,Promise 所说的异步执行,只是将 Promise 构造函数中 resolvereject 方法和注册的 callback 转化为 eventLoop的 microtask/Promise Job,并放到 Event Loop 队列中等待执行,也就是 Javascript 单线程中的“异步执行”。git

Promise/A+ 规范中,并无明确是以 microtask 仍是 macrotask 形式放入队列,对没有 microtask 概念的宿主环境采用 setTimeout 等 task/Job 类的任务。规范中另外明确的一点也很是重要:回调函数的异步调用必须在当前 context,也就是 JS stack 为空以后执行。es6

在最新的 ECMAScript 规范 中,明确了 Promise 必须以 Promise Job 的形式入 Job 队列(也就是 microtask),并仅在没有运行的 stack(stack 为空的状况下)才能够初始化执行。github

HTML 规范 也提出,在 stack 清空后,执行 microtask 的检查方法。也就是必须在 stack 为空的状况下才能执行。web

Google Chrome 的开发者 Jake Archibald (ES6-promise 做者)的文章 Tasks, microtasks, queues and schedules中,将这个区分的问题描述得很清楚。假如要在 Javascript 平台或者引擎中实现 Promise,优先以 microtask/Promise Job 方式实现。目前主流浏览器的 Javascript 引擎原生实现,主流的 Promise 库(es6-promise,bluebrid)基本都是使用 microtask/Promise Job 的形式将 Promise 放入队列。api

其余以 microtask/Promise Job 形式实现的方法还有:process.nextTicksetImmediatepostMessageMessageChannel

根据规范,microtask 存在的意义是:在当前 task 执行完,准备进行 I/O,repaintredraw 等原生操做以前,须要执行一些低延迟的异步操做,使得浏览器渲染和原生运算变得更加流畅。这里的低延迟异步操做就是 microtask。原生的 setTimeout 就算是将延迟设置为 0 也会有 4 ms 的延迟,会将一个完整的 task 放进队列延迟执行,并且每一个 task 之间会进行渲染等原生操做。假如每执行一个异步操做都要从新生成一个 task,将提升宿主平台的负担和响应时间。因此,须要有一个概念,在进行下一个 task 以前,将当前 task 生成的低延迟的,与下一个 task 无关的异步操做执行完,这就是 microtask。

这里的 Quick Sort Demo 展现了 microtask 和 task 在延迟执行上的巨大区别。

对于在不通宿主环境中选择合适的 microtask,能够选择 asapsetImmediate 的代码做为参考。

Promise 的中的同步与异步

new Promise((resolve) => {
  console.log('a')
  resolve('b')
  console.log('c')
}).then((data) => {
  console.log(data)
})

// a, c, b复制代码

使用过 Promise 的人都知道输出 a, c, b,但有多少人能够清楚地说出从建立 Promise 对象到执行完回调的过程?下面是一个完整的解释:

构造函数中的输出执行是同步的,输出 a, 执行 resolve 函数,将 Promise 对象状态置为 resolved,输出 c。同时注册这个 Promise 对象的回调 then 函数。整个脚本执行完,stack 清空。event loop 检查到 stack 为空,再检查 microtask 队列中是否有任务,发现了 Promise 对象的 then 回调函数产生的 microtask,推入 stack,执行。输出 b,event loop的列队为空,stack 为空,脚本执行完毕。

以基础的 Promises/A+ 规范为范本

规范地址:

值得注意的是:

Finally, the core Promises/A+ specification does not deal with how to create, fulfill, or reject promises, choosing instead to focus on providing an interoperable then method. Future work in companion specifications may touch on these subjects.

Promises/A+ 规范主要是制定一个通用的回调方法 then,使得各个实现的版本能够造成链式结构进行回调。这使得不一样的 Promise 库内部细节实现可能不同,可是只有具备想通的 then 方法,返回的 Promise API 之间就能够相互调用。

下面会实现一个简单的 Promise,不想看实现的能够跳过。项目地址在这里,欢迎更多讨论。

Promise 构造函数,选择平台的 microtask 实现

// Simply choose a microtask
const asyncFn = function() {
  if (typeof process === 'object' && process !== null && typeof(process.nextTick) === 'function')
    return process.nextTick
  if (typeof(setImmediate === 'function'))
    return setImmediate
  return setTimeout
}()

// States
const PENDING = 'PENDING'

const RESOLVED = 'RESOLVED'

const REJECTED = 'REJECTED'

// Constructor
function MimiPromise(executor) {
  this.state = PENDING
  this.executedData = undefined
  this.multiPromise2 = []

  resolve = (value) => {
    settlePromise(this, RESOLVED, value)
  }

  reject = (reason) => {
    settlePromise(this, REJECTED, reason)
  }

  executor(resolve, reject)
}复制代码

stateexecutedData 都容易理解,可是必需要理解一下为何要维护一个 multiPromise2 数组。因为规范中说明,每一个调用过 then 方法的 promise 对象必须返回一个新的 promise2 对象,因此最好的方法是当调用 then 方法的时候将一个属于这个 then 方法的 promise2 加入队列,在 promise 对象中维护这些新的 promise2 的状态。

  • executorpromise 构造函数的执行函数参数
  • statepromise 的状态
  • multiPromise2:维护的每一个注册 then 方法须要返回的新 promise2
  • resolve:函数定义了将对象设置为 RESOLVED 的过程
  • reject:函数定义了将对象设置为 REJECTED 的过程

最后执行构造函数 executor,并调用 promise 内部的私有方法 resolvereject

settlePromise 如何将一个新建的 Promise settled

function settlePromise(promise, executedState, executedData) {
  if (promise.state !== PENDING)
    return

  promise.state = executedState
  promise.executedData = executedData

  if (promise.multiPromise2.length > 0) {
    const callbackType = executedState === RESOLVED ? "resolvedCallback" : "rejectedCallback"

    for (promise2 of promise.multiPromise2) {
      asyncProcessCallback(promise, promise2, promise2[callbackType])
    }
  }
}复制代码

第一个判断条件很重要,由于 Promise 的状态是不可逆的。在 settlePromise 的过程当中假如状态不是 PENDING,则不须要继续执行下去。

当前 settlePromise 的环境,能够有三种状况:

  • 异步延迟执行 settlePromise 方法,线程已经同步注册好 then 方法,须要执行全部注册的 then 回调函数
  • 同步执行 settlePromise 方法,then 方法未执行,后面须要执行的 then 方法会在注册的过程当中直接执行
  • 不管执行异步 settlePromise 仍是同步 settlePromise 方法,并无注册的 then 方法须要执行,只须要将本 Promise 对象的状态设置好便可

then 方法的注册和当即执行

MimiPromise.prototype.then = function(resolvedCallback, rejectedCallback) {
  let promise2 = new MimiPromise(() => {})

  if (typeof resolvedCallback === "function") {
      promise2.resolvedCallback = resolvedCallback;
  }
  if (typeof rejectedCallback === "function") {
      promise2.rejectedCallback = rejectedCallback;
  }

  if (this.state === PENDING) {
    this.multiPromise2.push(promise2)
  } else if (this.state === RESOLVED) {
    asyncProcessCallback(this, promise2, promise2.resolvedCallback)
  } else if (this.state === REJECTED) {
    asyncProcessCallback(this, promise2, promise2.rejectedCallback)
  }

  return promise2
}复制代码

每一个注册 then 方法都须要返回一个新的 promise2 对象,根据当前 promise 对象的 state,会出现三种状况:

  • 当前 promise 对象处于 PENDING 状态。构造函数异步执行了 settlePromise 方法,须要将这个 then 方法对应返回的 promise2 放入当前 promisemultiPromise2 队列当中,返回这个 promise2。之后当 settlePromise 方法异步执行的时候,执行所有注册的 then 回调方法
  • 当前 promise 对象处于 RESOLVED 状态。构造函数同步执行了 settlePromise 方法,直接执行 then 注册的回调方法,返回 promise2
  • 当前 promise 对象处于 REJECTED 状态。构造函数同步执行了 settlePromise 方法,直接执行 then 注册的回调方法,返回 promise2

异步执行回调函数

function asyncProcessCallback(promise, promise2, callback) {
  asyncFn(() => {
    if (!callback) {
      settlePromise(promise2, promise.state, promise.executedData);
      return;
    }

    let x

    try {
      x = callback(promise.executedData)
    } catch (e) {
      settlePromise(promise2, REJECTED, e)
      return
    }

    settleWithX(promise2, x)
  })
}复制代码

这里用到咱们以前选取的平台异步执行函数,异步执行 callback。假如 callback 没有定义,则将返回 promise2 的状态转换为当前 promise 的状态。而后将 callback 执行。最后再 settleWithX promise2 与 callback 返回的对象 x

最后的 settleWithX 和 settleXthen

function settleWithX (p, x) {
    if (x === p && x) {
        settlePromise(p, REJECTED, new TypeError("promise_circular_chain"));
        return;
    }

    var xthen, type = typeof x;
    if (x !== null && (type === "function" || type === "object")) {
        try {
            xthen = x.then;
        } catch (err) {
            settlePromise(p, REJECTED, err);
            return;
        }
        if (typeof xthen === "function") {
            settleXthen(p, x, xthen);
        } else {
            settlePromise(p, RESOLVED, x);
        }
    } else {
        settlePromise(p, RESOLVED, x);
    }
    return p;
}

function settleXthen (p, x, xthen) {
    try {
        xthen.call(x, function (y) {
            if (!x) return;
            x = null;

            settleWithX(p, y);
        }, function (r) {
            if (!x) return;
            x = null;

            settlePromise(p, REJECTED, r);
        });
    } catch (err) {
        if (x) {
            settlePromise(p, REJECTED, err);
            x = null;
        }
    }
}复制代码

这里的两个方法对应 Promise/A+ 规范里的第三章,因为实在太啰嗦,这里就再也不过多解释了。

配合 async/await 使用更加美味

V8 已经原生实现了 async/await,Node 和各浏览器引擎的实现也会慢慢跟进,而 babel 早就加入了 async/await。目前客户端仍是用 babel 预编译使用比较好,而 Node 须要升级到 v7 版本,而且加入 --harmony-async-await 参数。

Promise 其中的一个局限在于:全部操做过程都必须包含在构造函数或者 then 回调中执行,假若有一些变量须要累积向下链式使用,还要加入外部全局变量,或者引发回调地狱,像这样。

let result1
let result2
let result3

getSomething1()
  .then((data) => {
    result1 = data
    // do some shit with result1
    return getSomething2()
  })
  .then((data) => {
    result2 = data
    // do some other shit with result1 and result2
    return getSomething3()
  })
  .then((data) => {
    result3 = data
    // do some other shit with result1, result2 and result3
  })
  .catch((err) => {
    console.error(err);
  })

getSomething1()
  .then((data1) => {
    // do some shit with data1
    return getSomething2()
    .then((data2) => {
      // do some shit with data1 and data2
      return getSomething3()
      .then((data3) => {
        // do some shit with data1, data2 and data3
      })
    })
  })
  .catch((err) => {
    console.error(err);
  })复制代码

引入了全局变量和写出了回调地狱都不是明智的作法,假如用了 async/await,能够这样:

async function a() {
  try {
    const result1 = await getSomething1()
    // do some shit with result1
    const result2 = await getSomething2()
    // do some other shit with result1 and result2
    const result3 = await getSomething3()
    // do some other shit with result1, result2 and result3
  } catch (e) {
    console.error(e);
  }
}复制代码

async/await 配合 Promise,没有了 then 方法和回调地狱的写法是否是清爽了不少?

结语

本文后续其实还有更多值得挖掘的地方:

  • 如何更加有效地选取平台的 microtask?
  • 如何实现一个可用的符合 ECMAScript 规范的 Promise?
  • microtask 和 task 在 event loop 具体的执行过程?

能够期待后续的更多内容。最后再贴一下项目地址,欢迎继续的讨论。

参考资料

相关文章
相关标签/搜索