JavaScript Generator异步过度的实现详解

异步过渡方案Generator

在使用 Generator 前,首先知道 Generator 是什么。

如果读者有 Python 开发经验,就会发现,无论是概念还是形式上,ES2015 中的 Generator 几乎就是 Python 中 Generator 的翻版。

Generator 本质上是一个函数,它最大的特点就是可以被中断,然后恢复执行。通常来说,当开发者调用一个函数之后,这个函数的执行就脱离了开发者的控制,只有函数执行完毕之后,控制权才能重新回到调用者手中,因此程序员在编写方法代码时,唯一

能够影响方法执行的只有预先定义的 return 关键字。

Promise 也是如此,我们也无法控制 Promise 的执行,新建一个 Promise 后,其状态自动转换为 pending,同时开始执行,直到状态改变后我们才能进行下一步操作。

Generator 函数不同,Generator 函数可以由用户执行中断或者恢复执行的操作,Generator 中断后可以转去执行别的操作,然后再回过头从中断的地方恢复执行。

1. Generator 的使用

Generator 函数和普通函数在外表上最大的区别有两个:

  • function 关键字和方法名中间有个星号(*)。
  • 方法体中使用 yield 关键字。
function* Generator() {
  yield "Hello World";
  return "end";
}

和普通方法一样,Generator 可以定义成多种形式:

// 普通方法形式
function* generator() {}
//函数表达式
const gen = function* generator() {}
// 对象的属性方法
const obi = {
  * generator() {
  }
}

Generator 函数的状态

yield 关键字用来定义函数执行的状态,在前面代码中,如果 Generator 中定义了 xyield 关键字,那么就有 x + 1 种状态(+1是因为最后的 return 语句)。

2. Generator 函数的执行

跟普通函数相比,Generator 函数更像是一个类或者一种数据类型,以下面的代码为例,直接执行一个 Generator 会得到一个 Generator 对象,而不是执行方法体中的内容。

const gen = Generator();

按照通常的思路,gen 应该是 Generator() 函数的返回值,上面也提到Generator 函数可能有多种状态,读者可能会因此联想到 Promise,一个 Promise 也可能有三种状态。不同的是 Promise 只能有一个确定的状态,而 Generator 对象会逐个经历所有的状态,直到 Generator 函数执行完毕。

当调用 Generator 函数之后,该函数并没有立刻执行,函数的返回结果也不是字符串,而是一个对象,可以将该对象理解为一个指针,指向 Generator 函数当前的状态。(为了便于说明,我们下面采用指针的说法)。

Generator 被调用后,指针指向方法体的开始行,当 next 方法调用后,该指针向下移动,方法也跟着向下执行,最后会停在第一个遇到的 yield 关键字前面,当再次调用 next 方法时,指针会继续移动到下一个 yield 关键字,直到运行到方法的最后一行,以下面代码为例,完整的执行代码如下:

function* Generator() {
  yield "Hello World";
  return "end";
}
const gen = Generator();
console.log(gen.next()); // { value: 'Hello World', done: false }
console.log(gen.next()); // { value: 'end', done: true }
console.log(gen.next()); // { value: undefined, done: true }

上面的代码一共调用了三次 next 方法,每次都返回一个包含执行信息的对象,包含一个表达式的值和一个标记执行状态的 flag

第一次调用 next 方法,遇到一个 yield 语句后停止,返回对象的 value 的值就是 yield 语句的值,done 属性用来标志 Generator 方法是否执行完毕。

第二次调用 next 方法,程序执行到 return 语句的位置,返回对象的 value 值即为 return 语句的值,如果没有 return 语句,则会一直执行到函数结束,value 值为 undefineddone 属性值为 true

第三次调用 next 方法时,Generator 已经执行完毕,因此 value 的值为undefined

2.1 yield 关键字

yield 本意为 生产 ,在 Python、Java 以及 C# 中都有 yield 关键字,但只有Python 中 yield 的语义相似(理由前面也说了)。

next 方法被调用时,Generator 函数开始向下执行,遇到 yield 关键字时,会暂停当前操作,并且对 yield 后的表达式进行求值,无论 yield 后面表达式返回的是何种类型的值,yield 操作最后返回的都是一个对象,该对象有 valuedone 两个属性。

value 很好理解,如果后面是一个基本类型,那么 value 的值就是对应的值,更为常见的是 yield 后面跟的是 Promise 对象。

done 属性表示当前 Generator 对象的状态,刚开始执行时 done 属性的值为false,当 Generator 执行到最后一个 yield 或者 return 语句时,done 的值会变成 true,表示 Generator 执行结束。

注意:yield关键字本身不产生返回值。例如下面的代码:

function* foo(x) {
  const y = yield(x + 1);
  return y;
}
const gen = foo(5);
console.log(gen.next()); // { value: 6, done: false }
console.log(gen.next()); // { value: undefined, done: true }

为什么第二个 next 方法执行后,y 的值却是 undefined

实际上,我们可以做如下理解:next 方法的返回值是 yield 关键字后面表达式的值,而 yield 关键字本身可以视为一个不产生返回值的函数,因此 y 并没有被赋值。上面的例子中如果要计算 y 的值,可以将代码改成:

function* foo(x) {
  let y;
  yield y = x + 1;
  return 'end';
}

next 方法还可以接受一个数值作为参数,代表上一个 yield 求值的结果。

function* foo(x) {
  const y = yield(x + 1);
  return y;
}
const gen = foo(5);
console.log(gen.next()); // { value: 6, done: false }
console.log(gen.next(10)); // { value: 10, done: true }

上面的代码等价于:

function* foo(x) {
  let y = yield(x + 1);
  y = 10;
  return y;
}
const gen = foo(5);
console.log(gen.next()); // { value: 6, done: false }
console.log(gen.next()); // { value: 10, done: true }

next 可以接收参数代表可以从外部传一个值到 Generator 函数内部,乍一看没有什么用处,实际上正是这个特性使得 Generator 可以用来组织异步方法,我们会在后面介绍。

2.2 next 方法与 Iterator 接口

一个 Iterator 同样使用 next 方法来遍历元素。由于 Generator 函数会返回一个对象,而该对象实现了一个 Iterator 接口,因此所有能够遍历 Iterator 接口的方法都可以用来执行 Generator,例如 for/ofaray.from()等。

可以使用 for/of 循环的方式来执行 Generator 函数内的步骤,由于 for/of 本身就会调用 next 方法,因此不需要手动调用。

注意:循环会在 done 属性为 true 时停止,以下面的代码为例,最后的 'end' 并不会被打印出来,如果希望被打印,需要将最后的 return 改为 yield

function* Generator() {
  yield "Hello Node";
  yield "From Lear"
  return "end"
}
const gen = Generator();
for (let i of gen) {
  console.log(i);
}
// 和 for/of 循环等价
console.log(Array.from(Generator()));;

前面提到过,直接打印 Generator 函数的示例没有结果,但既然 Generator 函数返回了一个遍历器,那么就应该具有 Symbol.iterator 属性。

console.log(gen[Symbol.iterator]);

// 输出:[Function: [Symbol.iterator]]

3. Generator 中的错误处理

Generator 函数的原型中定义了 throw 方法,用于抛出异常。

function* generator() {
  try {
    yield console.log("Hello");
  } catch (e) {
    console.log(e);
  }
  yield console.log("Node");
  return "end";
}
const gen = generator();
gen.next();
gen.throw("throw error");

// 输出
// Hello
// throw error
// Node

上面代码中,执行完第一个 yield 操作后,Generator 对象抛出了异常,然后被函数体中 try/catch 捕获。当异常被捕获后,Generator 函数会继续向下执行,直到遇到下一个 yield 操作并输出 yield 表达式的值。

function* generator() {
  try {
    yield console.log("Hello World");
  } catch (e) {
    console.log(e);
  }
  console.log('test');
  yield console.log("Node");
  return "end";
}
const gen = generator();
gen.next();
gen.throw("throw error");

// 输出
// Hello World
// throw error
// test
// Node 

如果 Generator 函数在执行的过程中出错,也可以在外部进行捕获。

function* generator() {
  yield console.log(undefined, undefined);
  return "end";
}
const gen = generator();
try {
  gen.next();
} catch (e) {
}

Generator 的原型对象还定义了 return() 方法,用来结束一个 Generator 函数的执行,这和函数内部的 return 关键字不是一个概念。

function* generator() {
  yield console.log('Hello World');
  yield console.log('Hello 夏安');
  return "end";
}
const gen = generator();
gen.next(); // Hello World
gen.return();
// return() 方法后面的 next 不会被执行
gen.next();

4. 用 Generator 组织异步方法

我们之所以可以使用 Generator 函数来处理异步任务,原因有二:

  • Generator 函数可以中断和恢复执行,这个特性由 yield 关键字来实现。
  • Generator 函数内外可以交换数据,这个特性由 next 函数来实现。

概括一下 Generator 函数处理异步操作的核心思想:先将函数暂停在某处,然后拿到异步操作的结果,然后再把这个结果传到方法体内。

yield 关键字后面除了通常的函数表达式外,比较常见的是后面跟的是一个 Promise,由于 yield 关键字会对其后的表达式进行求值并返回,那么调用 next 方法时就会返回一个 Promise 对象,我们可以调用其 then 方法,并在回调中使用 next 方法将结果传回 Generator

function* gen() {
  const result = yield readFile_promise("foo.txt");
  console.log(result);
}
const g = gen();
const result = g.next();
result.value.then(function (data) {
  g.next(data);
});

上面的代码中,Generator 函数封装了 readFile_promise 方法,该方法返回一个 PromiseGenerator 函数对 readFile_promise 的调用方式和同步操作基本相同,除了 yield 关键字之外。

上面的 Generator 函数中只有一个异步操作,当有多个异步操作时,就会变成下面的形式。

function* gen() {
  const result = yield readFile_promise("foo.txt");
  console.log(result);
  const result2 = yield readFile_promise("bar.txt");
  console.log(result2);
}
const g = gen();
const result = g.next();
result.value.then(function (data) {
  g.next(data).value.then(function (data) {
    g.next(data);
  })
});

然而看起来还是嵌套的回调?难道使用 Generator 的初衷不是优化嵌套写法吗?说的没错,虽然在调用时保持了同步形式,但我们需要手动执行 Generator 函数,于是在执行时又回到了嵌套调用。这是 Generator 的缺点。

5. Generator 的自动执行

Generator 函数来说,我们也看到了要顺序地读取多个文件,就要像上面代码那样写很多用来执行的代码。无论是 Promise 还是 Generator,就算在编写异步代码时能获得便利,但执行阶段却要写更多的代码,Promise 需要手动调用 then 方法,Generator 中则是手动调用 next 方法。

当需要顺序执行异步操作的个数比较少的情况下,开发者还可以接受手动执行,但如果面对多个异步操作就有些难办了,我们避免了回调地狱,却又陷到了执行地狱里面。我们不会是第一个遇到自动执行问题的人,社区已经有了很多解决方案,但为了更深入地了解 PromiseGenerator,我们不妨先试着独立地解决这个问题,如何能够让一个 Generator 函数自动执行?

5.1 自动执行器的实现

既然 Generator 函数是依靠 next 方法来执行的,那么我们只要实现一个函数自动执行 next 方法不就可以了吗,针对这种思路,我们先试着写出这样的代码:

function auto(generator) {
  const gen = generator();
  while (gen.next().value !== undefined) {
    gen.next();
  }
}

思路虽然没错,但这种写法并不正确,首先这种方法只能用在最简单的 Generator 函数上,例如下面这种:

function* generator() {
  yield 'Hello World';
  return 'end';
}

另一方面,由于 Generator 没有 hasNext 方法,在 while 循环中作为条件的:gen.next().value !== undefined 在第一次条件判断时就开始执行了,这表示我们拿不到第一次执行的结果。因此这种写法行不通。

那么换一种思路,我们前面介绍了 for/of 循环,那么也可以用它来执行 Generator

function* Generator() {
  yield "Hello World";
  yield "Hello 夏安";
  yield "end";
}
const gen = Generator();
for (let i of gen) {
  console.log(i);
}

// 输出结果
// Hello World
// Hello 夏安
// end

看起来没什么问题了,但同样地也只能拿来执行最简单的 Generator 函数,然而我们的主要目的还是管理异步操作。

5.2 基于Promise的执行器

前面实现的执行器都是针对普通的 Generator 函数,即里面没有包含异步操作,在实际应用中,yield 后面跟的大都是 Promise,这时候 for/of 实现的执行器就不起作用了。

通过观察,我们发现 Generator 的嵌套执行是一种递归调用,每一次的嵌套的返回结果都是一个 Promise 对象。

const g = gen();
const result = g.next();
result.value.then(function (data) {
  g.next(data).value.then(function (data) {
    g.next(data);
  })
});

那么,我们可以根据这个写出新的执行函数。

function autoExec(gen) {
  function next(data) {
    const result = gen.next(data);
    // 判断执行是否结束
    if (result.done) return result.value;
    result.value.then(function (data) {
      next(data);
    });
  }
  next();
}

这个执行器因为调用了 then 方法,因此只适用于 yield 后面跟一个 Promise 的方法。

5.3 使用 co 模块来自动执行

为了解决 generator 执行的问题,TJ 于2013年6月发布了著名 co 模块,这是一个用来自动执行 Generator 函数的小工具,和 Generator 配合可以实现接近同步的调用方式,co 方法仍然会返回一个 Promise

const co = require("co");
function* gen() {
  const result = yield readFilePromise("foo.txt");
  console.log(result);
  const result2 = yield readFilePromise("bar.txt");
  console.log(result2);
}
co(gen);

只要将 Generator 函数作为参数传给 co 方法就能将内部的异步任务顺序执行,要使用 co 模块,yield 后面的语句只能是 promsie 对象。

到此为止,我们对异步的处理有了一个比较妥当的方式,利用 generator+co,我们基本可以用同步的方式来书写异步操作了。但 co 模块仍有不足之处,由于它仍然返回一个 Promise,这代表如果想要获得异步方法的返回值,还要写成下面这种形式:

co(gen).then(function (value) {
  console.log(value);
});

另外,当面对多个异步操作时,除非将所有的异步操作都放在一个 Generator 函数中,否则如果需要对 co 的返回值进行进一步操作,仍然要将代码写到 Promise 的回调中去。

到此这篇关于JavaScript Generator异步过度的实现详解的文章就介绍到这了,更多相关JavaScript Generator 内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

若文章对您有帮助,帮忙点个赞!

0
-5
发布时间 2022-08-05 21:00:58
0 条回复(回复会通过微信通知作者)
点击加载更多评论
登录 后再进行评论
(微信扫码即可登录,无需注册)