Skip to content

generator学习及使用

1. js解决异步的发展历程

js解决异步,最早靠的是回调,然后随着promise的出现,升级为promise,但是,promise还是不支持同步的写法,写起来还是得不停的.then,写长了也很麻烦,代码看起来也很痛苦,后面出现了generator,但是generator写起来还是很不友好,直到最终出现了async + await才让一步很方便

generator用的不是很多,但是它的原理需要掌握,以便更好的理解async + await

2. generator是什么?

生成器对象Generator是由一个 generator function 返回的,并且它符合可迭代协议迭代器协议

generator(生成器)是ES6标准引入的新的数据类型。一个generator看上去像一个函数,但可以返回多次。

generator和函数不同的是,generatorfunction*定义(注意多出的*号),并且,除了return语句,还可以用yield返回多次。

js
function * fn(){
  
}

let it = fn()

注意: generator函数的这个 * 可以挨着function,或者挨着函数名,也可以放在中间谁都不挨;

生成器函数调用后,会返回一个iterator,叫做迭代器;

调用一个生成器函数并不会马上执行它里面的语句,而是返回一个这个生成器的 迭代器 ( iterator )对象。当这个迭代器的 next() 方法被首次(后续)调用时,其内的语句会执行到第一个(后续)出现yield的位置为止,yield 后紧跟迭代器要返回的值。或者如果用的是 yield*(多了个星号),则表示将执行权移交给另一个生成器函数(当前生成器暂停执行)。

迭代器中有next方法,可以通过iterator调用next,是固定写法it.next(),调用后会有返回结果,例如:

js
function * fn(){
  
}

let it = fn()
let obj = it.next()
console.log(obj) // { value: undefined, done: true }

{ value: undefined, done: true }中,valueundefined表示没有产出结果,donetrue表示已经结束了;

next()方法返回一个对象,这个对象包含两个属性:value 和 done,value 属性表示本次 yield 表达式的返回值,done 属性为布尔类型,表示生成器后续是否还有 yield 语句,即生成器函数是否已经执行完毕并返回。

*可以配合yeild来使用,yield含义是产出,且有中断功能更,相当于,函数执行过程中,碰到yield就停止执行,例如:

js
function * fn(){
  let a = yield 1
}

let it = fn()
let obj = it.next()
console.log(obj) // { value: 1, done: false } 这里的value:1并不是a的值,而是yield产出的结果
// 此时函数执行,碰到yield,产出1,且函数停止执行,所以obj中value为1,done为false

当迭代器每调用一次next方法,会让执行一个yield后面的代码执行,并产出结果,但是此时代码是卡在yield的位置,要想yield左边得到返回值,需要接着调用yield,例如:

js
function * fn(){
  let a = yield 1
  console.log(a); // undefined
}

let it = fn()
let obj = it.next()
console.log(obj) // { value: 1, done: false } 这里的value:1并不是a的值,而是yield产出的结果
let obj1 = it.next()
console.log(obj1); // { value: undefined, done: true } 表示函数执行完毕,但是没有返回值

若是用一个return返回一个值,则看效果

js
function * fn(){
  let a = yield 1
  console.log(a); // undefined
  return 100
}

let it = fn()
let obj = it.next()
console.log(obj) // { value: 1, done: false } 这里的value:1并不是a的值,而是yield产出的结果
let obj1 = it.next()
console.log(obj1); // { value: 100, done: true } 表示函数执行完毕,返回值是100

上面的例子可以看出,generator函数执行完后,返回的是一个带有valuedown的对象; yield非常像return,但是从上面例子可以看出,a的值依然是undefined,所以yield并不会给它前面的变量赋值,要想将yield产出的值赋给它前面的变量a,可以这样写:

js
function * fn(){
  let a = yield 1
  console.log(a); // 1
}

let it = fn()
//  第一个next函数中传递参数是没意义的
let obj = it.next()
console.log(obj) // { value: 1, done: false } 这里的value:1并不是a的值,而是yield产出的结果

// next会将传入从参数,会传递给上一次yield前面的变量(或者说会传递给上一次yield的返回值)
let obj1 = it.next(obj.value) 
console.log(obj1); // { value: undefined, done: true } 表示函数执行完毕,但是没有返回值

注意

  1. next会将传入从参数,会传递给上一次yield前面的变量(或者说会传递给上一次yield的返回值)
  2. 第一个next函数中传递参数是没意义的,因为没有上一个yield

调用 next()方法时,如果传入了参数,那么这个参数会传给上一条执行的 yield语句左边的变量

当在生成器函数中显式 return 时,会导致生成器立即变为完成状态,即调用 next() 方法返回的对象的 done 为 true。如果 return 后面跟了一个值,那么这个值会作为当前调用 next() 方法返回的 value 值。

总结:

在generator 函数中,

  1. yield 相当于设置了一个断点,执行到这就暂停当前函数的运行,并返回值或执行语句的结果value。它和return的区别在于,return的返回意味着函数生命周期的结束,而yield的返回只是挂起了函数的运行。
  2. next 相当于从之前的yield断点或函数起始位置按F5执行,并在下一个yield断点,或return正常结束时停下脚步,后获得返回的value。next同样可以传参数,其参数会占领上一轮yield的结束位置,如果未指定则替代值为undefined。next 和 yield是一一对应的开始与暂停符,而最后一个next对应return。generator 函数如果正常结束(以return结束),则视为完成任务(done == true).
  3. yield 如果想在其后嵌套调用generator 函数,则必须在其后加个*号,yield* generatorFunc() 会继续自动调用 generatorFunc().next(),而不会暂停当前函数。yield* 必须是一个generator 函数对象,否则会将其当成字符串以yield的方式逐个字符输出。

3. generator函数原理

要想知道generator语法的原理,可以看一下,编译过后,generator函数语法具体是怎么实现的,这里借助babel,将generator进行编译:

babel在线编译器入口

image.png

从上图可以看出,编译前后代码对比,我们可以知道,核心靠的就是 switch case 来实现的;

4. 迭代器基本实现

js
const interable = { 0: 'a', 1: 'b', 2: 'c', length: 3 };
interable[Symbol.iterator] = function() {
    let index = 0;
    return { // 遍历器对象
        next: () => {
            return { value: this[index], done: index++ == this.length }
        }
    }
}

console.log([...interable]); // [ 'a', 'b', 'c' ]

解释: 一个伪数组该具备的条件:

  1. 有索引
  2. 有length属性
  3. 能被遍历(迭代)

一个迭代器都会有一个Symbol.iterator属性,且,迭代器会返回一个next方法,调用这个next会返回一个包含value和done属性的对象。

如果我们自己去迭代一个对象需要实现一个迭代器接口,自己返回一个具有next方法的对象。内部会调用这个next方法返回结果包含value和done,当done为true时迭代完成

1. Array.from于...展开运算符号的区别

看一下例子

js
let obj = {
    0: 1,
    1: 2,
    2: 3,
    length:3
}
// 用from
let arr = Array.from(obj)
console.log(arr); // [ 1, 2, 3 ]

// 展开运算符
let arr1 = [...obj] // 报错:object is not iterable (cannot read property Symbol(Symbol.iterator))

5. 通过生成器实现遍历器

上面说过,调用生成器函数会得到一个可迭代对象,那么可以将上一步中实现的迭代器的interable[Symbol.iterator]方法直接其换成生成器函数,如下:

js
const iterable = { 0: 'a', 1: 'b', 2: 'c', length: 3 };
iterable[Symbol.iterator] = function*() {
    let index = 0;
    while (index !== this.length) {
        yield this[index++]
    }
}
console.log([...iterable]);

生成器函数会自带next方法

6. 生成器的使用

这里主是掌握异步迭代的思想。(产生一个迭代函数,当做回调函数使用)

js
// it表示 iterator,也就是生成器函数调用之后的结果
function co(it){
    return new Promise((resolve,reject)=>{
        function next(data){
            let {value,done} = it.next(data);
            if(!done){
                Promise.resolve(value).then(data=>{
                    next(data);
                },reject)
            }else{
                resolve(value);
            }
        }
        next();
    });
}

这里是模拟了一个库,co,co库就是对生成器函数的使用进行了封装,我们子使用生成器函数时候就不用很麻烦的写一堆next方法了。 CO库npm地址:https://www.npmjs.com/package/co

[Symbol.iterator]

Symbol.iterator是ES6中提供的,可以用来会去对象的@@iterator内部属性。

数组,字符串等对象之所以可以用for...of进行比遍历,原因是这些对象内部都内置的有@@iterator对象,

for...in循环除外,for...in循环用来遍历对象,并且for...in遍历对象是无法直接获取属性值的,因为它遍历的实际上是对象中所有可枚举属性,取药手动取属性值。

for...of不一样,for...of首先会向背访问对象请求一个迭代器对象,然后通过调用迭代器对象的next()方法来遍历所有返回值

例如:数组内置了@@iterator,for...of可以直接应用在数组上,我们使用内置的@@iterator进行手动遍历数组,可以看他是怎么工作的:

js
let arr = [1,2,3];
let it == arr[Symbol.iterator]();

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

与数组不同,普通对象内并没有@@iterator,所以无法自动完成for...of遍历。

但是,我们若是想用for...of遍历一个普通对象,只需要手动为这个对象添加@@iterator即可,例如:

js
let obj = {
  a: 1,
  b: 2,
};
// --------------为obj添加@@iterator对象------------
// 这里用Object.defineProperty定义的原因是为了让Symbol.iterator属性不可枚举
Object.defineProperty(obj, Symbol.iterator, {
  enumerable: false,
  writable: false,
  configurable: true,
  value: () => {
    let keys = Object.keys(obj);
    let idx = 0;
    return {
      next: () => ({
        value: obj[keys[idx++]],
        done: idx > keys.length,
      }),
    };
  },
});

for (const v of obj) {
  console.log(v);
}
// 1
// 2

这样普通对象就变成了可迭代对象。