深入浅出Iterator,Generator,Async

285 阅读4分钟

本篇不会教条式地复述语法、如果你需要语法的详细请移步阮一峰老师的教程.

为什么要把Iterator和Generator合为一谈?因为Generator就是Iterator接口的实现,Generator遵循Iterator接口的规范,只是单看Generator语法其实是一件很令人混乱的事,所以对这些知识进行了一个整理。

什么是Iterator接口

Iterator翻译过来就是迭代器的意思,接口代表着对一类函数输出的规定。如果用ts去描述Iterator大概是这样的:

interface IteratorNextRet {
    done: boolean,
    value?: any
}

interface Iterator {
    next: ():IteratorNextRet,
    throw?: ():void,
    return?: ():void
}

function iterator(): Iterator {
    return {
        next() { return { done: true } }
    }
}

也就是Iterator规定了函数的输出必须是一个对象并且有一个next函数、next函数输出一个对象带有done 与value。后面的throw与return均为可选值

Iterator与for of

根据上述Iterator我们可以试着改写数据的迭代器:比如遍历器只遍历索引为奇数的值,应该这么写:

// 你可以试着把这串代码输出到控制台
var arr = [0, 1, 2, 3, 4, 5]
arr[Symbol.iterator] = function() {
    // this 指向实例
    const len = this.length
    let idx = -1
    return {
        next: () => {
            idx += 2
            return {
                value: this[idx],
                done: idx >= len
            }
        }
    }
}
for (let i of arr) {
    console.log(i)
}

for of 大概做了这样的事情:生成一个新的迭代器、然后执行next,每次执行都输出value直到next返回值done为true、则停止输出value,停止迭代。

image.png

Iterator还有一些应用场景(ps:大家可以根据上面的实现去试一下以下的场景):

  1. 数组解构:[a1, a2] = 'hello;' // a1: h, a2: e String.prototype[Symbol.iterator]是有定义的
  2. 拓展运算符:var o = {} arr = [...o] // 报错Symbol.iterator未定义
  3. yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。

Generator

开头就说了Generator 是Iterator接口的实现,上述我们用普通函数的方法重写了数组的迭代器,这里试着使用Generator语法再写一个:

var arr = [0, 1, 2, 3, 4, 5]
arr[Symbol.iterator] = function* () {
    const len = this.length
    for (let i = 0; i <= len; i++) {
        if (i%2 === 1 && i > 0) {
            yield i;
        }
    }
}
for (let i of arr) console.log(i) // 1, 3, 5
// 甚至你也可以这样
for (let j of arr[Symbol.iterator]()) console.log(j) // 1 3 5

代码与之前相比简洁了很多并且逻辑也简单很多,从2个代码的对比可以知道 * 与 yield语法干了什, * 标志着这个函数是一个Generator函数,并且妹执行一次衍生出一个实例相当于它给你主动new了、yield 相当于调用 Generator 衍生子类的 next 返回的 value 。

function* gen() {
    var arr = [0, 1, 2]
    for (let v of arr) yield v
}
var g1 = gen()
var g2 = gen()
g1 === g2 // false
g1.next() // {value: 0, done: false}
g1.next() // {value: 1, done: false}
g1.next() // {value: 2, done: false}
g1.next() // {value: undefined, done: true}

Generator的相当于一个指针、指向哪里由 yield 决定,什么时候指向由暴露出的 next 决定。当然还有其他2个api returnthrowreturnthrow每次执行相当于打断指针、直接调到最后面(return)或者掉到catch语句(throw),并且donetrue

yield*

yield* 本质其实更像语法糖。

function* foo() {
    yield 1;
    yield 2;
}
function* bar() {
    yield 'a';
    // 到这里执行foo()
    yield* foo();
    yield 'b';
}
for (var v of bar()) {
    console.log(v) // a 1 2 b
}
// 相当于
function* bar() {
    yield 'a'
    for (var v of foo()) {
        yield v;
    }
    yield 'b'
}

如何实现一个类async await的runner?

都说async await 是 Generator 的语法糖,我们试着用Generator去模拟async函数、从而可以更加细致的看2者的区别。

假设 await 后面跟的一个 promise 函数。

// 模拟一个fetch接口
function fetch(time) {
  return function() {
    return new Promise((resolve, reject) => {
      setTimeout(resolve, time)
    })
  }
}
// 启动器、依次调用generator的next直到done为true
function run(g) {
  const { value: ret, done } = g.next()
  if (done) {
    return Promise.resolve();
  } else {
    return ret.then(() => {
      return run(g)
    })
  }
}

var fetch1 = fetch(1000)
var fetch2 = fetch(2000)
function* gen() {
  console.log('开始执行fetch1');
  yield fetch1();
  console.log('开始执行fetch2')
  yield fetch2();
  console.log('done')
}
var g = gen();
console.log(run(g));
// Promise
// 开始执行fetch1
// 1s 后
// 开始执行fetch2
// 2s 后
// done

假设,有时候 await 后面跟的不一定是promise、很有可能是Thunk函数。Thunk函数可能长这样:$.get('xxx').done(callback)

// 模拟数据获取 thunk 函数
function fetch(time) {
    return function() {
        let cb
        const ret = {
            done(callback) {
                cb = callback
            },
            exec() {
                cb && cb()
            }
        }
        setTimeout(ret.exec, time)
        return ret
    }
}
// 重写一个执行器
function run(g) {
  const { value: ret, done } = g.next()
  if (!done) {
    ret.done(function() {
      run(g);
    })
  }
}

var fetch1 = fetch(1000)
var fetch2 = fetch(2000)
function* gen() {
  console.log('开始执行fetch1');
  yield fetch1();
  console.log('开始执行fetch2')
  yield fetch2();
  console.log('done')
}
var g = gen();
run(g);
// 开始执行fetch1
// 1s 
// 开始执行fetch2
// 2s
// done

可以看出来写出了一个简易版的co模块。当然这里假设都比较极端,没有那么多的兼容。大家意会就好、所以如果有面试官问你async与generator的区别是什么?

async 相比 Generator 更加语义化、async 函数自动执行下面的await而Generator需要手动调用next,async 自动返回一个Promise函数而Generator返回Iterator接口。

以上.