JS:同步迭代器和异步迭代器

3,126 阅读5分钟

什么是同步迭代器呢?

举个例子:

var obj = {
  *[Symbol.iterator]() {
    yield 1;
    yield 2;
    yield 3;
  }
}

var iterator = obj[Symbol.iterator]()

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

这里的 iterator 就是同步迭代器了,每调用一次 next 方法,就返回一个 { value: xx, done: xx } 形式的对象。

什么是异步迭代器呢?

再举个例子:

var obj = {
  async *[Symbol.asyncIterator]() {
    yield 1;
    yield 2;
    yield 3;
  }
}

var asyncIterator = obj[Symbol.asyncIterator]()

asyncIterator.next().then(data => console.log(data)) // {value: 1, done: false}
asyncIterator.next().then(data => console.log(data)) // {value: 2, done: false}
asyncIterator.next().then(data => console.log(data)) // {value: 3, done: false}
asyncIterator.next().then(data => console.log(data)) // {value: undefined, done: true}

与同步可迭代对象部署了 [Symbol.iterator] 属性不同的是,异步可迭代对象的标志是部署了 [Symbol.asyncIterator] 这个属性。

这里的 asyncIterator 就是异步迭代器了。与同步迭代器 iterator 不同的是,在 asyncIterator 上调用 next 方法得到是一个 Promise 对象,其内部值是 { value: xx, done: xx } 的形式,类似于 Promise.resolve({ value: xx, done: xx })

为什么会有异步迭代器呢?

同步迭代器里数据都是当时就能获取的(没有延迟),而异步迭代器里的数据往往获取是需要时间的(有延迟)。

下面举了几个例子,来说明为什么要用异步迭代器,以及它的使用场景。

  1. 同步迭代器数据即用即给,处理顺序等于遍历顺序。
var obj = {
  *[Symbol.iterator]() {
    yield 1;
    yield 2;
    yield 3;
  }
}

for (let item of obj) {
	console.log(item) // 1 -> 2 -> 3。处理顺序等于遍历顺序
}
  1. 如果同步迭代器数据获取需要时间,那么再用 for-of 遍历的话,就有问题。
var obj = {
  *[Symbol.iterator]() {
    yield new Promise(resolve => setTimeout(() => resolve(1), 5000));
    yield new Promise(resolve => setTimeout(() => resolve(2), 2000));
    yield new Promise(resolve => setTimeout(() => resolve(3), 500));
  }
}

console.log(Date.now())
for (let item of obj) {
    item.then(data => console.log(Date.now(), data))
}

// 1579253648926
// 1579253649427 3 // 1579253649427 - 1579253648926 = 501
// 1579253650927 2 // 1579253650927 - 1579253648926 = 2001
// 1579253653927 1 // 1579253653927 - 1579253648926 = 5001

可以把这里的每个 item 当成是接口请求,数据返回的时间不一定的。上面的打印结果就说明了问题所在——我们控制不了数据的处理顺序。

  1. 再来看看异步迭代器
var obj = {
  async *[Symbol.asyncIterator]() {
    yield new Promise(resolve => setTimeout(() => resolve(1), 5000));
    yield new Promise(resolve => setTimeout(() => resolve(2), 3000));
    yield new Promise(resolve => setTimeout(() => resolve(3), 500));
  }
}

console.log(Date.now())
for await (let item of obj) {
	console.log(Date.now(), item)
}

// 1579256590699
// 1579256595700 1 // 1579256595700 - 1579256590699 = 5001
// 1579256598702 2 // 1579256598702 - 1579256590699 = 8003
// 1579256599203 3 // 1579256599203 - 1579256590699 = 8504

注意,异步迭代器要声明在 [Symbol.asyncIterator] 属性里,使用 for-await-of 循环处理的。最终效果是,对任务挨个处理,上一个任务等待处理完毕后,再进入下一个任务。

因此,异步迭代器就是用来处理这种不能即时拿到数据的情况,还能保证最终的处理顺序等于遍历顺序,不过需要依次排队等待。

for-await-of

for-await-of 除了能用在异步可迭代对象上,还能用在同步可迭代对象上

var obj = {
  *[Symbol.iterator]() {
    yield 1;
    yield 2;
    yield 3;
  }
}

for await(let item of obj) {
	console.log(item) // 1 -> 2 -> 3
}

是这样的,如果 for-await-of 发现遍历对象上没有 [Symbol.asyncIterator] 的话,就去调用 [Symbol.iterator]。我们都知道,由此得来的是同步迭代器,在这个迭代器上调用 next 方法的时候,返回就是一个普通的对象,结构类似于 { value: xx, done: xx }

而 await obj 的结果还是 obj,下例为证:

(async function() {
    var obj = { value: 1, done: false }
    console.log((await obj) === obj) // true
})()

所以才会有上面的结果。

还有一点,需要注意的是,如果一个对象上同时部署了 [Symbol.asyncIterator] 和 [Symbol.iterator],那就会优先使用 [Symbol.asyncIterator] 生成的异步迭代器。这很好理解,因为 for-await-of 本来就是为异步迭代器而生的。

var obj = {
  *[Symbol.iterator]() {
    yield 1;
    yield 2;
    yield 3;
  },
  async *[Symbol.asyncIterator]() {
    yield 4;
    yield 5;
    yield 6;
  }
}

for await(let item of obj) {
	console.log(item) // 4 -> 5 -> 6。优先使用由 [Symbol.asyncIterator] 生成的异步迭代器
}

(正文完)


广告时间(长期有效)

我有一位好朋友开了一间猫舍,在此帮她宣传一下。现在猫舍里养的都是布偶猫。如果你也是个爱猫人士并且有需要的话,不妨扫一扫她的【闲鱼】二维码。不买也不要紧,看看也行。

(完)