生成器函数声明与调用

生成器Generator

生成器本身是一个函数,是ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同

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

function* fn() {
    console.log("hello world");
}

let iterator = fn();
// console.log(iterator); //Object [Generator] {}
// console.log(iterator.next()); // hello world { value: undefined, done: true }
iterator.next(); // hello world

形式上,Generator 函数是一个普通函数,但是有两个特征

-function关键字与函数名之间有一个星号;
-函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。

yield表达式

yield可以看作是函数代码的分隔符。

由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。

function* fn() {
    console.log(111);
    console.log(222);
    console.log(333);
    console.log(444);
}

let iterator = fn();
iterator.next();
// 1111
// 2222
// 3333
// 4444

上面代码,在调用next方法后,遍历指针会依次执行函数中的代码。

使用yield分隔符后,将函数中的代码分割成n个代码片段,可以通过依次调用next方法执行

function* fn() {
    console.log(111);
    yield '一只没有耳朵';
    console.log(222);
    yield '一只没有尾巴';
    console.log(333);
    yield '真奇怪';
    console.log(444);
}

let iterator = fn();
console.log(iterator.next()); 
console.log(iterator.next()); 
console.log(iterator.next());
console.log(iterator.next()); 

// 111
// { value: '一只没有耳朵', done: false }
// 222
// { value: '一只没有尾巴', done: false }
// 333
// { value: '真奇怪', done: false }
// 444
// { value: undefined, done: true } 注意:当done为true时,表示遍历结束

遍历器对象的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;
}

let iterator = gen();
console.log(iterator.next().value); // 579
console.log(iterator.next().value); // undefined

Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)。

function* gen() {
    yield 123 + 456;
}

let iterator = gen();
console.log(iterator.next()); // { value: 579, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

下一步,必须调用遍历器对象的next方法使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行

总结:

调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着valuedone两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

yield表达式与return语句

共同点:都能返回紧跟在语句后面的那个表达式的值
区别:
1、每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。
2、一个函数里面,只能执行一次(或者说一个)return语句,但是可以执行多次(或者说多个)yield表达式。正常函数只能返回一个值,因为只能执行一次return;Generator 函数可以返回一系列的值,因为可以有任意多个yield。

与 Iterator 接口的关系

任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。

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

var foo = {};
foo[Symbol.iterator] = function* fn() {
    yield 123;
    yield 456;
    yield 789;
}
console.log([...foo]);) // [123, 456, 789]

上面代码中,Generator 函数赋值给Symbol.iterator属性,从而使得myIterable对象具有了 Iterator 接口,可以被...运算符遍历了。

生成器Generator函数的参数传递以及next方法的参数

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

function* fn(arg) {
    console.log(arg);
    let one = yield 123;
    console.log(one);
    let two = yield 456;
    console.log(two);
    let three = yield 789;
    console.log(three);
}
let iterator = fn('aaa');
console.log(iterator.next()); //{ value: 123, done: false }
// next方法传参 
console.log(iterator.next('bbb'));
console.log(iterator.next('ccc'));
console.log(iterator.next('ddd'));

来看另一个例子

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 }

上面代码先定义了一个可以无限运行的 Generator 函数f,如果next方法没有参数,每次运行到yield表达式,变量reset的值总是undefined。当next方法带一个参数true时,变量reset就被重置为这个参数(即true),因此i会等于-1,下一轮循环就会从-1开始递增。

这个功能有很重要的语法意义。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 }
b.next(13) // { value:42, done:true }

上面代码中,第二次运行next方法的时候不带参数,导致 y 的值等于2 * undefined(即NaN),除以 3 以后还是NaN,因此返回对象的value属性也等于NaN。第三次运行Next方法的时候不带参数,所以z等于undefined,返回对象的value属性等于5 + NaN + undefined,即NaN

如果向next方法提供参数,返回结果就完全不一样了。上面代码第一次调用bnext方法时,返回x+1的值6;第二次调用next方法,将上一次yield表达式的值设为12,因此y等于24,返回y / 3的值8;第三次调用next方法,将上一次yield表达式的值设为13,因此z等于13,这时x等于5y等于24,所以return语句的值等于42

 注意,由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的

for...of 循环

for...of循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法。

function* fn() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    yield 5;
}

for (let k of fn()) {
    console.log(k);
}
// 1 2 3 4 5

利用for...of循环,可以写出遍历任意对象(object)的方法。原生的 JavaScript 对象没有遍历接口,无法使用for...of循环,通过 Generator 函数为它加上这个接口,就可以用了。

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

for (let [key, value] of Object.entries(jane)) {
    console.log(`${key}:${value}`);
}

生成器Generator函数在异步编程中的表现

假设有一个需求:1s后输出111,2s后输出222,3s后输出333...

// 传统做法=>回调函数
setTimeout(() => {
    console.log(111);
    setTimeout(() => {
        console.log(222);
        setTimeout(() => {
            console.log(333);
        }, 1000);
    }, 1000);
}, 1000);

// 111
// 222
// 333

回调函数本身并没有问题,它的问题出现在多个回调函数嵌套。

代码不是纵向发展,而是横向发展,很快就会乱成一团,无法管理。因为多个异步操作形成了强耦合,只要有一个操作需要修改,它的上层回调函数和下层回调函数,可能都要跟着修改。这种情况就称为"回调函数地狱"(callback hell)。

第一个实例

function one() {
    setTimeout(() => {
        console.log(111);
        iterator.next();
    }, 1000);
}

function two() {
    setTimeout(() => {
        console.log(222);
        iterator.next();
    }, 1000);
}

function three() {
    setTimeout(() => {
        console.log(333);
        iterator.next();
    }, 1000);
}

function* gen() {
    yield one();
    yield two();
    yield three();
}

let iterator = gen();
iterator.next();

// for (let k of gen()) {
//     console.log(k);
// }

实例2: 模拟获取:用户数据、订单数据、商品数据

// 模拟获取:用户数据、订单数据、商品数据
function getUsers() {
    setTimeout(() => {
        let data = "用户数据";
        // 调用next方法,并且将数据传入
        iterator.next(data); // 第二次调用next方法,传入的参数将作为第一次yield的返回结果
    }, 1000);
}

function getOrders() {
    setTimeout(() => {
        let data = "订单数据";
        iterator.next(data); // 第三次调用next方法,传入的参数将作为第二次yield的返回结果
    }, 1000);
}

function getGoods() {
    setTimeout(() => {
        let data = "商品数据";
        iterator.next(data); // 第四次调用next方法,传入的参数将作为第三次yield的返回结果
    }, 1000);
}

// 生成器函数
function* fn() {
    let user = yield getUsers();
    console.log(user); // 用户数据
    let orders = yield getOrders();
    console.log(orders); // 订单数据
    let goods = yield getGoods();
    console.log(goods); // 商品数据
}

// 调用生成器函数
let iterator = fn();
iterator.next();

 

posted @ 2021-07-30 14:40  半白半黑  阅读(165)  评论(0)    收藏  举报