美文网首页
ES6标准入门 摘要 (Generator)

ES6标准入门 摘要 (Generator)

作者: Upcccz | 来源:发表于2019-10-16 20:52 被阅读0次

    基本概念

    Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。

    Generator 函数是一个状态机,封装了多个内部状态。

    执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

    形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态。

    function* helloWorldGenerator() {
      yield 'hello';
      yield 'world';
      return 'ending';
    }
    
    var hw = helloWorldGenerator()
    // 该函数有三个状态:hello,world 和 return 语句(结束执行)。
    // 返回的hw 也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象。
    
    hw.next() // { value: 'hello', done: false }
    hw.next() // { value: 'world', done: false }
    hw.next() // { value: 'ending', done: true }
    hw.next() // { value: undefined, done: true }
    

    每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

    遍历器对象的next方法的运行逻辑如下。

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

    yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

    function* gen() {
      yield  123 + 456;
    }
    // 表达式123 + 456,不会立即求值
    // 只会在next方法将指针移到这一句时,才会求值。
    

    yield表达式与return语句共同点

    • 都能返回紧跟在语句后面的那个表达式的值。

    yield表达式与return语句区别

    • 区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。
    • 一个函数里面,只能执行一次(或者说一个)return语句,但是可以执行多次(或者说多个)yield表达式。正常函数只能返回一个值,因为只能执行一次return;

    Generator 函数可以返回一系列的值,因为可以有任意多个yield。从另一个角度看,也可以说 Generator 生成了一系列的值,这也就是它的名称的来历(生成器)。

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

    function* f() {
      console.log('执行了!')
    }
    
    var generator = f(); // console不会执行
    
    setTimeout(function () {
      generator.next() // 在调用next的时候才会执行
    }, 2000);
    

    函数f如果是普通函数,在为变量generator赋值时就会执行。但是,函数f是一个 Generator 函数,就变成只有调用next方法时,函数f才会执行。

    var arr = [1, [[2, 3], 4], [5, 6]];
    
    var flat = function* (a) {
      var length = a.length;
      for (var i = 0; i < length; i++) {
        var item = a[i];
        if (typeof item !== 'number') {
          yield* flat(item); // 会等到递归完成后再往下执行。
        } else {
          yield item;
        }
      }
    };
    
    for (var f of flat(arr)) {
      console.log(f);
    }
    // 1, 2, 3, 4, 5, 6
    

    yield表达式如果用在另一个表达式之中,必须放在圆括号里面。用作函数参数或放在赋值表达式的右边,可以不加括号。

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

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

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

    function* gen(){
      // some code
    }
    var g = gen();
    
    g[Symbol.iterator]() === g
    // true
    

    next方法的参数

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

    function* f() {
      for(var i = 0; true; i++) {
        var reset = yield i;
        // 第一次调用之后,之后的调用相当于就是从这里开始执行
        // 当第三次调用传参true的时候,被当作上一个yield表达式的返回值。
        // 就相当于 yield 1 是 true
        // 即 reset是 true ,就继续往下执行,i= -1最后++为0
        // 然后在遇到yield 就是 yield 0
        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 }
    

    这个功能有很重要的语法意义。Generator 函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过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 } 这次调用之后y变成了24
    b.next(13) // { value:42, done:true } 这次调用之后z变成了13
    

    由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。

    再看一个通过next方法的参数,向 Generator 函数内部输入值的例子。

    function* dataConsumer() {
      console.log('Started');
      console.log(`1. ${yield}`); // 第一次调用 执行到这个yield 但是这个console没有执行
      console.log(`2. ${yield}`); // 第二次调用 执行到这个yield  第二个console执行了 并将a赋值给了第一个yield 但是该行的console没有执行
      return 'result'; // 第三次调用 将return后的值作为value返回 并执行了第三个console
    }
    
    let genObj = dataConsumer();
    genObj.next(); // {value: undefined, done : false}
    // Started  执行了第一个console
    genObj.next('a') // {value: undefined, done : false}
    // 1. a 执行了第二个console
    genObj.next('b') // {value: result, done : true}
    // 2. b  执行了第三个console
    

    如果想要第一次调用next方法时,就能够输入值,可以在 Generator 函数外面再包一层。

    function wrapper(generatorFunction) {
      return function (...args) {
        // 2.调用Generator函数得到遍历器对象,但是Generator中的语句不会被执行
        let generatorObject = generatorFunction(...args);
        // 3.调用遍历器对象的next(),Generator中的语句开始执行
        generatorObject.next(); // 得到一个对象:{value: undefined, done: false}
        // 5.上面的next执行完,暂停在console,回到这里返回遍历器对象
        return generatorObject;
      };
      // 1.wrapped() 相当于这个匿名函数的调用
    }
    
    const wrapped = wrapper(function* () {
      // 4.第三步调用的next()进入到这里 碰到yield暂停执行 console语句没有执行完 不会打印
      console.log(`First input: ${yield}`);
      // 7.进入Generator函数继续执行return,代表上面的console语句已经执行完毕,并把hello赋值给yield表达式
      return 'DONE';
    });
    
    // 6.第二次调用遍历器对象的next的方法并传入hello
    wrapped().next('hello!') //得到一个对象 {value : 'DONE', done: true}
    // First input: hello! 8.此时打印出语句
    

    for...of

    function* foo() {
      yield 1;
      yield 2;
      return 3;
    }
    
    for (let v of foo()) {
      console.log(v);
    }
    // 1 2
    
    // 一旦next方法的返回对象的done属性为true
    // for...of循环就会中止,且不包含该返回对象 
    // 所以上面代码的return 3,不包括在for...of循环之中。
    

    Generator.prototype.throw()

    Generator 函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。

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

    var g = function* () {
      try {
        yield;
      } catch (e) {
        console.log(e);
      }
    };
    
    var i = g();
    i.next();
    i.throw(new Error('出错了!'));
    // Error: 出错了!(…)
    

    不要混淆遍历器对象的throw方法和全局的throw命令。上面代码的错误,是用遍历器对象的throw方法抛出的,而不是用throw命令抛出的。后者只能被函数体外的catch语句捕获。

    如果 Generator 函数内部没有部署try...catch代码块,那么throw方法抛出的错误,将被外部try...catch代码块捕获。

    如果 Generator 函数内部和外部,都没有部署try...catch代码块,那么程序将报错,直接中断执行。

    throw方法抛出的错误要被内部捕获,前提是必须至少执行过一次next方法。

    function* gen() {
      try {
        yield 1;
      } catch (e) {
        console.log('内部捕获');
      }
    }
    
    var g = gen();
    g.throw(1);
    // Uncaught 1
    

    g.throw(1)执行时,next方法一次都没有执行过。这时,抛出的错误不会被内部捕获,而是直接在外部抛出,导致程序出错。第一次执行next方法,等同于启动执行 Generator 函数的内部代码,否则 Generator 函数还没有开始执行,这时throw方法抛错只可能抛出在函数外部。

    throw方法被捕获以后,会附带执行下一条yield表达式。也就是说,会附带执行一次next方法。

    var gen = function* gen(){
      try {
        yield console.log('a');
      } catch (e) {
        // ...
      }
      yield console.log('b');
      yield console.log('c');
    }
    
    var g = gen();
    g.next() // a
    g.throw() // b
    g.next() // c
    

    g.throw方法被捕获以后,自动执行了一次next方法,所以会打印b。另外,也可以看到,只要 Generator 函数内部部署了try...catch代码块,那么遍历器的throw方法抛出的错误,不影响下一次遍历。

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

    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 }
    // return方法调用时,不提供参数,则返回值的value属性为undefined。
    g.next()        // { value: undefined, done: true }
    

    如果 Generator 函数内部有try...finally代码块,且正在执行try代码块,那么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 }
    
    // return方法后,就开始执行finally代码块
    // 然后等到finally代码块执行完,再执行return方法。
    

    next()、throw()、return() 的共同点

    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;
    // 如果next方法没有参数,就相当于替换成undefined。
    

    throw()是将yield表达式替换成一个throw语句

    // 场景都是调用过一次next的情况下 ,第一次调用next相当于是启动函数的执行 传参了都会被忽略。
    gen.throw(new Error('出错了')); // Uncaught Error: 出错了
    // 相当于将 let result = yield x + y
    // 替换成 let result = throw(new Error('出错了'));
    

    return()是将yield表达式替换成一个return语句。

    // 场景都是调用过一次next的情况下 ,第一次调用next相当于是启动函数的执行 传参了都会被忽略。
    gen.return(2); // Object {value: 2, done: true}
    // 相当于将 let result = yield x + y
    // 替换成 let result = return 2;
    

    yield* 表达式

    ES6 提供了yield*表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数。

    function* foo() {
      yield 'a';
      yield 'b';
    }
    
    function* bar() {
      yield 'x';
      yield* foo();
      yield 'y';
    }
    
    // 等同于
    // yield*后面的 Generator 函数(没有return语句时),不过是for...of的一种简写形式。
    function* bar() {
      yield 'x';
      for (let v of foo()) {
        yield v;
      }
      yield 'y';
    }
    // 使用for of
    for (let v of bar()){
      console.log(v);
    }
    // "x"
    // "a"
    // "b"
    // "y"
    
    // 如果foo() 有 return 
    function* foo() {
      yield 'a';
      yield 'b';
      return 'c';
      // 上面的for of 还是一样会输出x a b y
    }
    
    // 除非bar像下面这样写 才会输出 x a b c y 
    // 因为foo() 在执行到return的时候 done就是true for...of循环就会中止,且不包含该返回对象 
    function* bar() {
      yield 'x';
      yield yield* foo();
      yield 'y';
    }
    

    再来看一个对比的例子

    function* inner() {
      yield 'hello!';
    }
    
    function* outer1() {
      yield 'open';
      yield inner();
      yield 'close';
    }
    
    var gen = outer1()
    gen.next().value // "open"
    gen.next().value // 返回一个遍历器对象
    gen.next().value // "close"
    
    function* outer2() {
      yield 'open'
      yield* inner()
      yield 'close'
    }
    
    var gen = outer2()
    gen.next().value // "open"
    gen.next().value // "hello!"
    gen.next().value // "close"
    

    从语法角度看,如果yield表达式后面跟的是一个遍历器对象,需要在yield表达式后面加上星号,表明它返回的是一个遍历器对象。这被称为yield*表达式。

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

    function* gen(){
      yield* ["a", "b", "c"];
    }
    
    gen().next() // { value:"a", done:false }
    // yield命令后面如果不加星号,返回的是整个数组
    // 加了星号就表示返回的是数组的遍历器对象。
    
    let read = (function* () {
      yield 'hello';
      yield* 'hello';
    })();
    
    read.next().value // "hello"
    read.next().value // "h"
    // yield*语句返回单个字符
    // 因为字符串具有 Iterator 接口,所以被yield*遍历。
    

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

    function* foo() {
      yield 2;
      return "foo";
    }
    
    function* bar() {
      yield 1;
      var v = yield* foo();
      // foo() 有 return 所有 ”foo“就是 yield* foo()的值
      console.log("v: " + v);
      yield 3;
    }
    
    var it = bar();
    
    it.next()
    // {value: 1, done: false}
    it.next()
    // {value: 2, done: false}
    it.next();
    // "v: foo" 
    // {value: 3, done: false}
    it.next()
    // {value: undefined, done: true}
    

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

    function* iterTree(tree) {
      if (Array.isArray(tree)) {
        for(let i=0; i < tree.length; i++) {
          yield* iterTree(tree[i]);
          // 为什么不直接递归iterTree(tree[i])
          // 因为iterTree(tree[i])相当于是g() 只得到了一个遍历器对象 并不会执行g中的代码
          // 所以 你会想到时候 for of 循环 iterTree(tree[i]) 这个遍历器对象。然后yield 循环得到的value
          // 这正是yield* 的实现,yield* iterTree(tree[i])
          // 等同于
          // for(let v of iterTree(tree[i])) {
          //   yield v;
          // }
        }
      } else {
        yield tree;
      }
    }
    
    const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
    
    for(let x of iterTree(tree)) {
      console.log(x);
      // a b c d e
    }
    
    [...iterTree(tree)] // ["a", "b", "c", "d", "e"]
    

    Generator 函数的this

    Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的prototype对象上的方法。

    function* g() {}
    
    g.prototype.hello = function () {
      return 'hi!';
    };
    
    let obj = g();
    
    obj instanceof g // true
    obj.hello() // 'hi!'
    

    g返回的总是遍历器对象,而不是this对象。 如果函数g在this对象上面添加了一个属性a,但是obj对象拿不到这个属性。

    function* g() {
      this.a = 11; // 这个this指向调用者window
    }
    
    let obj = g();
    obj.next();
    obj.a // undefined
    console.log(window.a) // 11
    console.log(a) // 11
    

    Generator 函数也不能跟new命令一起用,会报错。

    如何让 Generator 函数返回一个正常的对象实例,获得正常的this呢,以及才能够使用new呢

    function* g() {
      this.a = 1; // 此时的this执行g.prototype 
      yield this.b = 2;
      yield this.c = 3;
      // 所以next调用完之后 g.prototype上就有了a b c三个属性
    }
    // 只是一个包装函数,最后实际上new的是F,是不能使用 new g()的,会报错
    function F() { 
      return g.call(g.prototype);
    }
    
    var f = new F();// new F() 放回的就是遍历器对象 
    // 这个遍历器对象 就是g的实例
    // 所以可以使用f.a 访问到 g.prototype.a
    
    f.next();  // Object {value: 2, done: false}
    f.next();  // Object {value: 3, done: false}
    f.next();  // Object {value: undefined, done: true}
    // 必须要调用next 使g函数中个的代码执行完毕
    f.a // 1
    f.b // 2
    f.c // 3
    

    应用

    异步操作的同步化表达

    Generator 函数的暂停执行的效果,意味着可以把异步操作写在yield表达式里面,等到调用next方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在yield表达式下面,反正要等到调用next方法时再执行。

    function* loadUI() {
      showLoadingScreen();
      yield loadUIDataAsynchronously();
      hideLoadingScreen();
    }
    var loader = loadUI();
    // 加载UI
    loader.next()
    
    // 卸载UI
    loader.next()
    
    // 第一次调用loadUI函数时,该函数不会执行,仅返回一个遍历器。
    // 下一次对该遍历器调用next方法,则会显示Loading界面(showLoadingScreen)
    // 并且异步加载数据(loadUIDataAsynchronously)。
    // 等到数据加载完成,再一次使用next方法,则会隐藏Loading界面。
    

    Ajax 是典型的异步操作,通过 Generator 函数部署 Ajax 操作,可以用同步的方式表达。

    function* main() {
      var result = yield request("http://some.url");
      var resp = JSON.parse(result); // 此时 result 就是 response
        console.log(resp.value);
    }
    
    function request(url) {
      makeAjaxCall(url, function(response){
        it.next(response); // 第二次调用将response赋值第一次yield
      });
    }
    
    var it = main();
    it.next(); // 第一次调用启动函数
    

    控制流管理

    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); 
      // next执行了5次,第5次执行完之后 taskObj.done 才是true
      // 通过taskObj 来存储每次step()的值
      // 并在下次next的传递给上一次的yield 第一次传递undefined 之后依次传递
    
      // 如果Generator函数未结束,就继续调用
      if (!taskObj.done) {
        task.value = taskObj.value
        scheduler(task);
      }
    }
    

    上面这种做法,只适合同步操作,即所有的task都必须是同步的,不能有异步操作。因为这里的代码一得到返回值,就继续往下执行,没有判断异步操作何时完成。

    for...of的本质是一个while循环

    var it = iterateJobs(jobs);
    var res = it.next();
    
    while (!res.done){
      var result = res.value;
      // ...
      res = it.next();
    }
    

    作为数组结构

    function* doStuff() {
      yield fs.readFile.bind(null, 'hello.txt');
      yield fs.readFile.bind(null, 'world.txt');
      yield fs.readFile.bind(null, 'and-such.txt');
      // bind返回一个函数
      // return function(arg) {
      //   return fs.readFile.apply(null, arg)
      // }
    }
    
    for (task of doStuff()) {
      // task是一个函数,可以像回调函数那样使用它
    }
    

    如果用 ES5 表达,完全可以用数组模拟 Generator 的这种用法。

    function doStuff() {
      return [
        fs.readFile.bind(null, 'hello.txt'),
        fs.readFile.bind(null, 'world.txt'),
        fs.readFile.bind(null, 'and-such.txt')
      ];
    }
    // 可以用一模一样的for...of循环处理!
    // 两相一比较,就不难看出 Generator 使得数据或者操作,具备了类似数组的接口。
    

    相关文章

      网友评论

          本文标题:ES6标准入门 摘要 (Generator)

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