美文网首页
Generator函数的语法和异步应用

Generator函数的语法和异步应用

作者: JarvanZ | 来源:发表于2018-04-25 20:54 被阅读0次

    同步应用

    简介

    基本概念

    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对象

    基本概念

    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对象也可以做到这一点.

    相关文章

      网友评论

          本文标题:Generator函数的语法和异步应用

          本文链接:https://www.haomeiwen.com/subject/ewtylftx.html