重学ES6(八)Generator

737 阅读5分钟

基础概念

Generator 函数是一个状态机,封装了多个内部状态。同时也是一个遍历器对象生成函数。返回的遍历器对象,可以一次遍历Generator 函数内部的每个状态。

function* generator() {
	yield 'hello'
    yield 'world'
    return 'ending'
}
var gen = generator()

gen.next()
// { value: 'hello', done: false }
gen.next()
// { value: 'world', done: false }
gen.next()
// { value: 'ending', done: false } 
gen.next()
// { value: undefined, done: false }

yield 表达式

由于 Generator 函数返回的遍历器对象,只有调用 next 方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停的标志。

遍历器对象 next 的执行逻辑

  1. 遇到 yield 表达式,就暂停执行后面的操作,并将紧跟在 yield 后面的那个表达式的值,作为返回的对象的 value 属性值。
  2. 下次调用 next 方法时,再继续往下执行,直到遇到下一个 yield 表达式。 3.如果没有再遇到新的 yield 表达式,就一直运行到函数结束,知道 return语句为止,并将return语句后面的表达式的值,作为返回对象的 value的值。
  3. 如果没有return 语句,则返回的对象 value 属性值为 undefined。

yield 表达式后面的表达式,只有当调用 next 方法、内部指针指向该语句时才会执行。

function *gen () {
	yield 123 + 456
}

上面代码 yield 后面表达式不会立即执行,只会在 next 方法将指针移到这一句时,才会求值。

generator 函数可以不用 yield 表达式,这时就变成了一个淡出的暂缓执行函数。

function* f(){
	console.log('执行了')
}

var gen = f()

setTimeout(() => {
	gen.next()
}, 2000)

上面代码中, f 如果是普通函数,在为变量 gen 赋值的时候就会执行。但是函数 f 是 generator 函数,就只有调用 next 方法时,函数 f 才会执行。

与 Iterator 接口的关系

之前我们说过,任意一个 Symbol.iterator 方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的遍历器对象。

由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的 Symbol.iterator属性,从而使得该对象具有 Iterator 接口。

var itertor = {}
itertor[Symbol.iterator] = function* () {
	yield 1;
    yield 2;
    yield 3;
}

[...itertor]	// 1 2 3

// 原来是这样写的
itertor[Symbol.iterator] = function () {
	var current = 0   
    return {
        next: function(){
            if(current < 3) {
                current++
                return {
                    value: current,
                    done: false
                }
            } else {
                return {
                    value: undefined,
                    done: true
                }
            }
        }
    }
}

generator 函数执行后,返回一个遍历器对象。该对象本身也具有 Symbol.iterator 属性,执行后返回自身。

function* gen() {}

var g = gen()

g[Symbol.iterator]() === g

next 方法的参数

yield 表达式本身没有返回值,或总是返回 undefined。next 方法可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值。

function *f() {
	for(var i = 0; true; i++) {
      var reset = yield i
      if(reset) { i = -1; }
    }
}

var g = f()

g.next()	// {value: 0, done: false}

g.next()	// {value: 1, done: false}

g.next(true)	// {value: 0, done: false}

next 方法可以带一个参数,这个参数会被当做上一个 yield 的返回值,所以这里传值 true,会被当做上一轮的返回值,赋值给了 reset,然后重置了 reset 的值,在执行 for 循环,然后得到 value 为 0。

如果要在第一次调用 next 方法时,就能输入值,就需要在 Generator 函数外在包一层

function wrapper(generatorFunction) {
  return function (...args) {
    let generatorObject = generatorFunction(...args);
    generatorObject.next();
    return generatorObject;
  };
}

const wrapped = wrapper(function* () {
  console.log(`First input: ${yield}`);
  return 'DONE';
});

let g = wrapped()
g.next('hello!')

这里实际上就是在 return generator 对象时之前,先执行了一遍 不传参的 next 方法。

for...of 循环

function* foo() {
	yield 1;
    yield 2;
    yield 3;
    yield 4;
    yield 5;
    return 6;
}

for (let v of foo) {
	console.log(v)
}
// 1 2 3 4 5 

通过 Generator 函数为对象添加 Iterator 接口,可以使用 for...of

function* objectEntries(obj) {
	let propKyes = Reflect.ownKeys(obj)
    
    for (let propKey of propKyes) {
    	yield [propKey, obj[propKey]]
    }
}

let jane = { first: 'Jane', last: 'Doe' };

for (let [key, value] of objectEntries(jane)) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

Generator.prototype.throw

var g = function* () {
    try {
        yield
    } catch (e) {
        console.log('内部捕获', e)
    }
}

var i = g()

try {
    i.throw('a')
    i.throw('b')
} catch(e) {
    console.log('外部捕获', e)
}

// 内部捕获 a
// 外部捕获 b

上面代码我们可以看到遍历器 i 连续抛出2个错误。第一个错误被Generator 函数体内部的 catch 语句捕获。 i 第二次抛出错误,由于 Generator 语句一句执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出 Generator 函数体了,被函数体外的 catch 捕获了。

Generator.prototype.return

Generator 函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历 Generator 函数。

function* gen() {
	yield 1;
    yield 2;
    yield 3;
}

var g = gen()

g.next()	// { value: 1, done: false }
g.return('foo')	// { value: foo, done: true }
g.next()	// { value: undefined, done: true }

yield* 表达式

如果再 Generator 函数内部,调用另一个 Generator 函数。需要在前者的函数体内部,自己手动完成遍历。

function* foo() {
	yield 'a'
    yield 'b'
}

function* bar() {
	yield 'x'
    // 手动遍历 foo()
    for (let i of foo()) {
    	console.log(i)
    }
    yield 'y'
}

for (let v of bar()) {
	cosole.log(v)
}
// x
// a
// b
// y

上述代码中,foo 和 bar 都是 Generator 函数,在 bar 里面调用 foo,就需要手动遍历 foo,如果多个 Generator 函数嵌套就很麻烦。现在我们使用 yield* 表达式来解决.

function* bar() {
	yield 'x'
    yield* foo()
    yield 'y'
}
// 等同于
function* bar() {
	yield 'x'
    yield 'a'
    yield 'b'
    yield 'y'
}

Generator 与状态机

var ticking = true
var clock = function() {
	if (ticking)
    	console.log('Tick!')
    else 
    	console.log('Tock!')
    ticking = !ticking
}

// 如果用 Generator 的方式
var clock = function* () {
  while (true) {
    console.log('Tick!')
    yield;
    console.log('Tock!')
    yield;
  }
}

应用

异步操作的同步化表达

ajax 是典型的异步操作,通过 generator 函数部署 ajax 操作

function* main(){
	var result = yield request('http://test.url')
    var resp = JSON.parse(result)
    console.log(resp.value)
}

function request(url) {
	makeAjax(url, function(response) {
    	it.next()
    })
}

var it = main()
it.next()

控制流管理

如果一个异步操作非常多,采用回调函数,可能会写成下面这样

step1(function(value1) {
	step2(value1, function(value2) {
    	step3(value2, function(value3) {
        	...
        })
    })
})

// 使用 Promise 的方式
Promise.resolve(step1)
	.then(step2)
    .then(step3)
    .then(function (value3) {
    	...
    }, function(error) {
    	...
    })
    .done()
    
// 使用 Generator
function* longRunningTask(value1) {
  try {
    var value2 = yield step1(value1);
    var value3 = yield step2(value2);
    var value4 = yield step3(value3);
    var value5 = yield step4(value4);
    // Do something with value4
  } catch (e) {
    // Handle any error from step1 through step4
  }
}

scheduler(longRunningTask(initialValue));

function scheduler(task) {
  var taskObj = task.next(task.value);
  // 如果Generator函数未结束,就继续调用
  if (!taskObj.done) {
    task.value = taskObj.value
    scheduler(task);
  }
}

注意,上面的做法,只适合同步操作,即所有的 task 都必须是同步的,不能有异步操作。

协程

传统的编程语言,早有异步编程的解决方案(其实是多任务解决方案)。其中一种叫做协程,意思是多个线程互相协作,完成异步任务。

协程有点像函数,又有点像线程。它的运行流程大致如下。

  • 第一步,协程A开始执行
  • 第二步,协程A执行到一半,进入暂停,执行权转移到协程B
  • 第三步,一段时间后,协程B交还执行权
  • 第四步,协程A恢复执行

Generator 方法,如果要能自动执行,并且能执行有异步的操作,可以和 Promise 联合使用。