阮一峰es6要点总结——Generator

笔记,总结摘录自阮一峰
笔记中有不少自己看书的总结

基本概念

核心目的:异步编程解决方案

关键概念:状态机,执行权限的传递,数据的传入传出

  • 写法
  • function关键字和函数名之间有一个星号
  • 函数内部,使用yield语句,定义内部状态
  • 作为对象属性,可以简写
let obj = {
  * myGeneratorMethod() {
    ···
  }
};
function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

调用Generator函数,函数并不执行,而是返回一个遍历器对象。必须调用遍历器对象的next方法,将函数执行到下一个yield语句的地方。

当遇到yield语句,就暂停执行,并将yield后边的表达式的值,作为返回对象的value属性值。

yield语句如果用在一个表达式之中,必须放在圆括号里面

yield语句用作函数参数或放在赋值表达式的右边,可以不加括号

return语句,将返回表达式后边的值,并结束函数。

  • 传入与传出

通过yield后的表达式,可以实现传出

通过next(value)方法中,传入参数,可以实现传入。value将作为上一个yield语句的返回值。

我们都知道,一个运算符,都有返回值,比如+号,比如=号,比如,号。正常情况下,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 }

所以,我们是可以对函数的行为进行控制的。这就非常方便了,比如,函数执行权的交替。

碎知识

  • for...of配合
    比如,实现对象的遍历
function* objectEntries(obj) {
  let propKeys = Reflect.ownKeys(obj);

  for (let propKey of propKeys) {
    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

也可以直接加到对象的Symbol.iterator

function* objectEntries() {
  let propKeys = Object.keys(this);

  for (let propKey of propKeys) {
    yield [propKey, this[propKey]];
  }
}

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

jane[Symbol.iterator] = objectEntries;

for (let [key, value] of jane) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
  • Generator.prototype.throw()
    Generator函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在Generator函数体内捕获。
var g = function* () {
  try {
    yield;
  } catch (e) {
    console.log('内部捕获', e);
  }
};

var i = g();
i.next();

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

throw方法可以接受一个参数,该参数会被catch语句接收,建议抛出Error对象的实例。

不管内外,必须有try...catch语句来捕获错误,不然程序将报错。

throw方法被捕获以后,会附带执行到下一条yield语句。(他就是它本身带了一个next方法)

一旦Generator执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用next方法,将返回一个value属性等于undefineddone属性等于true的对象,即JavaScript引擎认为这个Generator已经运行结束了。

function* g() {
  yield 1;
  console.log('throwing an exception');
  throw new Error('generator broke!');
  yield 2;
  yield 3;
}

function log(generator) {
  var v;
  console.log('starting generator');
  try {
    v = generator.next();
    console.log('第一次运行next方法', v);
  } catch (err) {
    console.log('捕捉错误', v);
  }
  try {
    v = generator.next();
    console.log('第二次运行next方法', v);
  } catch (err) {
    console.log('捕捉错误', v);
  }
  try {
    v = generator.next();
    console.log('第三次运行next方法', v);
  } catch (err) {
    console.log('捕捉错误', v);
  }
  console.log('caller done');
}

log(g());
// starting generator
// 第一次运行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉错误 { value: 1, done: false }
// 第三次运行next方法 { value: undefined, done: true }
// caller done
  • Generator.prototype.return()

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

如果return(value)方法传值了,那就返回传入的那个值,如果没有传值,就返回undefined

如果Generator函数内部有try...finally代码块,那么当遇到return时候,不会立刻结束,而是会把finally代码块中的执行完,然后再return

function* numbers () {
  yield 1;
  try {
    yield 2;
    yield 3;
  } finally {
    yield 4;
    yield 5;
  }
  yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }
  • yield*
    在Generator函数调用另一个Generator函数时候,要用yield*

等价于,在yield*位置,展开另一个Generator函数的状态。

function* bar() {
  yield 'x';
  yield* foo();
  yield 'y';
}

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

// 等同于
function* bar() {
  yield 'x';
  for (let v of foo()) {
    yield v;
  }
  yield 'y';
}

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

任何数据结构只要有Iterator接口,就可以被yield*遍历

如果被代理的Generator函数有return语句,那么就可以向代理它的Generator函数返回数据。

function *foo() {
  yield 2;
  yield 3;
  return "foo";
}

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}

yield*命令可以很方便地取出嵌套数组的所有成员。

// 下面是二叉树的构造函数,
// 三个参数分别是左树、当前节点和右树
function Tree(left, label, right) {
  this.left = left;
  this.label = label;
  this.right = right;
}

// 下面是中序(inorder)遍历函数。
// 由于返回的是一个遍历器,所以要用generator函数。
// 函数体内采用递归算法,所以左树和右树要用yield*遍历
function* inorder(t) {
  if (t) {
    yield* inorder(t.left);
    yield t.label;
    yield* inorder(t.right);
  }
}

// 下面生成二叉树
function make(array) {
  // 判断是否为叶节点
  if (array.length == 1) return new Tree(null, array[0], null);
  return new Tree(make(array[0]), array[1], make(array[2]));
}
let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]);

// 遍历二叉树
var result = [];
for (let node of inorder(tree)) {
  result.push(node);
}

result
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']
  • Generator函数中的this

Generator函数,不能跟new操作符一起使用,会报错

把Generator函数,当成构造函数,会失效,因为它总是返回遍历器对象,而不是那个新创建的对象。

一个方法解决

function* F() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
var obj = {};
var f = F.call(obj);

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

obj.a // 1
obj.b // 2
obj.c // 3

Generator 与 协程

协程(coroutine)是一种程序运行的方式,可以理解成“协作的线程”或“协作的函数”。

协程既可以用单线程实现,是一种特殊的子例程。
也可以用多线程实现,是一种特殊的线程。

  • 协程与子例程的差异

传统的“子例程”(subroutine)采用堆栈式“后进先出”的执行方式,只有当调用的子函数完全执行完毕,才会结束执行父函数。协程与其不同,多个线程(单线程情况下,即多个函数)可以并行执行,但是只有一个线程(或函数)处于正在运行的状态,其他线程(或函数)都处于暂停态(suspended),线程(或函数)之间可以交换执行权。也就是说,一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权的时候,再恢复执行。这种可以并行执行、交换执行权的线程(或函数),就称为协程。

从实现上看,在内存中,子例程只使用一个栈(stack),而协程是同时存在多个栈,但只有一个栈是在运行状态,也就是说,协程是以多占用内存为代价,实现多任务的并行。

  • 协程与普通线程的差异

不难看出,协程适合用于多任务运行的环境。在这个意义上,它与普通的线程很相似,都有自己的执行上下文、可以分享全局变量。它们的不同之处在于,同一时间可以有多个线程处于运行状态,但是运行的协程只能有一个,其他协程都处于暂停状态。此外,普通的线程是抢先式的,到底哪个线程优先得到资源,必须由运行环境决定,但是协程是合作式的,执行权由协程自己分配。

由于ECMAScript是单线程语言,只能保持一个调用栈。引入协程以后,每个任务可以保持自己的调用栈。这样做的最大好处,就是抛出错误的时候,可以找到原始的调用栈。不至于像异步操作的回调函数那样,一旦出错,原始的调用栈早就结束。

Generator函数是ECMAScript 6对协程的实现,但属于不完全实现。Generator函数被称为“半协程”(semi-coroutine),意思是只有Generator函数的调用者,才能将程序的执行权还给Generator函数。如果是完全执行的协程,任何函数都可以让暂停的协程继续执行。

如果将Generator函数当作协程,完全可以将多个需要互相协作的任务写成Generator函数,它们之间使用yield语句交换控制权。

应用

改写异步操作——同步化表达

例子一:Ajax操作

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

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

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

注意request方法里边,请求到后,调用next方法,要传参数进去。

例子二:逐行读取文本

function* numbers() {
  let file = new FileReader("numbers.txt");
  try {
    while(!file.eof) {
      yield parseInt(file.readLine(), 10);
    }
  } finally {
    file.close();
  }
}

控制流管理

例子一:同步方法的流管理

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);
  }
}

例子二:利用for...of

let steps = [step1Func, step2Func, step3Func];

function *iterateSteps(steps){
  for (var i=0; i< steps.length; i++){
    var step = steps[i];
    yield step();
  }
}

以上将任务,分成多个步骤

let jobs = [job1, job2, job3];

function *iterateJobs(jobs){
  for (var i=0; i< jobs.length; i++){
    var job = jobs[i];
    yield *iterateSteps(job.steps);
  }
}

又将项目,分成多个任务。

最后,用for...of循环,一次性执行所有任务的所有步骤。

for (var step of iterateJobs(jobs)){
  console.log(step.id);
}

部署Iterator接口

function* iterEntries(obj) {
  let keys = Object.keys(obj);
  for (let i=0; i < keys.length; i++) {
    let key = keys[i];
    yield [key, obj[key]];
  }
}

let myObj = { foo: 3, bar: 7 };

for (let [key, value] of iterEntries(myObj)) {
  console.log(key, value);
}

// foo 3
// bar 7

异步应用

异步编程的方法

  • 回调函数
  • 事件监听
  • 发布/订阅模式
  • Promise 对象

ES6,有了Generator

几种方法的分析

回调函数,在写法上,会出现多重嵌套的问题(回调地狱)

Promise解决了这个问题,但是引入了大量的Promise语法。总体来说,没有新意。

Generator函数是协程的ES6实现,通过执行权的交替,实现了异步编程。

Generator的自动流程管理

  • 回到函数,封装Thunk函数
  • Promise

Thunk 函数

参数的求职策略

即,函数的参数到底何时求值?

  • 传值调用
    在进入函数体之前。c用的传值调用
  • 传名调用
    在真正用到时候,再求值。

Thunk函数,在传名调用实现中,将参数放入一个临时函数之中,再将这个临时函数,传入函数体。这个临时函数,就是Thunk函数。

function f(m) {
  return m * 2;
}

f(x + 5);

// 等同于

var thunk = function () {
  return x + 5;
};

function f(thunk) {
  return thunk() * 2;
}

JavaScript的Thunk函数

js是传值调用,它的Thunk函数,替换的是多参数函数,将其替换成,只接受回调函数作为参数的单参数函数。

// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);

// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
  return function (callback) {
    return fs.readFile(fileName, callback);
  };
};

var readFileThunk = Thunk(fileName);
readFileThunk(callback);

也就是说,我们通过thunk将回到函数,普通参数分离开来传入。这为我们交替执行权做准备。

下边是简单的Thunk函数转换器

// ES5版本
var Thunk = function(fn){
  return function (){
    var args = Array.prototype.slice.call(arguments);
    return function (callback){
      args.push(callback);
      return fn.apply(this, args);
    }
  };
};

// ES6版本
var Thunk = function(fn) {
  return function (...args) {
    return function (callback) {
      return fn.call(this, ...args, callback);
    }
  };
};

使用方法

var readFileThunk = Thunk(fs.readFile);
readFileThunk(fileA)(callback);

生产环境的Thunk

建议使用Thunkify模块
它的源码,与上边的很像

function thunkify(fn) {
  return function() {
    var args = new Array(arguments.length);
    var ctx = this;

    for (var i = 0; i < args.length; ++i) {
      args[i] = arguments[i];
    }

    return function (done) {
      var called;

      args.push(function () {
        if (called) return;
        called = true;
        done.apply(null, arguments);
      });

      try {
        fn.apply(ctx, args);
      } catch (err) {
        done(err);
      }
    }
  }
};

它的源码主要多了一个检查机制,变量called确保回调函数只运行一次。

重点:利用Thunk的 Generator函数自动流程管理

由于多个异步操作,需要保证前一步执行完,再执行后一步。可以这样

var fs = require('fs');
var thunkify = require('thunkify');
var readFileThunk = thunkify(fs.readFile);

var gen = function* (){
  var r1 = yield readFileThunk('/etc/fstab');
  console.log(r1.toString());
  var r2 = yield readFileThunk('/etc/shells');
  console.log(r2.toString());
};

两个异步操作,在执行他们时候,把执行权交给了另一个函数,然后,在另一个函数(readFileThunk)中,当异步操作完成,我们再把执行权返还给gen,这样就保证了前一步完成,再执行下一步。

现在我们有Thunk函数,有了Generator函数,关键就是,自动执行的函数怎么写。

我们的目的是,在Thunk的回调函数与Generator之间进行切换

function run(fn) {
  var gen = fn();

  function next(err, data) {
    var result = gen.next(data);
    if (result.done) return;
    result.value(next);
    // 注意这一步,result.value是gen函数中的readFileThunk,
    // 它是一个Thunk函数,也就是说,这一步等价于readFileThunk(file1)(next)
  }

  next();
}

function* g() {
  // ...
}

run(g);

上边的函数中:
第一步——next就是Thunk函数的回调函数,首先将执行权还给gen函数
第二步——gen 函数执行到yield语句,将执行权交给next函数,并把需要的异步操作一并传给next函数。
第三步——run函数收到返回的对象,判断流程是否结束,如果没有结束,直接调用传回的异步操作,即result.value(next),等价于readFileThunk(file1)(next)
第四步——异步操作完成,将会调用回到函数next,并将请求的数据传入next函数。此时,next函数重复第一步,并将数据传回gen函数。

如此往复循环,知道结束

也就是说,异步操作的回调函数,用来控制流程,并且将数据原封不动的传回给gen函数。真正对请求数据的操作不是在回调函数里做的,而是在gen函数里。

co模块——基于Promise对象的Generator函数自动流程管理

TJ Holowaychuk发布的小工具,就是上边的run函数,只不过用的是Promise

一个例子:

var fs = require('fs');

var readFile = function (fileName){
  return new Promise(function (resolve, reject){
    fs.readFile(fileName, function(error, data){
      if (error) return reject(error);
      resolve(data);
    });
  });
};

var gen = function* (){
  var f1 = yield readFile('/etc/fstab');
  var f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

首先将异步操作封装成Promise(上一部分是封装成Thunk)。

如果手动执行:

var g = gen();

g.next().value.then(function(data){
  g.next(data).value.then(function(data){
    g.next(data);
  });
});

其实就是一堆then,,,

ok,接下来写自动执行器

function run(gen){
  var g = gen();

  function next(data){
    var result = g.next(data);
    if (result.done) return result.value;
    result.value.then(function(data){
      next(data);
    });
  }

  next();
}

run(gen);

跟上一部分没什么差别。用Promise编程而已

来看看co的源码

最外层,co接受一个Generator,返回一个Promise

function co(gen) {
  var ctx = this;

  return new Promise(function(resolve, reject) {
  });
}

在返回的Promise对象中,首先检查gen是否为Generator,如果是,就执行,如果不是,就返回,并将Promise的状态resolved

function co(gen) {
  var ctx = this;

  return new Promise(function(resolve, reject) {
    if (typeof gen === 'function') gen = gen.call(ctx);
    if (!gen || typeof gen.next !== 'function') return resolve(gen);
  });
}

co将next方法抽象为两部分,1-执行权交给gen,2-调用自身。在第一部分加入了出错处理,使得方法更健壮。

function co(gen) {
  var ctx = this;

  return new Promise(function(resolve, reject) {
    if (typeof gen === 'function') gen = gen.call(ctx);
    if (!gen || typeof gen.next !== 'function') return resolve(gen);

    onFulfilled();
    function onFulfilled(res) {
      var ret;
      try {
        ret = gen.next(res);
      } catch (e) {
        return reject(e);
      }
      next(ret);// 关键部分
    }
  });
}

最关键的是新的next函数

function next(ret) {
  if (ret.done) return resolve(ret.value);
  var value = toPromise.call(ctx, ret.value);
  if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
  return onRejected(
    new TypeError(
      'You may only yield a function, promise, generator, array, or object, '
      + 'but the following object was passed: "'
      + String(ret.value)
      + '"'
    )
  );
}

如果gen结束了,那执行器也结束
第二行,保证gen传过来的每一步,都是Promise
第三行,如果确实是Promise,就执行异步操作,并设置onFulfilled为成功的回调函数。
第四行,如果参数不符合要求,终止执行,并将返回的Promise状态改为rejected。

co支持并发的异步操作
并发,即,某些操作同时进行,等到他们全部完成,才执行下一步

将并发的操作放在数组/对象里边,跟在yield语句后边即可

// 数组的写法
co(function* () {
  var res = yield [
    Promise.resolve(1),
    Promise.resolve(2)
  ];
  console.log(res);
}).catch(onerror);

// 对象的写法
co(function* () {
  var res = yield {
    1: Promise.resolve(1),
    2: Promise.resolve(2),
  };
  console.log(res);
}).catch(onerror);

另一个例子

co(function* () {
  var values = [n1, n2, n3];
  yield values.map(somethingAsync);
});

function* somethingAsync(x) {
  // do something async
  return y
}

处理Stream

Stream,就是将整个数据,一块块挨着处理。

Stream模式使用EventEmitter API,会释放三个事件

  • data事件:下一块数据准备好了
  • end事件:整个数据流处理完了
  • error事件:发生错误

利用Promise.race()函数,可以判断只有当data事件发生,才进入下一个数据块的处理。

const co = require('co');
const fs = require('fs');

const stream = fs.createReadStream('./les_miserables.txt');
let valjeanCount = 0;

co(function*() {
  while(true) {
    const res = yield Promise.race([
      new Promise(resolve => stream.once('data', resolve)),
      new Promise(resolve => stream.once('end', resolve)),
      new Promise((resolve, reject) => stream.once('error', reject))
    ]);
    if (!res) {
      break;
    }
    stream.removeAllListeners('data');
    stream.removeAllListeners('end');
    stream.removeAllListeners('error');
    valjeanCount += (res.toString().match(/valjean/ig) || []).length;
  }
  console.log('count:', valjeanCount); // count: 1120
});

推荐阅读更多精彩内容

  • 异步编程对JavaScript语言太重要。Javascript语言的执行环境是“单线程”的,如果没有异步编程,根本...
    呼呼哥阅读 5,707评论 5 22
  • 简介 基本概念 Generator函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同。本章详细介绍...
    呼呼哥阅读 760评论 0 4
  • 在此处先列下本篇文章的主要内容 简介 next方法的参数 for...of循环 Generator.prototy...
    醉生夢死阅读 1,064评论 1 8
  • 本文为阮一峰大神的《ECMAScript 6 入门》的个人版提纯! babel babel负责将JS高级语法转义,...
    Devildi已被占用阅读 1,444评论 0 4
  • 特别说明,为便于查阅,文章转自https://github.com/getify/You-Dont-Know-JS...
    杀破狼real阅读 206评论 0 0