前端异步代码解决方案实践(二)

早前有针对 Promise 的语法写过博文,不过仅限入门级别,浅尝辄止食而无味。后面一直想写 Promise 实现,碍于理解程度有限,多次下笔未能满意。一拖再拖,时至今日。

随着 Promise/A+规范、ECMAScript规范 对 Promise API 制定执行落地,Javascript 异步操作的基本单位也逐渐从 callback 转换到 promise。绝大多数 JavaScript/DOM平台新增的异步API( FetchServiceworker)也都是基于 Promise构建的。这其中对 Promise 理解不是仅看过 API,读过几篇实践就能完全掌握的。笔者以此行文,剖析细节,伴随读者一起成长,砥砺前行。

本文为前端异步编程解决方案实践系列第二篇,主要分析 Promise 内部机制及实现原理。后续异步系列还会包括 GeneratorAsync/Await相关,挖坑占位。由于微信文章不能有外链,相关引用地址或文档链接请点击阅读原文,见谅。

注:本文 Promise 遵守 Promises/A+ 规范,实现参照 then/promise。

Promise 是什么

既然要讲实现原理,不免要承前启后交代清楚 Promise 是什么。查阅文档,如下:

A promise represents the eventual result of an asynchronous operation. -- Promises/A+ A Promise is an object that is used as a placeholder for the eventual results of a deferred (and possibly asynchronous) computation. -- ECMAScript

Promises/A+ 规范中表示为一个异步操作的最终结果, ECMAScript 规范定义为延时或异步计算最终结果的占位符。言简意赅,但稍微聱牙诘屈,如何表述更浅显易懂呢?

说个故事, Promise 是一个美好的承诺,承诺本身会做出正确延时或异步操作。承诺会解决 callback处理异步回调可能产生的调用过早,调用过晚、调用次数过多过少、吞掉可能出现的错误或异常问题等。另外承诺只接受首次 resolve(..)reject(..) 决议,承诺本身状态转变后不会再变,承诺所有通过 then(..)注册的回调总是依次异步调用,承诺所有异常总会被捕获抛出。她,是一个可信任的承诺。

严谨来讲, Promise 是一种封装和组合未来值得易于复用机制,实现关注点分离、异步流程控制、异常冒泡、串行/并行控制等。

注:文中提及 callback 问题详情见<<你不知道的JavaScript(中卷)>> 2.3 、3.3章节

标准解读

PromiseA+ 规范字数不多简明扼要,但仔细翻读,其中仍有有几点需要引人注意。

thenable 对象

thenable 是一个定义 then(..) 方法的对象或函数。 thenable 对象的存在目的是使 Promise 的实现更具有通用性,只要其暴露出一个遵循 Promise/A+ 规范的 then(..) 方法。同时也会使遵循 Promise/A+ 规范的实现可以与那些不太规范但可用的实现能良好共存。

识别 thenable 或行为类似 Promise 对象可以根据其是否具有 then(..) 方法来判断,这其实叫类型检查也可叫鸭式辩型( duck typing)。对于 thenable 值鸭式类型检测大致类似于:

if ( p !== null && 
     (
       typeof p === 'object' || 
       typeof p === 'function'
     ) &&
     typeof p.then === 'function'
) {
    // thenable
} else {
    // 非 thenable 
}
then 回调异步执行

众所周知, Promise 实例化时传入的函数会立即执行, then(...) 中的回调需要异步延迟调用。至于为什么要延迟调用,后文会慢慢解读。这里有个重要知识点,回调函数异步调用时机。

onFulfilled or onRejected must not be called until the execution context stack contains only platform code -- Promise/A+

简译为 onFulfilledonRejected 只在执行环境堆栈仅包含平台代码时才可被调用。稍有疑惑,Promise/A+ 规范又对此句加以解释:“实践中要确保 onFulfilledonRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。这个事件队列可以采用宏任务 macro-task机制或微任务 micro-task机制来实现。”

虽然 PromiseA+未明确指出是以 microtask 还是 macrotask 形式放入队列,但 ECMAScript 规范明确指出 Promise 必须以 Promise Job 形式加入 job queues(也就是 microtask)。Job Queue 是 ES6 中新提出的概念,建立在事件循环队列之上。 job queue存在也是为了满足一些低延迟的异步操作。

敲黑板划重点,注意这里 macrotask microtask 分别表示异步任务的两种分类。在挂起任务时,JS 引擎会将所有任务按照类别分到两个队列中,首先在 macrotask 的队列(也叫 task queue)中取出第一个任务,执行完毕后取出 microtask 队列中的所有任务顺序执行;之后再取 macrotask 任务,周而复始,直至两个队列的任务都取完。

对于 microtask执行时机,whatwg HTML规范中也有阐述,详情可点击查阅。更多相关文章可参考附录 eventloop

再看一个示例,加深理解:

setTimeout(function () {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function () {
  console.log('promise1');
}).then(function () {
  console.log('promise2');
});

打印的顺序?正确答案是: promise1,promise2,setTimeout

在进一步实现 Promise 对象之前,简单模拟异步执行函数供后文 Promise回调使用(也可采用 asap库等)。

var asyncFn = function () {
  if (typeof process === 'object' && process !== null && 
      typeof(process.nextTick) === 'function'
  ) {
    return process.nextTick;
  } else if (typeof(setImmediate) === 'function') {
    return setImmediate;
  }
  return setTimeout;
}();
Promise 状态

Promise 必须为以下三种状态之一:等待态( Pending)、执行态( Fulfilled)和拒绝态( Rejected)。一旦 Promiseresolvereject,不能再迁移至其他任何状态(即状态 immutable)。

为保持代码清晰,暂无异常处理。同时为表述方便,约定如下:

  • fulfilled 使用 resolved 代替
  • onFulfilled 使用 onResolved 代替

Promise 构造函数

从构造函数开始,我们一步步实现符合 PromsieA+ 规范的 Promise。大概描述下, Promise构造函数需要做什么事情。

  1. 初始化 Promise 状态( pending
  2. 初始化 then(..) 注册回调处理数组( then 方法可被同一个 promise 调用多次)
  3. 立即执行传入的 fn 函数,传入 Promise 内部 resolvereject 函数
  4. ...
function Promise (fn) {
  // 省略非 new 实例化方式处理
  // 省略 fn 非函数异常处理

  // promise 状态变量
  // 0 - pending
  // 1 - resolved
  // 2 - rejected
  this._state = 0;
  // promise 执行结果
  this._value = null;

  // then(..) 注册回调处理数组
  this._deferreds = [];

  // 立即执行 fn 函数
  try {
    fn(function (value) {
      resolve(this, value);
    }, function (reason) {
      reject(this, reason);
    })
  } catch (err) {
    // 处理执行 fn 异常
    reject(this, err);
  }
}

_state_value 变量很容易理解, _deferreds变量做什么?规范描述: then 方法可以被同一个 promise 调用多次。为满足多次调用 then 注册回调处理,内部选择使用 _deferreds 数组存储处理对象。具体处理对象结构,见 then 函数章节。

最后执行 fn 函数,并调用 promise 内部的私有方法 resolverejectresolvereject 内部细节随后介绍。

then 函数

PromiseA+提到规范专注于提供通用的 then 方法。 then 方法可以被同一个 promise 调用多次,每次返回新 promise 对象 。 then 方法接受两个参数 onResolvedonRejected(可选)。在 promiseresolvereject 后,所有 onResolvedonRejected 函数须按照其注册顺序依次回调,且调用次数不超过一次。

根据上述, then 函数执行流程大致为:

  1. 实例化空 promise 对象用来返回(保持 then链式调用)
  2. 构造 then(..) 注册回调处理函数结构体
  3. 判断当前 promise 状态, pending 状态存储延迟处理对象 deferred ,非 pending状态执行 onResolvedonRejected 回调
  4. ...
Promise.prototype.then = function (onResolved, onRejected) {

  var res = new Promise(function () {});
  // 使用 onResolved,onRejected 实例化处理对象 Handler
  var deferred = new Handler(onResolved, onRejected, res);

  // 当前状态为 pendding,存储延迟处理对象
  if (this._state === 0) {
    this._deferreds.push(deferred);
    return;
  }

  // 当前 promise 状态不为 pending
  // 调用 handleResolved 执行onResolved或onRejected回调
  handleResolved(this, deferred);

  // 返回新 promise 对象,维持链式调用
  return res;
};

Handler 函数封装存储 onResolvedonRejected 函数和新生成 promise 对象。

function Handler (onResolved, onRejected, promise) {
  this.onResolved = typeof onResolved === 'function' ? onResolved : null;
  this.onRejected = typeof onRejected === 'function' ? onRejected : null;
  this.promise = promise;
}

链式调用为什么要返回新的 promise

如我们理解,为保证 then 函数链式调用, then 需要返回 promise 实例。但为什么返回新的 promise,而不直接返回 this 当前对象呢?看下面示例代码:

var promise2 = promise1.then(function (value) {
  return Promise.reject(3)
})

假如 then 函数执行返回 this 调用对象本身,那么 promise2===promise1promise2状态也应该等于 promise1 同为 resolved。而 onResolved 回调中返回状态为 rejected对象。考虑到 Promise 状态一旦 resolvedrejected就不能再迁移,所以这里 promise2 也没办法转为回调函数返回的 rejected 状态,产生矛盾。

handleResolved 函数功能为根据当前 promise 状态,异步执行 onResolvedonRejected 回调函数。因在 resolvereject 函数内部同样需要相关功能,提取为单独模块。往下翻阅查看。

resolve 函数

Promise 实例化时立即执行传入的 fn 函数,同时传递内部 resolve 函数作为参数用来改变 promise 状态。 resolve 函数简易版逻辑大概为:判断并改变当前 promise 状态,存储 resolve(..)value 值。判断当前是否存在 then(..) 注册回调执行函数,若存在则依次异步执行 onResolved 回调。

但如文初所 thenable 章节描述,为使 Promise 的实现更具有通用性,当 value 为存在 then(..) 方法的 thenable 对象,需要做 PromiseResolutionProcedure 处理,规范描述为 [[Resolve]](promise,x)。( x 即 为后面 value 参数)。

具体处理逻辑流程如下:

  • 如果 promisex 指向同一对象,以 TypeError 为据因拒绝执行 promise
  • 如果 xPromise ,则使 promise 接受 x 的状态
  • 如果 x 为对象或函数
    1. x.then 赋值给 then
    2. 如果取 x.then 的值时抛出错误 e ,则以 e 为据因拒绝 promise
    3. 如果 then 是函数,将 x 作为函数的作用域 this 调用之。
    4. 如果 x 不为对象或者函数,以 x 为参数执行 promise

原文参考 PromiseA+规范 Promise Resolution Procedure 。

function resolve (promise, value) {
  // 非 pending 状态不可变
  if (promise._state !== 0) return;

  // promise 和 value 指向同一对象
  // 对应 Promise A+ 规范 2.3.1
  if (value === promise) {
    return reject( promise, new TypeError('A promise cannot be resolved with itself.') );
  }

  // 如果 value 为 Promise,则使 promise 接受 value 的状态
  // 对应 Promise A+ 规范 2.3.2
  if (value && value instanceof Promise && value.then === promise.then) {
    var deferreds = promise._deferreds

    if (value._state === 0) {
      // value 为 pending 状态
      // 将 promise._deferreds 传递 value._deferreds
      // 偷个懒,使用 ES6 展开运算符
      // 对应 Promise A+ 规范 2.3.2.1
      value._deferreds.push(...deferreds)
    } else if (deferreds.length !== 0) {
      // value 为 非pending 状态
      // 使用 value 作为当前 promise,执行 then 注册回调处理
      // 对应 Promise A+ 规范 2.3.2.2、2.3.2.3
      for (var i = 0; i < deferreds.length; i++) {
        handleResolved(value, deferreds[i]);
      }
      // 清空 then 注册回调处理数组
      value._deferreds = [];
    }
    return;
  }

  // value 是对象或函数
  // 对应 Promise A+ 规范 2.3.3
  if (value && (typeof value === 'object' || typeof value === 'function')) {
    try {
      // 对应 Promise A+ 规范 2.3.3.1
      var then = obj.then;
    } catch (err) {
      // 对应 Promise A+ 规范 2.3.3.2
      return reject(promise, err);
    }

    // 如果 then 是函数,将 value 作为函数的作用域 this 调用之
    // 对应 Promise A+ 规范 2.3.3.3
    if (typeof then === 'function') {
      try {
        // 执行 then 函数
        then.call(value, function (value) {
          resolve(promise, value);
        }, function (reason) {
          reject(promise, reason);
        })
      } catch (err) {
        reject(promise, err);
      }
      return;
    }
  }

  // 改变 promise 内部状态为 `resolved`
  // 对应 Promise A+ 规范 2.3.3.4、2.3.4
  promise._state = 1;
  promise._value = value;

  // promise 存在 then 注册回调函数
  if (promise._deferreds.length !== 0) {
    for (var i = 0; i < promise._deferreds.length; i++) {
      handleResolved(promise, promise._deferreds[i]);
    }
    // 清空 then 注册回调处理数组
    promise._deferreds = [];
  }
}

resolve 函数逻辑较为复杂,主要集中在处理 valuex)值多种可能性。如果 valuePromise 且状态为 pending时,须使 promise 接受 value 的状态。在 value 状态为 pending 时,简单将 promisedeferreds 回调处理数组赋予 value deferreds变量。非 pending 状态,使用 value 内部值回调 promise注册的 deferreds

如果 valuethenable 对象,以 value 作为函数的作用域 this 调用之,同时回调调用内部 resolve(..)reject(..)函数。

其他情形则以 value 为参数执行 promise,调用 onResolvedonRejected 处理函数。

事实上, PromiseA+规范 定义的 PromiseResolutionProcedure 处理流程是用来处理 then(..) 注册的 onResolvedonRejected 调用返回值 与 then 新生成 promise 之间关系。不过考虑到 fn 函数内部调用resolve(..)产生值 与当前 promise 值仍然存在相同关系,逻辑一致,写进相同模块。

reject 函数

Promise 内部私有方法 reject 相较于 resolve 逻辑简单很多。如下所示:

function reject (promise, reason) {
  // 非 pending 状态不可变
  if (promise._state !== 0) return;

  // 改变 promise 内部状态为 `rejected`
  promise._state = 2;
  promise._value = reason;

  // 判断是否存在 then(..) 注册回调处理
  if (promise._deferreds.length !== 0) {
    // 异步执行回调函数
    for (var i = 0; i < promise._deferreds.length; i++) {
      handleResolved(promise, promise._deferreds[i]);
    }
    promise._deferreds = [];
  }
}

handleResolved 函数

了解完 Promise 构造函数、 then 函数、以及内部 resolvereject 函数实现,你会发现其中所有的回调执行我们都统一调用 handleResolved函数,那 handleResolved 到底做了哪些事情,实现又有什么注意点?

handleResolved 函数具体会根据 promise 当前状态判断调用 onResolvedonRejected,处理 then(..)注册回调为空情形,以及维护链式 then(..) 函数后续调用。具体实现如下:

function handleResolved (promise, deferred) {
  // 异步执行注册回调
  asyncFn(function () {
    var cb = promise._state === 1 ? 
            deferred.onResolved : deferred.onRejected;

    // 传递注册回调函数为空情况
    if (cb === null) {
      if (promise._state === 1) {
        resolve(deferred.promise, promise._value);
      } else {
        reject(deferred.promise, promise._value);
      }
      return;
    }

    // 执行注册回调操作
    try {
      var res = cb(promise._value);
    } catch (err) {
      reject(deferred.promise, err);
    }

    // 处理链式 then(..) 注册处理函数调用
    resolve(deferred.promise, res);
  });
}

具体处理注册回调函数 cb 为空情形,如下面示例。判断当前回调 cb 为空时,使用 deferred.promise 作为当前 promise 结合 value 调用后续处理函数继续往后执行,实现值穿透空处理函数往后传递。

Promise.resolve(233)
  .then()
  .then(function (value) {
    console.log(value)
  })

关于 then 链式调用,简单再说下。实现 then 函数的链式调用,只需要在 Promise.prototype.then(..) 处理函数中返回新的 promise 实例即可。但除此之外,还需要依次调用 then 注册的回调处理函数。如 handleResolved 函数最后一句 resolve(deferred.promise,res) 所示。

then 注册回调函数为什么异步执行

这里回答开篇所提到的一个问题, then 注册的 onResolvedonRejected 函数为什么要采用异步执行?再来看一段实例代码。

var a = 1;

promise1.then(function (value) {
  a = 2;
})

console.log(a)

promise1 内部执行同步或异步操作未知。假如未规定 then 注册回调为异步执行,则这里打印 a 可能存在两种值。promise1 内部同步操时 a === 2,相反执行异步操作时 a === 1。为屏蔽依赖外部的不确定性,规范指定 onFulfilledonRejected 方法异步执行。

promise 内部错误或异常

如果 promiserejected,则会调用拒绝回调并传入拒由。比如在 Promise 的创建过程中( fn执行时)出现异常,那这个异常会被捕捉并调用 onRejected

但还存在一处细节,如果 Promise 完成后调用 onResolved 查看结果时出现异常错误会怎么样呢?注意此时 onRejected 不会被触发执行,因为 onResolved 内部异常并不会改变当前 promise 状态(仍为 resolved),而是改变 then 中返回新的 promise 状态为 rejected。异常未丢失但也未调用错误处理函数。

如何处理? ECMAScript规范有定义 Promise.prototype.catch方法,假如你对 onResolved 处理过程没有信心或存在异常 case 情况,最好还是在 then 函数后调用 catch方法做异常捕获兜底处理。

Promise 相关的方法实现

查阅 Promise 相关文档或书籍,你还会发现 Promise 相关有用的API: Promise.racePromise.allPromise.resolvePromise.reject。这里对 Promise.race 方法实现做个展示,剩余可自行参考实现。

Promise.race = function (values) {
  return new Promise(function (resolve, reject) {
    values.forEach(function(value) {
      Promise.resolve(value).then(resolve, reject);
    });
  });
};

Generator Function 是 ES6 提供的一种异步流程控制解决方案。在此之前异步编程形式有,回调函数、事件监听、发布/订阅、Promise 等。但仔细思考前面解决方案,实际还是以回调函数作为基础,并没有从语法结构来改变异步写法。

区别于普通函数,Generator Function 可以在执行时暂停,后面又能从暂停处继续执行。通常在异步操作时交出函数执行权,完成后在同位置处恢复执行。新语法更容易在异步场景下达到以同步形式处理异步任务。

之前有写过关于 Promise 解决方式和内部原理实现。接续上文,此篇文章主要阐述 迭代器相关、Generator Function 语法、yield操作符、异步场景使用、常用自动执行器、Babel转译等。

注意后文将 Generator Function 翻译为生成器函数,个别处简述生成器。

迭代器

在了解生成器函数前,有必要先认识下迭代器。迭代器是一种特殊对象,具有专门为迭代流程设计的 next() 方法。每次调用 next() 都会返回一个包含 valuedone 属性的对象。ECMAScript 文档 The IteratorResult Interface 解释为:

  • done (布尔类型)
    • 如果迭代器遍历到迭代序列末端时 done 为 true
    • 如果迭代器仍可继续在序列中遍历时 done 为 false
  • value (任何类型)
    • 如果 done 为 false,值为当前迭代元素 value
    • 如果 done 为 true,且迭代器存在 return value 则为相应值
    • 如果没有返回值 则为 undefined

简单用 ECMAScript 5 语法创建一个符合迭代器接口示例:

function createIterator (items) {
  var i = 0

  return {
    next: function () {
      var done = (i >= items.length)
      var value = !done ? items[i++] : undefined

      return {
        done: done,
        value: value
      }
    }
  }
}

var iterator = createIterator([1, 2])

console.log(iterator.next())    // {done: false, value: 1}
console.log(iterator.next())    // {done: false, value: 2}
console.log(iterator.next())    // {done: true, value: undefined}

通常标准的 for 循环代码,使用变量 i 或 j 等来标示内部索引,每次迭代自增自减维系正确索引值。对比迭代器,循环语句语法简单,但是如果要处理多个循环嵌套则需要设置跟踪多个索引变量,代码复杂度会大大增加。迭代器的出现一定程度能消除这种复杂性,减少循环中的错误。

除此之外,迭代器提供一致的符合迭代器协议接口,可以统一可迭代对象遍历方式。例如 for...of 语句可以来迭代包含迭代器的可迭代对象(如 Array、Map、Set、String 等)。

生成器

生成器是一种返回迭代器的函数,通过 function 关键字后跟星号 (*) 来表示,此外函数中还需要包含新关键字 yield。将上面示例改写为生成器函数方式。

function *createIterator (items) {
  for (let i = 0; i < items.length; i++) {
    yield items[i]
  }
}

const iterator = createIterator([1, 2])

console.log(iterator.next())    // {done: false, value: 1}
console.log(iterator.next())    // {done: false, value: 2}
console.log(iterator.next())    // {done: true, value: undefined}

上述代码中,通过星号 (*) 表明 createIterator 是一个生成器函数,yield 关键字用来指定调用迭代器的 next() 方法时的返回值及返回顺序。

调用生成器函数并不会立即执行内部语句,而是返回这个生成器的迭代器对象。迭代器首次调用 next() 方法时,其内部会执行到 yield 后的语句为止。再次调用 next() ,会从当前 yield 之后的语句继续执行,直到下一个 yield 位置暂停。

next() 返回一个包含 value 和 done 属性的对象。value 属性表示本次 yield 表达式返回值,done 表示后续是否还有 yield 语句,即生成器函数是否已经执行完毕。

生成器相关方法如下:

  • Generator.prototype.next(),返回一个由 yield表达式生成的值
  • Generator.prototype.return(),返回给定的值并结束生成器
  • Generator.prototype.throw(),向生成器抛出一个错误

生成器函数继承于 FunctionObject,不同于普通函数,生成器函数不能作为构造函数调用,仅是返回生成器对象。完整的生成器对象关系图所示:

yield 关键字

yield 关键字可以用来暂停和恢复一个生成器函数。yield 后面的表达式的值返回给生成器的调用者,可以认为 yield 是基于生成器版本的 return 关键字。yield 关键字后面可以跟 任何值 或 表达式。

一旦遇到 yield 表达式,生成器的代码将被暂停运行,直到生成器的 next() 方法被调用。每次调用生成器的next()方法时,生成器都会在 yield 之后紧接着的语句继续执行。直到遇到下一个 yield 或 生成器内部抛出异常 或 到达生成器函数结尾 或 到达 return 语句停止。

注意,yield 关键字只可在生成器内部使用,在其他地方使用会导致语法错误。即使在生成器内部函数中使用也是如此。

function *createIterator (items) {
  items.forEach(item => {
    // 语法错误
    yield item + 1
  })
}

另外, yield* 可以用于声明委托生成器,即在 Generator 函数内部调用另一个 Generator 函数。

next 方法

Generator.prototype.next() 返回一个包含属性 done 和 value 的对象,也可以接受一个参数用以向生成器传值。返回值对象包含的 done 和 value 含义与迭代器章节一致,没有可过多说道的。值得关注的是,next() 方法可以接受一个参数,这个参数会替代生成器内部上条 yield 语句的返回值。如果不传 yield 语句返回值则为 undefined。例如:

function *createIterator (items) {
  let first = yield 1
  let second = yield first + 2
  yield second + 3
}

let iterator = createIterator()

console.log(iterator.next())    // {value: 1, done: false}
console.log(iterator.next(4))   // {value: 6, done: false}
console.log(iterator.next())    // {value: NaN, done: false}
console.log(iterator.next())    // {value: undefined, done: true}

有个特例,首次调用 next() 方法时无论传入什么参数都会被丢弃。因为传给 next() 方法的参数会替代上一次 yield 的返回值,而在第一次调用 next() 方法前不会执行任何 yield 语句,所以首次调用时传参是无意义的。

事实上能给迭代器内部传值的能力是很重要的。比如在异步流程中,生成器函数执行到 yield 关键字处挂起,异步操作完成后须传递当前异步值供迭代器后续流程使用。

异步流程控制

Generator 函数可以暂停和恢复执行,next() 可以做函数内外数据交换,这使得生成器函数可作为异步编程的完整解决方案。以一个异步场景为例:

function *gen () {
  const url = 'https://api.github.com/user/github'
  const result = yield fetch(url)
  console.log(result.bio)
}

上述代码中,Generator 函数封装了一个异步请求操作。除了增加 yield 关键字外,上面代码非常像同步操作。不过运行上述代码还需要一段执行器代码。

const g = gen()
const result = g.next()

result.value.then(function (data) => {
  g.next(data.json())
})

执行器相关代码先执行 Generator 函数获取遍历器对象,然后使用 next() 执行异步任务的第一阶段,在 fetch 返回的 promise.then 方法中调用 next 方法执行第二阶段操作。可以看出,虽然 Generator 函数把异步操作表示得很简洁,但是流程管理却不方便,需要额外手动添加运行时代码。

通常为了省略额外的手动流程管理,会引入自动执行函数辅助运行。假如生成器函数中 yield 关键字后全部为同步操作,很容易递归判断返回值 done 是否为 true 运行至函数结束。但更复杂的是异步操作,需要异步完成后执行迭代器 next(data) 方法,传递异步结果并恢复接下来的执行。但以何种方式在异步完成时执行 next(),需要提前约定异步操作形式。

常用的自动流程管理有 Thunk 函数模式 和 co 模块。co 同样可以支持 Thunk 函数 和 Promise 异步操作。在接下来解释自动流程管理模块前,先简单说道 Thunk 函数。

在 JavaScript 语言中,Thunk 函数指的是将多参数函数替换为一个只接受回调函数作为参数的单参数函数(注:这里多参数函数指的是类似 node 中异步 api 风格,callback 为最后入参)。类似于函数柯里化的转换过程,把接受多个参数变换成只接受一个单参数函数。以 node 中异步读取文件为例:

// 正常版本的 readFile(多参数)
fs.readFile(fileName, callback)

// Thunk 版本的 readFile (单参数)
const Tunk = function (fileName) {
  return function (callback) {
    return fs.readFile(fileName, callback)
  }
}

const readFileThunk = readFileThunk(fileName)
readFileThunk(callback)

其实任何函数参数中包含回调函数,都能写成 Thunk 函数形式。类似函数柯里化过程,简单的 Thunk 函数转换器如下所示。生成环境建议使用 Thunkify 模块,可以处理更多异常边界情况。

// Thunk 转换器
const Thunk = function (fn) {
  return function (...args) {
    return function (callback) {
      return fn.call(this, ...args, callback)
    }
  }
}

// 生成 fs.readFile Thunk 函数调用
const readFileThunk = Thunk(fs.readFile)
readFileThunk(fileA)(callback)

自动流程管理

先来介绍基于 Thunk 函数的自动流程管理,我们约定 yield 关键字后的表达式返回只接受 callback 参数的函数,即前面讲的 Thunk 类型函数。基于 Thunk Generator 简单自动执行器如下。

function run (fn) {
  var gen = fn()

  function next (err, data) {
    var result = gen.next(data)

    if (result.done) return

    result.value(next)
  }

  next()
}

上述自动执行器函数,迭代器首先运行到首个 yield 表达式处,yield 表达式返回只接受参数为 callback 的函数,同时将 next() 递归方法作为 callback 入参执行。当异步处理完成回掉 callback 时恢复执行生成器函数。

另外一种是基于 Promise 对象的自动执行机制。实际上 co 模块同样支持,Thunk 函数和 Promise 对象,两种模式自动流程管理。前者是将异步操作包装成 Thunk 函数,在 callback 中交回执行权,后者是将异步操作包装成 Promise 对象,在 then 函数中交回生成器执行权。

沿用上述示例,先将 fs 模块的 readFile 方法包装成 Promise 对象。

const fs = require('fs')

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function (err, data) {
      if (err) reject(err)

      resolve(data)
    })
  })
}

相较于 Thunk 模式在 callback 处理递归,Promise 对象的自动执行器,则是在 then 方法内调用递归处理方法。简单实现为:

function fun (gen) {
  const g = gen()

  function next (data) {
    var result = g.next(data)

    if (result.done) return result.value

    result.value.then((function (data) {
      next(data)
    }))
  }

  next()
}

翻阅 co 文档可以发现,yield 后对象支持多种形式:promises、thunks、array(promise)、objects(promise)、generators 和 generator functions。大致实现原理与上述一致,这里就不在贴 co 模块源码。更多信息可以参考 https://github.com/tj/co。

状态机

类似于 Promise,其实 Generator 也是有限状态机,翻阅 ECMAScript 文档 Properties of Generator Instances 会发现,生成器函数内部存在 undefinedsuspendedStartsuspendedYieldexecutingcompleted 五种状态。

从语意上很容易理解,伴随着生成器函数运行,内部状态发生相应变化。但具体 Generator 内部状态如何变化,这里暂时不继续写下去,会在下篇文章会结合 Generator es5 运行时源码详解。

Regenerator 转换器

由于浏览器端环境表现不一致,并不能全部原生支持 Generator 函数,一般会采用 babel 插件 facebook/regenerator进行编译成 es5 语法,做到低版本浏览器兼容。regenerator 提供 transformruntime 包,分别用在 babel 转码和 运行时支持。

不同于 Promise 对象引入 ployfill 垫片就可以运行,Generator 函数是新增的语法结构,仅仅依靠添加运行时代码是无法在低版本下运行的。Generator 编译成低版本可用大致流程为,编译阶段需要处理相应的抽象语法树(ast),生成符合运行时代码的 es5 语法结构。运行时阶段,添加 runtime 函数辅助编译后语句执行。

regenerator 网站 提供可视化操作,简单 ast 转码前后示例如下:

function *gen() {
  yield 'hello world'
}

var g = gen()

console.log(g.next())
var _marked = regeneratorRuntime.mark(gen);

function gen() {
  return regeneratorRuntime.wrap(function gen$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          _context.next = 2;
          return 'hello world';

        case 2:
        case "end":
          return _context.stop();
      }
    }
  }, _marked, this);
}

var g = gen();
console.log(g.next());

regenerator-transform 插件处理 ast 语法结构,regenerator-runtime 提供 运行时 regeneratorRuntime 对象支持。这里涉及到 babel 如何转码以及 运行时框架如何运行,内容较多会新起一篇文章再来细说。具体源码可参考 facebook/regenerator 项目。

插个话题,说到 babel 来科普下常见 bable-polyfillbabel-runtimebabel-plugin-transform-runtime 插件功能与区别。babel 只负责 es 语法转换,不会对新的对象或方法进行转换,比如 Promise、Array.from 等。babel-polyfill 或 babel-runtime 可以用来模拟实现相应的对象。

babel-polyfill 和 babel-runtime 两者的区别在于,polyfill 会引入新的全局对象,修改污染掉原有全局作用域下的对象。runtime 则是将开发者依赖的全局内置对象,抽取成单独的模块,并通过模块导入的方式引入,避免对全局作用域污染。

babel-runtime 与 babel-plugin-transform-runtime 区别在于,前者是实际导入项目代码的功能模块,后者是用于构建过程的运行时代码抽取转换,将所需的运行时代码引用自 babel-runtime。

可以参考两篇文章,babel-polyfill使用与性能优化,babel-runtime使用与性能优化。

Generator与协程关系

阮老师书中有提到相应的关系,可以在 Generator 函数章节查看。前端很少涉及进程、线程、协程知识点,这里就不在赘述。

可迭代协议和迭代器协议

前面说到迭代器,再顺便解释下 可迭代协议 和 迭代器协议。

可迭代协议允许 JavaScript 对象去定义它们的迭代行为, 例如在 for...of 结构中什么值可以循环。常用数据类型都内置了可迭代对象并且有默认的迭代行为, 比如 Array、Map, 注意 Object 默认不能使用 for...of 遍历。

为了变成可迭代对象,一个对象必须实现 @@iterator 方法, 可以在这个对象(或者原型链上的某个对象)设置 Symbol.iterator 属性,其属性值为返回一个符合迭代器协议对象的无参函数。

接着说迭代器协议,其定义了一种标准的方式来产生序列值。即迭代器对象必须实现 next()方法且 next() 包含 done 和 value 属性。两个属性同上,前面有过详细解释。

简而言之,可迭代对象必须满足可迭代协议有 Symbol.iterator 方法, Symbol.iterator 方法返回符合迭代器协议对象,包含 next 方法。

看个示例,Object 对象默认不存在迭代器方法,不能使用 for...of 遍历。我们可以修改 Object 原型添加迭代器方法,可以来访问相应 key、value 属性值。

Object.prototype[Symbol.iterator] = function () {
  let i = 0 
  let done, value
  const items = Object.entries(this)

  return {
    next: function () {
      done = (i >= items.length)
      value = done ? undefined : {
        key: items[i][0],
        value: items[i][1]
      }
      i += 1

      return {
        done: done,
        value: value
      }
    }
  }
}

const obj =  {
  name: 'spurs',
  age: '23'
}

for (let item of obj) {
  console.log(item)
}

// {key: "name", value: "spurs"}
// {key: "age", value: "23"}

结语

啰哩啰嗦,写了一篇入门级文章,很多地方都是蜻蜓点水一句带过。本篇文章定位在 Generator 语法入门,后续会再写篇 Generator 构建与运行时源码分析。

目前异步流程最佳解决方案已是 async/await 组合,相比而言语义更清晰,不需要额外自动执行模块。但其本质上是 Generator 一种语法糖,更好的理解生成器函数会从根源上认识异步流程控制的发展历程。

相关链接可点击原文查看,最后如有错误,敬请指正。

原文发布于微信公众号 - 交互设计前端开发与后端程序设计(interaction_Designer)

原文发表时间:2018-07-24

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Kevin-ZhangCG

[ Java面试题 ]算法篇

20211
来自专栏noteless

[二]Java虚拟机 jvm内存结构 运行时数据内存 class文件与jvm内存结构的映射 jvm数据类型 虚拟机栈 方法区 堆 含义

JVM全称是Java Virtual Machine  ,既然是虚拟机,他终归要运行在物理机上

2501
来自专栏jessetalks

Javascript基础回顾 之(二) 作用域

参数传递的问题   在Javascript中所有的参数传递都是按值传递的。也就是说把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。...

2816
来自专栏小灰灰

Java 动手写爬虫: 五 对象池

第五篇,对象池的设计与实现 前面每爬取一个任务都对应一个Job任务,试想一下,当我们爬取网页越来越多,速度越来越快时,就会出现频繁的Job对象的创建和销毁,因...

2225
来自专栏喵了个咪的博客空间

zephir-(8)类和对象1

#zephir-类和对象1# ? ##前言## 先在这里感谢各位zephir开源技术提供者 zephir全面使用对象编程,这就是为什么拓展的使用方式只能是方法和...

2853
来自专栏landv

C语言_函数【转】

3883
来自专栏青玉伏案

ReactiveSwift源码解析(八) SignalProducer的代码的基本实现

在前面几篇博客中我们详细的聊了ReactiveSwift中的Bag、Event、Observer以及Signal的使用方式和代码实现。那么在接下来的这几篇博客中...

2037
来自专栏程序员互动联盟

【编程基础】C函数的调用过程

这几天在看GCC Inline Assembly,在C代码中通过asm或__asm__嵌入一些汇编代码,如进行系统调用,使用寄存器以提高性能能,需要对函数调用过...

3255
来自专栏苦逼的码农

Shell编程 --- 变量

(2).如果按作用范围的话,可分为自定义变量和环境变量(后面会将自定义变量和环境变量)。

915
来自专栏LIN_ZONE

PHP 反射的简单使用

1554

扫码关注云+社区

领取腾讯云代金券