Generator函数的语法和异步应用

2018-04-25  本文已影响0人  JarvanZ

同步应用

简介

基本概念

Generator函数式ES6提供的一种异步编程解决方案,语法行为和传统函数完全不同.
语法上,首先可以把它理解成,Generator函数是一个状态机,封装了多个内部状态
执行Generator函数会返回一个遍历器对象,也就是说,Generator函数除了状态机,还是一个遍历器对象生成函数.返回的遍历器对象,可以依次遍历Generator函数内部的每一个状态
形式上,Generator函数是一个普通函数,但是有两个特征.一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield(产出)表达式,定义不同的内部状态.

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

调用Generator函数,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象.
下一步必须调用遍历器对象的next方法,使得指针移向下一个状态.换言之,Generator函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行.
每次调用遍历器对象的next方法,就会返回一个有着valuedone两个属性的对象.

yield表达式

yield表达式就是暂停标志
遍历器对象的next方法运行逻辑:

  1. 遇到yield表达式,就暂停执行后面的操作,并将紧跟着yield后面的那个表达式的值作为返回的对象的value属性值
  2. 下一次调用next方法时,再继续向下执行,知道遇到yield表达式
  3. 如果没有遇到新的yield表达式,就一直运行到函数结束,知道return语句为止.并将return语法后面的表达式的值,作为返回的对象的value
  4. 如果该函数没有return语句 ,则返回的对象的value属性值为undefined
    需要注意的是,yield表达式后面的表达式,只有调用next方法,内部指针指向该语句时才会执行,因此等于为JavaScript提供了手动的"惰性求职"的语法功能
    Generator函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数
    需要注意,yield表达式只能用在Generator函数里面.另外,yield表达式如果用在另一个表达式之中,必须放在圆括号里面.
    yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号.
与Iterator接口的关系

任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象
由于Generator函数就是遍历器生成函数,因此可以把Generator赋值给对象的Symbol.iterator属性,从而使得该对象具有Iterator接口.
Generator函数执行后,返回一个遍历器对象,该对象本身也具有Symbol.iterator属性,执行后返回自身

next方法的参数

yield表达式本身没有返回值,或者说总是返回undefined.next方法可以带一个参数,该参数就会被当做上一个yield表达式的返回值.
这个功能有很重要的语法意义.Generator函数从暂停状态到恢复运行,它的上下文状态是不变的.通过next方法的参数,就有办法在Generator函数开始运行之后,继续向函数内部注入值.也就是说,可以再Generator函数运行的不同阶段,从外部想内部注入不同的值,从而调整函数行为.

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

var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}

var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }

注意:由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的.只有从第二次使用next方法开始,参数才是有效的
如果想要在第一次调用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';
});

wrapped().next('hello!')
// First input: hello!

for...of循环

可以自动遍历Generator函数时生成的Iterator对象,且此时不再需要调用next方法.
这里需要注意:一旦next方法的返回对象的done属性,for...of循环就会中止,且不包含该返回对象.
利用Generator函数和for...of循环,实现斐波那契数列:

function* fibonacci() {
  let [prev, curr] = [0, 1];
  for (;;) {
    [prev, curr] = [curr, prev + curr];
    yield curr;
  }
}

for (let n of fibonacci()) {
  if (n > 1000) break;
  console.log(n);
}

利用for...of循环,可以写出遍历任意对象的方法.通过Generator函数为它加上遍历器接口

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

加上遍历器接口的另一种写法是,将Generator函数加到对象的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

除了for...of循环以外,拓展运算符(...)解构赋值和Array.from方法内部调用的,都是遍历器接口.他们都可以讲Generator函数返回的Iterator对象作为参数

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

第一个错误被Generator函数体内的catch语句捕获.第二次抛出错误,由于Generator函数内部的catch语句已经被执行过了,不会再捕捉到这个错误了,所以被函数体外的catch语句捕获.
throw方法可以接受一个参数,该参数会被catch语句接受,建议抛出Error对象的实例.
注意区分遍历器对象的throw方法和全局的throw命令.

var g = function* () {
  while (true) {
    try {
      yield;
    } catch (e) {
      if (e != 'a') throw e;
      console.log('内部捕获', e);
    }
  }
};

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

try {
  throw new Error('a');
  throw new Error('b');
} catch (e) {
  console.log('外部捕获', e);
}
// 外部捕获 [Error: a]

上面代码之所以只捕获了a,是因为函数体外的catch语句块捕获了抛出的a错误以后,就不会再继续try代码块里面剩余的语句了.
如果Generator函数内部没有部署try...catch代码块,那么throw方法抛出的错误,将被外部try...catch代码块捕获.
如果内外部都没有部署try...catch代码块,那么程序将报错中断执行.
throw方法被捕获以后,会附带执行下一条yield表达式,也即是说,会附带执行一次next方法.
只要Generator函数内部部署了try...catch代码块,那么遍历器的throw方法抛出的错误,不影响下一次遍历.
另外,throw命令和遍历器中的throw方法是无关的,两者互不影响.
Generator函数体外抛出的错误可以在函数体内捕获;反过来,Generator函数体内抛出的错误,也可以被函数体外的catch捕获

function* foo() {
  var x = yield 3;
  var y = x.toUpperCase();
  yield y;
}

var it = foo();

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

try {
  it.next(42);
} catch (err) {
  console.log(err);
  //会有报错信息,数值没有toUpperCase方法
}

一旦Generator执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了.如果此后还调用next方法,将返回一个value属性等于undefined,done属性等于true的对象.

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方法调用时,不提供参数,则返回值的value属性为undefined.
如果Generator函数内部有try...finally代码块,那么return方法会推迟到finally代码块执行完再执行

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 }

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;

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;

yield*表达式

如果在Generator函数内部,调用另一个Generator函数,默认情况下是没有效果的.这里就需要用到yield*表达式,用来在一个Generator函数里面执行另一个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"

yield*后面的Generator函数(没有return语句时),等同于在Generator函数内部,部署一个for...of循环
如果yield*后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员

function* gen(){
  yield* ["a", "b", "c"];
}

gen().next() // { value:"a", done:false }

上面代码中,如果yield命令后面如果不加星号,返回的是整个数组,加了星号就表示返回的是数组的遍历器对象.
如果被代理的Generator函数有return语句,那么就可以向代理它的Generator函数返回数据.
yield*命令可以很方便的取出嵌套数组的所有成员

function* iterTree(tree) {
  if (Array.isArray(tree)) {
    for(let i=0; i < tree.length; i++) {
      yield* iterTree(tree[i]);
    }
  } else {
    yield tree;
  }
}

const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];

for(let x of iterTree(tree)) {
  console.log(x);
}
// a
// b
// c
// d
// e

作为对象属性的Generator函数

//简写形式
let obj = {
  * myGeneratorMethod() {
    ···
  }
};
//等同于
let obj = {
  myGeneratorMethod: function* () {
    // ···
  }
};

Generator函数的this

Generator函数总数返回一个遍历器,ES6规定这个遍历器是Generator函数的实例,也继承了Generator函数的prototype对象上的方法.
Generator函数内部在this对象上添加一个属性,但是返回的遍历器对象拿不到这个属性.Generator函数也不能跟new命令一起用,会报错.
让Generator函数返回一个正常的对象实例,既可以用next方法,又可以获得正常的this的方法:
首先,生成一个空对象,使用call方法绑定Generator函数内部的this.这样,构造函数调用以后,这个空对象就是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

上面代码中,执行的是遍历器对象f,但是生成的对象实例是obj,将这个两个对象统一的办法:
将obj换成F.prototype

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

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

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

再将F改成构造函数,就可以对它执行new命令了

function* gen() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}

function F() {
  return gen.call(gen.prototype);
}

var f = new F();

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

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

含义

Generator和状态机
//ES5
var ticking = true;
var clock = function() {
  if (ticking)
    console.log('Tick!');
  else
    console.log('Tock!');
  ticking = !ticking;
}
//ES6
var clock = function* () {
  while (true) {
    console.log('Tick!');
    yield;
    console.log('Tock!');
    yield;
  }
};

上面的Generator实现与ES5实现对比,可以看到少了用来保存状态的外部变量,更符合函数式编程的思想.

Generator与协程

协程是一种程序运行的方式,可以理解为"协作的线程"或"协作的函数".协程既可以用单线程实现,也可以用多线程实现.前者是一种特殊的子例程,后者是一种特殊的线程.

  1. 协程与子例程的差异
    可以并行执行,交换执行权的线程(或函数),就称为协程.
    从实现上看,在内存中,子例程只使用一个栈,而协程是同事存在多个栈,但只有一个栈是在运行状态,也就是说,协程是以多占用内存为代价,实现多任务的并行.
  2. 协程与普通线程的差异
    普通的线程是抢先式的,到底哪个线程优先得到资源,必须有运行环境决定,但是协程是合作式的,执行权有协程自己分配.
    Generator函数是ES6对于协程的实现,但属于不完全实现.因为只有Generator函数的调用者才能将程序的执行权还给Generator函数.如果是完全执行的协程,任何函数都可以让暂停的协程继续之心.
Generator与上下文

Javascript执行函数(或块级代码)的时候,会在当前上下文环境的上层产生一个函数运行的上下文,变成当前的上下文,由此形成一个上下文环境的堆栈.这个堆栈是"后进先出"的数据结构,最后产生的上下文环境首先执行完成,退出堆栈,然后再执行完成它下层的上下文,直至所有代码执行完成,堆栈清空
Generator函数不是这样,它执行产生的上下文环境,一旦遇到yield命令,就会暂时退出堆栈,但是并不小时,里面的所有变量和对象会冻结在当前状态.等到对它执行next命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行.

应用

  1. 异步操作的同步化表达
    Ajax是典型的异步操作,通过Generator函数部署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);
    //回调成功之后再调用next方法,并传入参数赋值给result
  });
}

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

通过Generator函数逐行读取文本文件

function* numbers() {
  let file = new FileReader("numbers.txt");
  try {
    while(!file.eof) {
      yield parseInt(file.readLine(), 10);
    }
  } finally {
    file.close();
  }
}
控制流管理
step1(function (value1) {
  step2(value1, function(value2) {
    step3(value2, function(value3) {
      step4(value3, function(value4) {
        // Do something with value4
      });
    });
  });
});

Promise改写

Promise.resolve(step1)
  .then(step2)
  .then(step3)
  .then(step4)
  .then(function (value4) {
    // Do something with value4
  }, function (error) {
    // Handle any error from step1 through step4
  })
  .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);
  }
}

这种做法只能用于所有步骤都是同步操作的情况,不能有异步操作的步骤.
for...of的本质是一个while循环.

部署Iterator接口

利用Generator函数可以在任意对象上部署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
作为数据结构

可以看做一个数组结构,因为Generator函数可以返回一系列的值,这意味着它可以对任意表达式,提供类似数组的接口.

function* doStuff() {
  yield fs.readFile.bind(null, 'hello.txt');
  yield fs.readFile.bind(null, 'world.txt');
  yield fs.readFile.bind(null, 'and-such.txt');
}

for (task of doStuff()) {
  // task是一个函数,可以像回调函数那样使用它
}

异步应用

传统方法

ES6诞生以前,异步编程方法大概一下四种:

基本概念

Promise
fs.readFile(fileA, 'utf-8', function (err, data) {
  fs.readFile(fileB, 'utf-8', function (err, data) {
    // ...
  });
});

如果依次读取两个以上的文件,就会出现多重嵌套.因为多个异步操作形成了抢耦合,只要有一个操作需要修改,她的上层回调函数和下层回调函数可能都要跟着修改.这种情况成为"回调函数地狱".
Promise就是为了解决这个问题而提出的,它是一种新的写法,允许将回调函数的嵌套,改为链式调用.采用Promise,连续读取多个文件:

var readFile = require('fs-readfile-promise');

readFile(fileA)
.then(function (data) {
  console.log(data.toString());
})
.then(function () {
  return readFile(fileB);
})
.then(function (data) {
  console.log(data.toString());
})
.catch(function (err) {
  console.log(err);
});

Promise最大问题是代码冗余,原来的任务被Promise包装一下,不管什么操作都是一堆then,原来的语义变得不清楚

Generator函数

异步任务的封装

var fetch = require('node-fetch');

function* gen(){
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(result.bio);
}

上面代码中,Generator函数封装了一个异步操作,该操作先读取一个远程接口,然后从JSON格式的数据解析信息

//执行这段代码
var g = gen();
var result = g.next();

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

上面代码中,首先执行Generator函数,获取遍历器对象,然后使用next方法,执行异步任务的第一阶段.由于fetch模块返回的是一个Promise对象,因此要用then方法调用下一个next方法.
可以看到,虽然Generator函数将异步操作表示的很简介,但是流程管理却不方便.

Thunk函数

Thunk函数是自动执行Generator函数的一种方法

参数的求值策略
  1. "传值调用"
    在进入函数体之前,就计算参数表达式的值
  2. "传名调用"
    直接将表达式传入函数体,只有在用到的时候求值
Thunk函数的含义

编译器的"传名调用"实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体.这个临时函数就叫做Thunk函数.
这就是Thunk函数的定义,它是"传名调用"的一种实现策略,用来替换某个表达式.

JavaScript语言的Thunk函数

JavaScript语言是传值调用的e,它的Thunk函数含义有所不同.在JavaScript语言中,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版本
const Thunk = function(fn) {
  return function (...args) {
    return function (callback) {
      return fn.call(this, ...args, callback);
    }
  };
};
Thunkify模块

生产环境的转换器,建议使用Thunkify模块
使用方法:

var thunkify = require('thunkify');
var fs = require('fs');

var read = thunkify(fs.readFile);
read('package.json')(function(err, str){
  // ...
});

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确保回调函数只运行一次.这样的设计与下文的Generator函数相关.下面例子:

function f(a, b, callback){
  var sum = a + b;
  callback(sum);
  callback(sum);
}

var ft = thunkify(f);
var print = console.log.bind(console);
ft(1, 2)(print);
// 3

由于thunkify只允许回调函数执行一次,所以只输出一行结果.

Generator函数的流程管理

Generator函数可以自动执行

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

var g = gen();
var res = g.next();

while(!res.done){
  console.log(res.value);
  res = g.next();
}

但是,这不适合异步操作.以读取文件为例,下面的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());
};

上面代码中,yield命令用于将程序的执行权移出Generator函数,那么就需要一种方法将执行权再交还给Generator函数
这种方法就是Thunk函数,因为它可以在回调函数里,将执行权交还给Generator函数.

var g = gen();

var r1 = g.next();
//这里r1.value相当于readFileThunk('/etc/fstab'),此时该方法还不会被执行
//readFileThunk('/etc/fstab')(callback)
r1.value(function (err, data) {
  if (err) throw err;
  var r2 = g.next(data);
  //这里的r2.value已经被赋值为Thunk函数名
  r2.value(function (err, data) {
    if (err) throw err;
    g.next(data);
  });
});

上面代码中,变量g是Generator函数的内部指针,表示目前执行到哪一步.next方法负责将指针移动到下一步,并返回该步的信息(value和done).
可以知道Generator函数的执行结果,其实是将同一个回调函数,反复传入next方法的value属性.这使得我们可以用递归来自动完成这个过程.

Thunk函数的自动流程管理

Thunk函数真正的威力,在于可以自动执行Generator函数.下面就是一个基于Thunk函数的Generator执行器.

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

  function next(err, data) {
    var result = gen.next(data);
    //此时result.value已经被赋值为上一个Thunk函数名,next是一个回调函数
    if (result.done) return;
    result.value(next);
    //当上一个Thunk函数执行完成之后才会调用回调函数,再继续执行下一个Thunk函数
  }

  next();
  //next方法从来都不会被传入参数
}

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

run(g);

run函数就是一个Generator函数的自动执行器.内部的next函数就是Thunk的回调函数.next函数先将指针移到Generator函数的下一步(gen.next方法),然后判断Generator函数是否结束(result.done属性),如果没结束,就将next函数再传入Thunk函数(result.value
属性),否则就直接退出
使用该执行器的前提是每一个异步操作,都要是Thunk函数,也就是说,跟在yield命令后面的必须是Thunk函数.
Thunk函数并不是Generator函数自动执行的唯一方案.因为自动执行的关键是,必须有一种机制,自动控制Generator函数的流程,接收和交换程序的执行权.回调函数可以做到这一点,Promise对象也可以做到这一点.

上一篇下一篇

猜你喜欢

热点阅读