JS异步编程(4)-Generator

240 阅读5分钟

这是我参与更文挑战的第4天,活动详情查看:更文挑战

Generator 是与 Promise 同时由 ES6 引入标准的语法,最早由社区提出和实现
主要用于实现一种新的状态机制管理,让一段代码逻辑可以动态控制分段执行

Generator 新增关键字及函数

* 关键字

* 关键词用于生成 Generator函数* 前后的空格没有影响),但有以下几个限制:

  • 只能用于 function 关键字
  • 不能用于箭头函数
function * fun1 () {} // work
function* fun2 () {} // work
function *fun3 () {} // work
const fun4 = function * () {} // work

const fun5 = *() => {} // 报错!

一旦函数被生成为 Generator函数,这个函数就不能作为构造函数被 new 关键字使用了

function * Fun() {}
const obj = new Fun() {} // 报错!

yield 关键字

yield 的返回值

yield 在功能上和 return 有些类似,都用于将值返回,但有些差异:

  • return 语句之后无法进行任何操作;yield 后续的程序可以通过调用 next 继续执行
  • return 无法和赋值符一起使用;yield 可以和赋值符一起使用
  • return 可以独立使用;yield 必须配合 Generator函数next 函数才能使用
function * test() {
    const first = yield 1
    const second = yield 2
    const third = yield 3
}
const obj = test()
obj.next() // {value: 1, done: false}
obj.next() // {value: 2, done: false}
obj.next() // {value: 3, done: false}
obj.next() // {value: undefined, done: true}

yield 可以拼接的表达式

yield 后面可以拼接多种类型的值

  • 基本类型:yield '123'yield 123yield { name: 'Peter' } 等等
  • 函数:yield function test(){}
  • 表达式:yield 1 + 2 + 3

多个 yield 拼接

yieldyield 拼接时,后一个 yield 作为变量使用,为 undefined

function * test() {
    yield yield
}
const obj = test()
obj.next() // {value: undefined, done: false}

yield 的类函数形式

看一段代码

function * test() {
    yield 1
}
// 上面的写法,等价于下方的写法
function * test() {
    yield(1)
}

yield(1) 咋一看,会让人觉得是 yield 作为一个函数被调用了
其实是 yield 返回了一个使用小括号包裹的表达式(1)
这里纯粹是个人见解,没有找到权威的文档佐证,欢迎指正!

next 函数

第一次 next 调用的传参没有作用

function *test(x) {
    const re = yield 1 + x;
    return re + 2;
}

const obj = test(5);
obj.next(2) // {value: 6, done: false}
obj.next(3) // {value: 5, done: true}

如上

  1. 变量 x 来自生成 Generator对象 时的参数 5
  2. 第一次 next 调用,函数内部开始执行到 yield 返回 1 + 5,因此本次 next 参数无效
  3. 第二次 next 调用,next 的参数 3 赋值给 yield 后面的整个表达式 const re = 3,因此结果为 3 + 2

next 函数参数是对上一个 yield 及后方整个表达式的值覆盖

function *test() {
    console.log(yield 5) // 2
}

const obj = test();
obj.next()
obj.next(2)

第二次 next 调用,yield 5 的值为 2,被参数覆盖

yield 和 next 的返回值

对于 yieldnext 的返回值问题一直比较模糊
{ value: any, done: Boolean } 到底是 yield 的返回值还是 next 的返回值?

下面分别引用了 MDN 上对 yieldnext 返回值的描述

yield关键字实际返回一个IteratorResult对象,它有两个属性,value和done。value属性是对yield表达式求值的结果,而done是false,表示生成器函数尚未完全完成。

参考自yield

next() 方法返回一个包含属性 done 和 value 的对象。该方法也可以通过接受一个参数用以向生成器传值。

参考自Generator.prototype.next()

说实话,从两段描述上来看,yieldnext 返回值都是 { value: any, done: Boolean }

所以写了下方的代码做验证

function *test() {
    console.log(yield 1) // 2
}

const obj = test();
obj.next()
obj.next(2) // { value: undefined, done: true }

从结果上来看

  • yield 返回的是 value
  • next 返回的是 { value: undefined, done: true }

Generator 执行流程

  1. 生成 Generator对象,这时候 Generator函数 内部不会执行
function *test() {
    console.log('start')
    let re = yield 1;
    return re + 2;
}

const testObj = test(); // no console
const testObj2 = test(); // no console

如果生成多个 Generator对象,则各个 Generator对象 保持着各自的执行阶段,互不影响

  1. Generator对象调用 next 函数 从函数开头或上一个 yield 开始,执行到下一个 yield 或者 return
function *test() {
    console.log('before first yield')
    let first = yield 1;
    console.log('after first yield')
    let second = yield 2;
    console.log('after second yield')
    let third = yield 3;
    console.log('after third yield')
    return re + 2;
}

const testObj = test(); // no console
const firstResult = testObj.next(); // before first yield
const secondResult = testObj.next(); // after first yield
const thirdResult = testObj.next(); // after second yield
const fourthResult = testObj.next(); // after third yield

yield 委托迭代

*yield 组合使用,可以将多个生成器连接在一起

function * anotherGenerator(i) {
  yield i + 1;
  yield i + 2;
  yield i + 3;
}

function * generator(i) {
  yield* anotherGenerator(i);
}

var gen = generator(1);

gen.next().value; // 2
gen.next().value; // 3
gen.next().value; // 4

类型判断

业界常见的判断 Generator 对象和函数的方法

如何判断 Generator 对象

function isGenerator(obj) {
    return obj && typeof obj.next === 'function' && typeof obj.throw === 'function';
}

这里运用鸭子模型进行判断
如果对象中有 nextthrow 两个方法,那么就认为这个对象是一个生成器对象

如何判断 Generator 函数

function isGeneratorFunction(obj){
    var constructor = obj.constructor;
    if(!constructor) return false;
    if(
        'GeneratorFunction' === constructor.name ||
        'GeneratorFunction' === constructor.displayName
    ) return true;
    return isGenerator(constructor.prototype);
}

利用函数的 constructor 构造器的名字来判断(name 与 displayName 为了处理兼容性)
这里递归调用 isGenerator 判断 constructor 的原型是因为有自定义迭代器的存在

总结

Generator 函数作为一种新的状态机制管理,让一段代码逻辑可以动态控制分段执行,有独到的作用和使用场景

但如果独立使用,操作繁琐,语义也不清晰
所以在 ES6 引入后,社区使用例如 co 库进行二次封装

在前端异步编程领域,开阔思路的作用大于实际应用,有点昙花一现的意思
为 Async/Await 做铺垫