JavaScript Generator 函数的执行过程和消息传递机制

899 阅读5分钟

本篇文章以若干具体的生成器函数为例,逐步分析其 执行过程,详细展示生成器函数 内部执行流外部控制流 之间的 消息传递机制,包括 普通消息传递异常流传播 和生成器实例的 主动终结,最后简单展示 yield* 表达式的执行过程。

Tips: 代码注释中的 >> 表示 console.log 打印的内容,-> 表示函数调用返回的内容。

1. yieldnext()

yield ..next(..) 这一对组合起来,在生成器的执行过程中构成一个双向消息传递系统

——《你不知道的JavaScript》(中卷)

function * genFunc() {
    console.log('1st next()')           // inner_1

    console.log(yield 'yieldValue_1')   // inner_2
    console.log('2nd next()')           // inner_3
    
    console.log(yield 'yieldValue_2')   // inner_4
    console.log('3rd next()')           // inner_5
    return 'returnValue'                // inner_6
}

// 调用 genFunc() 获得一个生成器实例
const gen = genFunc()

// gen.next() 可接收一个参数,无参调用等同于传入一个 undefined 参数
gen.next()
/**
 * 第一次调用 next(),启动 generator,开始执行 genFunc() 函数体代码
 * inner_1: >> 1st next()
 * inner_2: 碰到 yield 语句
 *     1. 计算 yield 后面的表达式,保存计算值 value
 *     2. 暂停执行 genFunc(),等待下一次 next() 调用
 *     3. 此次 next() 调用返回 { value, done: false }
 * 
 * -> { value: 'yieldValue_1', done: false }
 */

gen.next('nextArg_1')
/**
 * 恢复执行 genFunc()
 * inner_2: yield 接收 next() 传进来的参数 'nextArg_1',
 *     作为 yield 语句的计算值
 *     >> nextArg_1
 * inner_3: >> 2nd next()
 * inner_4: 碰到 yield 语句
 *     1. 计算 yield 后面的表达式,保存计算值 value
 *     2. 暂停执行 genFunc(),等待下一次 next() 调用
 *     3. 此次 next() 调用返回 { value, done: false }
 *
 * -> { value: 'yieldValue_2', done: false }
 */
 
 gen.next('nextArg_2')
 /**
  * 恢复执行 genFunc()
  * inner_4: yield 接收 next() 传进来的参数 'nextArg_2',
  *     作为 yield 语句的计算值
  *     >> nextArg_1
  * inner_5: >> 3rd next()
  * inner_6: 碰到 return 语句
  *     1. 计算 return 语句后面的表达式,保存计算值 value
  *     2. genFunc() 执行结束
  *     3. 此次 next() 调用返回 { value, done: true }
  *
  * 若 genFunc() 不含 return 语句,则当 genFunc 执行结束时,
  * 相应的 next() 调用返回 { value: undefined, done: false }
  *
  * -> { value: 'returnValue', done: true }
  */
 
 gen.next('anyArgs')
 /**
  * 此后每次 next() 调用都返回 { value: undefined, done: true }
  */

2. 生成器的异常流

2.1 生成器函数内部抛出的异常从相应的 next() 调用处流出

若生成器函数内部抛出的异常未在函数内部被捕获,则该异常从相应的 next() 调用处流出。

function *genFunc() {
    yield 'yieldValue'          // inner_1
    throw 'innerExceptionValue' // inner_2
    return 'returnValue'        // inner_3
}

const gen = genFunc()

gen.next()
/**
 * -> { value: 'yieldValue', done: false }
 */

try {
    gen.next()                   // outer_1
} catch ('ouer catch:', value) { // outer_2
    console.log(value)           // outer_3
}
/**
 * outer_1: gen.next() 恢复 genFunc() 函数的执行,
 * inner_2: 抛出异常,该异常没有在 genFunc() 内部被捕获
 *     1. genFunc() 异常结束
 *     2. 生成器实例 gen 迭代结束,此后调用 gen.next()
 *        总是返回 { value: undefined, done: true }
 * outer_1: genFunc() 内部的抛出的异常从 gen.next() 流出
 * outer_2: 距离最近的 catch 语句捕获了异常
 * outer_3: >> outer catch: innerExceptionValue
 */

gen.next()
/**
 * -> { value: undefined, done: true }
 */

2.2 Generator.prototype.throw() 抛出的异常从相应的 yield 处流出

每个生成器实例都从 Generator.prototype 继承了 throw() 方法。

gen.throw() 抛出的异常首先流入生成器函数内部,从相应的 yield 处流出。该异常可在生成器函数内部捕获。

function *genFunc() {
    try {
        yield 'yieldValue_1'               // inner_1
    } catch (value) {                      // inner_2
        console.log('inner catch:', value) // inner_3
    }
    yield 'yieldValue_2'                   // inner_4
    return 'returnValue'                   // inner_5
}

const gen = genFunc()

gen.next()
/**
 * -> { value: 'yieldValue_1', done: false }
 */
 
gen.throw('outerExceptionValue') // outer_1
/**
 * outer_1:
 *     1. gen.throw() 抛出异常
 *     2. genFunc() 恢复执行
 *     3. 异常流入 genFunc() 内部
 * inner_1: 异常从 yield 语句流出
 * inner_2: 距离最近的 catch 语句捕获该异常
 * inner_3: >> inner catch: outerExceptionValue
 * inner_4: 碰到 yield 语句
 *
 * -> { value: 'yieldValue_2', done: false }
 */
 
gen.next()
/**
 * -> { value: 'returnValue', done: true }
 */

gen.throw() 抛出的异常在生成器函数内部没有被捕获,则该异常从 gen.throw() 处流出。

gen.throw() 抛出的异常在生成器函数内部被捕获,此次 gen.throw() 调用触发的函数执行过程中,若有其他未被捕获的异常,也会从 gen.throw() 处流出。

所有没有在生成器函数内部捕获的异常都会从相应的 gen.next()gen.throw() 调用处流出。发生这种情况时,生成器实例迭代结束。

3. 主动终结生成器实例

每个生成器实例都从 Generator.prototype 继承了 return() 方法。

调用 gen.return(val) 可主动终结生成器实例,返回 { value: val, done: true }。若调用时不提供参数,返回值为 { value: undefined, done: true }

next()throw()return() 的共同点

next()throw()return()这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换 yield 表达式。

next() 是将 yield 表达式替换成一个值。

const g = function* (x, y) {
  let result = yield x + y;
  return result;
};

const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}

gen.next(1); // Object {value: 1, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = 1;

上面代码中,第二个 next(1) 方法就相当于将 yield 表达式替换成一个值 1。如果 next 方法没有参数,就相当于替换成 undefined

throw() 是将 yield 表达式替换成一个 throw 语句。

gen.throw(new Error('出错了')); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));

return()是将yield表达式替换成一个return语句。

gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;

—— 《ESCMAScript 6 入门》阮一峰

4. yield* 表达式(yield 委托)

语法:yield* [[expression]]

expression 时返回一个可迭代对象的表达式。

yield* 表达式迭代操作数,并产生它返回的每个值。

yield* 表达式本身的值是当迭代器关闭时返回的值(即 donetrue 时)

—— MDN yield*

function* foo() {
    yield 'foo1'
    yield 'foo2'
}

function* bar() {
    yield 'bar1'
    yield 'bar2'
    return 'bar'
}

function* baz() {
    yield 'baz1'
    console.log('yield* foo() return:', yield* foo())
    console.log('yield* foo() return:', yield* bar())
    yield 'baz2'
}

const gen = baz()

for (let v of gen) {
    console.log(v)
}

/* >>
baz1
foo1
foo2
yield* foo() return: undefined
bar1
bar2
yield* foo() return: bar
baz2
*/