导航
导航
文章目录
  1. 1)生成器概念
  2. 2)迭代器概念
  3. 3)生成器返回的对象
  4. 4)完整示例代码
    1. 4.1 简单演示
    2. 4.2 Generator 函数体内含有其他代码
    3. 4.3 抛出错误
  5. 5)遍历 Generator 的输出
    1. 5.1 自定义循环方法
    2. 5.2 原生封装的方法
  6. 6)Generator 中包含 Generator
  7. 7)给 Generator 传递参数
    1. 7.1 给 Generator 函数传参
    2. 7.2 给 Iterator 的 next() 传参
  8. 8)Generator 原理简述
  9. 参考资料

异步之一:生成器

解决回调地狱的异步操作,Async 函数是终极办法,但了解生成器和 Promise 有助于理解 Async 函数原理。由于内容较多,分三部分进行,这是第一部分,介绍生成器相关,第二部分介绍 Promise,第三部分介绍 Async 函数。

介绍生成器前先看两个英文单词及对应的中文翻译:

  • Generator 生成器
  • Iterator 迭代器

1)生成器概念

Generator 是 function,但和普通函数有很大区别。

并且调用 Genrerator 并 不是一次执行完函数体内的全部代码,而是分步骤进行:

  1. 第一次调用 Generator,不执行函数体里面的代码,返回一个 Iterator 对象,后续 Generator 的调用都是通过 Iterator 对象完成;
  2. 每一次调用 Iterator 的next()方法,都会执行 Generator 里面的代码:
    2.1 如果遇到yield关键字,Generator 非阻塞挂起,同时返回一个对象({value: 'yieldValue', done: false}),执行步骤 3;
    2.2 如果没遇到yield关键字 Generator 就结束了,那么表示 Generator 执行完毕,同时,也返回一个对象,固定格式{value: undefined, done: true}
  3. 当再一次调用 Iterator 的next()方法时, Gererator 从前一次挂起的地方接着执行,然后重复步骤 2。

名词解释,“正在等待的 yield”:使 Generator 进入当前挂起状态的 yield 称为 “正在等待的 yield”。Generator 再次执行使从“正在等待的 yield 语句”之后开始执行。这个概念在通过next()给 Generator 传参时会用到。

生成器通过在function关键字后面加一个*号定义 Generator 函数,如下:

// 定义 generator 函数
function* generatorFn() {}

2)迭代器概念

Iterator 是调用 Generator 后返回一个对象,Iterator 关键属性是next()方法,触发 Generator 函数体内的代码  执行。同时,还有一个throw()可以结束 Generator 的执行,同时抛出一个错误

// Iterator 结构
{
next() {
// 触发 generator 函数体内的代码执行
},
throw() {
// 结束 Generator 的执行,同时抛出一个错误
},
}

3)生成器返回的对象

执行Iterator.next()后,生成器返回的对象结构如下:

// 如果 Generator 未结束
{
value: yieldValue,
done: false,
}

// 如果 Generator 结束
{
value: undefined,
done: true,
}

4)完整示例代码

4.1 简单演示

function* genFn() {
yield 111;
yield 222;
}

// iControl 是迭代器
var iControl = genFn();

/**
* 第 1 次调用,输出 111 后挂起
*/

console.log(iControl.next()); // {value: 111, done: false}

/**
* 第 2 次调用,从 yield 111; 之后开始执行,输出 222 后挂起
*/
console.log(iControl.next()); // {value: 222, done: false}

/**
* 第 3 次调用,从 yield 222; 之后开始执行
* 但此时无代码可执行,执行完毕,并有固定输出
*/
console.log(iControl.next()); // {value: undefined, done: true}

如果结束后继续调用iControl.next()则一直返回{value: undefined, done: true}

Object.prototype.toString.call(iControl)返回 “[object Generator]"
Object.prototype.toString.call(genFn)返回 “[object GeneratorFunction]"

4.2 Generator 函数体内含有其他代码

NOTICE:Generator 中如果yield关键字前后有其他代码,都会正常同步执行,见下面实例代码:

function* genFn() {
console.log(111);
yield 111;
console.log(222);
yield 222;
console.log(333);
}

var iControl = genFn();
console.log(iControl.next());
console.log(iControl.next());
console.log(iControl.next());

执行上面代码,打印顺序为:

// 第一次调用后挂起
111
{value: 111, done: false}

// 第二次调用后挂起
222
{value: 222, done: false}

// 最后一次调用后结束,注意结束之前先把前面的代码执行完,这里对应先打印 333
333
{value: undefined, done: true}

4.3 抛出错误

function* genFn() {
try {
yield 111;
yield 222;
} catch (e) {
console.log(e);
}
}

var iControl = genFn();

console.log(iControl.next()); // {value: 111, done: false}

console.log(iControl.throw("出错了")); // '出错了',之后结束,输出 {value: undefined, done: true}

5)遍历 Generator 的输出

5.1 自定义循环方法

 通过循环调用 Iterator 的next()方法可以遍历 Generator 的输出:

function* genFn() {
yield 111;
yield 222;
}

var iControl = genFn();
var item;

// 给 item 赋值后再进行判断,赋值的过程同时执行了 Iterator 的 next() 方法
while (!(item = iControl.next()).done) {
console.log(item.value);
}

5.2 原生封装的方法

可以通过 JS 内置let - of实现对迭代器对象的值序列的遍历

function* genFn() {
yield 111;
yield 222;
}

for (value of genFn()) {
console.log(value);
}

6)Generator 中包含 Generator

如果 Generator 中含有子 Generator,会进入子 Genretator 中执行,子 Generator 执行完,父 Generator 继续执行,如下:

function* genFn() {
yield 111;
yield* genFn2();
yield 222;
}

function* genFn2() {
yield "aaa";
yield "bbb";
}

for (value of genFn()) {
console.log(value); // 依次输出 111, 'aaa', 'bbb', 222
}

7)给 Generator 传递参数

可以通过两种形式给 Generator 传参:1,通过给 Generator 函数本身传递参数;2,通过给 Iterator 的next()方法传参。

7.1 给 Generator 函数传参

通过给 Generator 函数传递参数,在整个 Generator 生命周期中都可以访问,如下:

function* genFn(param) {
yield 1 + param;
yield 2 + param;
}

var iControl = genFn(10);
console.log(iControl.next().value); // 11
console.log(iControl.next().value); // 12

7.2 给 Iterator 的 next() 传参

通过给 Iterator 的next()传递的参数,会替换正在等待的整个 yield 语句,见下面例子:

function* genFn() {
const a = yield 1;
const b = yield 2 + a;

console.log(b);
}

var iControl = genFn();
console.log(iControl.next().value); // 1

// 传参后替换前一个 yield,所以代码变为 const b = yield 2 + 100;
console.log(iControl.next(100).value); // 102

// 传参后替换前一个 yield,所以代码变为 console.log(100);
console.log(iControl.next(100).value); // 100

注意,因为第一个next()调用时没有等待的yield语句,所以第一次调用next()时传递的参数会被忽略。

如果想在第一次调用next()时使用参数,可以给 Generator 函数本身传参

8)Generator 原理简述

  • Generator 函数能够 挂起-执行 本质是它的执行上下文在函数执行结束之前不会销毁(_挂起_ 是中间阶段,没有结束,所以上下文在 _挂起_ 的时候不会销毁);
  • 但当 Generator 挂起的时候(遇到yield关键字)会离开函数调用栈让渡给其他函数,实现非阻塞;
  • 同时,Generator 返回的 Iterator 对象保存着 Generator 的执行上下文信息,所以可以在 Generator 重新回到函数调用栈时(通过next()或者throw()方法触发)从上次挂起的位置继续执行;
  • Generator 执行完成,它的上下文随即销毁。

这里的关键在于,Generator 挂起的时候上下文会得到保持,配合yieldnext()throw()的工作,实现 挂起-执行 的循环,直到执行结束。

从调用开始到结束,其生命周期可描述如下:

调用 Generator --> 挂起开始 --> 执行 [--> yield 挂起 --> 执行 --> ...] --> 结束

首次调用 Generator 会立即进入挂起等待执行阶段。

Generator 函数的工作机制 很像闭包:返回的 Iterator 对象保持对 Generator 上下文的引用,所以 Generator 的上下文在离开函数调用栈的时候可以保持。

参考资料

[美]JOHN RESIG,BEAR BIBEAULT and JOSIP MARAS 著(2016),Secrets of the JavaScript Ninja (Second Edition),第 6 章 生成器部分,Manning Publications Co.