Skip to content

Generator 学习

1. The Basics Of ES6 Generators

函数里用 yield 关键字来暂停函数运行。外部没有任何东西可以暂停它。所以当它暂停了,必须有外部控制来重新启动它。 通常一个函数,你刚开始传递参数进去,然后 用 return 来返回。而 Generator 函数,通过 yield 传递消息出去,每次启动的时候传递消息进去,传递的值是 yield 表达式的结果。

Generator 返回的是一个迭代器

js
function* foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
}
var it = foo();

yeild 调用

由于第 1 次调用 it.next(5) 时,因为并没有 yeild 表达式接收 5 这个值,都没有 yeild 执行过。 所以只能初始化 foo(5), 传递值 5 进去给 x。

js
function* foo(x) {
  var y = 2 * (yield x + 1);
  var z = yield y / 3;
  return x + y + z;
}

var it = foo(5);

// note: not sending anything into `next()` here
console.log(it.next()); // { value:6, done:false }
console.log(it.next(12)); // { value:8, done:false }
console.log(it.next(13)); // { value:42, done:true }

for..of

ES6 也可以用 for..of 来迭代 generator, 但是注意下面的例子, return 返回的值并不能被迭代出, 所以 generator 里不建议用 return; 另外一个角度理解, 函数的最后 return undefined

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

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

console.log(v); // still `5`, not `6`

2. Diving Deeper With ES6 Generators

如何往 Generator 里面抛入一个异常; 注意抛进去的异常, 并不是赋值给了 x, 而是给了 catch 里的 err。

js
function* foo() {
  try {
    var x = yield 3;
    console.log("x: " + x); // may never get here!
  } catch (err) {
    console.log("Error: " + err);
  }
}
var it = foo();
var res = it.next(); // { value:3, done:false }
it.throw("Oops!"); // Error: Oops!

如果你往 Generator 里面抛异常, Generator 没有用 try...catch 捕获异常的话,异常就会抛出来,可以在外面捕获这个异常

js
function* foo() {
  var x = yield 3;
  console.log("x: " + x); // may never get here!
}
var it = foo();
var res = it.next(); // { value:3, done:false }
try {
  it.throw("Oops!");
} catch (e) {
  console.log("Error: " + e); // Error: Oops!
}

或者

js
function* foo() {
  var x = yield 3;
  var y = x.toUpperCase(); // could be a TypeError error!
  yield y;
}

var it = foo();

it.next(); // { value:3, done:false }

try {
  it.next(42); // `42` won't have `toUpperCase()`
} catch (err) {
  console.log(err); // TypeError (from `toUpperCase()` call)
}

这个就相当于普通函数如果有异常,在外面其实也可以捕获:

js
function f1() {
  console.log(aaaa);
}
try {
  f1();
} catch (e) {
  console.log(e); // 'ReferenceError: a is not defined'
}

3. Delegating Generators

如何在 generator 内部去调用另外一个 generator, 通过 yield * 当代码遇到 yield*, 会去实例化 foo(), 所以实际上是 yielding/delegating 另外一个 generator。 当 *foo() 结束,控制权转回原来的 generator;

js
function* foo() {
  yield 3;
  yield 4;
}

function* bar() {
  yield 1;
  yield 2;
  yield* foo(); // `yield *` delegates iteration control to `foo()`
  yield 5;
}

for (var v of bar()) {
  console.log(v);
}
// 1 2 3 4 5

如果不用 for..of, 正常的使用 next(..), 看下下面的例子

js
function* foo() {
  var z = yield 3;
  var w = yield 4;
  console.log("z: " + z + ", w: " + w);
}

function* bar() {
  var x = yield 1;
  var y = yield 2;
  yield* foo(); // 相当于 var ffff = foo(); ffff.next();
  var v = yield 5;
  console.log("x: " + x + ", y: " + y + ", v: " + v);
}

var it = bar();

it.next(); // { value:1, done:false }
it.next("X"); // { value:2, done:false }
it.next("Y"); // { value:3, done:false }
it.next("Z"); // { value:4, done:false }
it.next("W"); // { value:5, done:false }
// z: Z, w: W

it.next("V"); // { value:undefined, done:true }
// x: X, y: Y, v: V

另外一个情况是 yield* 可以接收从委托的 generator 里的 return 的值, 注意并没有输出 { value:'foo', done:false }

js
function* foo() {
  yield 2;
  yield 3;
  return "foo"; // return value back to `yield*` expression
}

function* bar() {
  yield 1;
  var v = yield* foo();
  console.log("v: " + v);
  yield 4;
}

var it = bar();

it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // "v: foo"   { value:4, done:false }
it.next(); // { value:undefined, done:true }

但是如果只迭代有 return 值的 foo, 则可以迭代出 { value:'foo', done:false }如果没有 return "foo", 可以理解函数最后有一个 return undefined;

js
function* foo() {
  yield 1;
  yield 2;
  return "foo"; // return value back to `yield*` expression
}
var it = foo();
it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.next(); // { value:'foo', done:true }

委托如果遇到报错, 是怎么传递的, 看下面的例子。 foo 里面的报错在 bar 里接收了。 注意下面的代码, it.throw( "Uh oh!" ) 会跳到了下一个 yield;

js
function* foo() {
  try {
    yield 2;
  } catch (err) {
    console.log("foo caught: " + err);
  }

  yield; // pause

  // now, throw another error
  throw "Oops!";
}

function* bar() {
  yield 1;
  try {
    yield* foo();
  } catch (err) {
    console.log("bar caught: " + err);
  }
}
var it = bar();
it.next(); // {value: 1, done: false}
it.next(); // {value: 2, done: false}
it.throw("Uh oh!"); // foo caught: Uh oh!, {value: undefined, done: false}
it.next(); // bar caught: Oops! {value: undefined, done: true}

4.一些例子

js
function* bar() {
  yield 1;
  try {
    console.log(a);
    yield 2;
  } catch (err) {
    console.log("bar caught: " + err);
  }
}
var it = bar();
it.next(); // {value: 1, done: false}
/*
  到不了 yiled 2;
  bar caught: ReferenceError: a is not defined;
  {value: undefined, done: true};
*/
it.next(2);

5.总结

yield* 允许你委托迭代控制从当前的 generator 到另外一个 generator, yield* 扮演一个传递的角色, 可以同时传递信息和报错。

throw 的例子, 下面的 2 个例子是等价的 所以 it.throw('xxx') 的时候可以大概理解成 it.next(throw 'xxx');

例子 1:

js
function* bar() {
  yield 1;
  try {
    yield 2;
  } catch (err) {
    console.log("bar caught: " + err);
  }
}
var it = bar();
it.next(); // {value: 1, done: false}
it.next(); // {value: 2, done: false}
it.throw("aaa"); // bar caught: aaa; {value: undefined, done: false}

例子 2:

js
function* bar() {
  yield 1;
  try {
    yield 2;
    throw "aaa";
  } catch (err) {
    console.log("bar caught: " + err);
  }
}
var it = bar();
it.next(); // {value: 1, done: false}
it.next(); // {value: 2, done: false}
it.next(); // bar caught: aaa; {value: undefined, done: false}

6. Going Async With ES6 Generators

Simplest Async

通过 request 里面调用 it.next 来进行下一步。yeild 不用关心 request 里面是怎么拿最新的 data 的。 注意 makeAjaxCall 里的代码不能立即执行 callback, 否则有个错误, 因为 generator 从技术上来说还没有在停止的状态,因为在那个时刻 generator 还在运行当中,yeild 还没有处理。

js
var id = 1;
function makeAjaxCall(url, callback) {
  setTimeout(() => {
    callback(
      JSON.stringify({
        value: url,
        id: id++
      })
    );
  }, 1000);
}

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

function* main() {
  var result1 = yield request("http://some.url.1");
  var data = JSON.parse(result1);

  var result2 = yield request("http://some.url.2?id=" + data.id);
  var resp = JSON.parse(result2);
  console.log("The value you asked for: " + resp.value);
}

var it = main();
it.next();

例子 2 也就是说 yield 里面的函数不能立刻执行 it.next

js
function doNext() {
  it.next();
}
function* foo() {
  var res = yield doNext();
  console.log(res);
  res = yield 2;
  console.log(res);
  res = yield 3;
  console.log(res);
}
var it = foo();
it.next();

Better Async

目前异步的问题有 3 个: 1: 对于异常的处理不清晰。虽然我们能用 it.throw 来处理异常, 然后用 try..catch 来捕获异常, 但是如果我们做很多的 generators, 代码不太好用。

2: makeAjaxCall(..) 的问题是不受我们控制,如果 callback 执行多次,或者 成功和失败同时执行。那么我们的 generator 就会失控

3: 如何触发多个任务在一个 generator yeild 里面。

如何解决这个问题,通过 yeild out promise, 通过定义一个 runGenerator 方法。

js
function request(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(url);
    }, 2000);
  });
}

runGenerator(function* main() {
  var result1 = yield request("http://some.url.1");
  console.log("result1=" + result1);
  var result2 = yield request("http://some.url.2?id=");
  console.log("result2=" + result2);
  var r3 = yield 2;
  console.log(r3);
});

function runGenerator(ge) {
  let it = ge();
  const loop = (data) => {
    let p = it.next(data);
    if (p.done) {
      return;
    }
    if (p.value.then) {
      p.value.then(loop);
    } else {
      /*
  这里其实不用 setTimeout 也不会报错,
  但是为了跟上面的 promise.then 一样是异步的, 所以加了个 setTimeout
 */
      setTimeout(() => {
        loop(p.value);
      });
    }
  };
  loop();
}