什么是异步迭代?如何自定义迭代?一文详解ES6的迭代器与生成器
本文于391天之前发表,文中内容可能已经过时。
迭代器
迭代器是一种有序、连续的、基于拉取的用于消耗数据的组织方式,用于以一次一步的方式控制行为。
简单的来说我们迭代循环一个可迭代对象,不是一次返回所有数据,而是调用相关方法分次进行返回。
迭代器是帮助我们对某个数据结构进行遍历的对象,这个object
有一个next
函数,该函数返回一个有value
和done
属性的object
,其中value
指向迭代序列中当前next
函数定义的值。
1 | { |
迭代协议
ES6的迭代协议分为迭代器协议(iterator protocol)和可迭代协议(iterable protocol),迭代器基于这两个协议进行实现。
迭代器协议: iterator协议
定义了产生value
序列的一种标准方法。只要实现符合要求的next
函数,该对象就是一个迭代器。相当遍历数据结构元素的指针,类似数据库中的游标。
可迭代协议: 一旦支持可迭代协议,意味着该对象可以用for-of
来遍历,可以用来定义或者定制 JS 对象的迭代行为。常见的内建类型比如Array
& Map
都是支持可迭代协议的。对象必须实现@@iterator
方法,意味着对象必须有一个带有@@iterator key
的可以通过常量Symbol.iterator
访问到的属性。
模拟实现一个迭代器
基于迭代器协议
1 | // 实现 |
基于可迭代协议
实现了生成迭代器方法的对象称为 可迭代对象
也就是说这个对象中包含一个方法, 该方法返回一个迭代器对象
一般使用 Symbol.iterator
来定义该属性, 学名叫做 @@iterator
方法
1 | // 一个可迭代对象需要具有[Symbol.iterator]方法,并且这个方法返回一个迭代器 |
在上面两个模拟迭代器示例中,还是相对比较复杂,但是ES6引入了一个生成器对象,它可以让创建迭代器对象的过程变得简单很多。
生成器
生成器(Generator
)是一种返回 迭代器 的 函数,通过function
关键字后星号(*)来表示,函数中会用到新的关键字yield
。
1 | // 生成器 |
上述示例中,creatIterator()
前的星号* 表明它是一个生成器,通过yield
关键字来指定调用迭代器的next()
方法时的返回值和返回顺序。
每当执行完一条yield语句后函数就会自动停止执行。拿上面的例子来说,执行完语句yield 1
之后,函数便不再执行其他任何语言,直到再次调用迭代器的next()
方法才会继续执行 yield 2
语句。
注意:yield
表达式只能用在 Generator 函数里面,用在其他地方都会报错。
1 | (function (){ |
注意:ES6 没有规定,function
关键字与函数名之间的星号,写在哪个位置。这导致下面的写法都能通过。
1 | function * foo(x, y) { ··· } |
生成器传参
yield
表达式本身没有返回值,或者说总是返回undefined
。next
方法可以带一个参数,该参数就会被当作上一个yield
表达式的返回值。
1 | function* dr(arg) { |
应用场景
日常开发中会出现,下一个接口依赖于上一个接口的数据的情况,就可以使用生成器,而无需考虑异步回调地狱嵌套的问题。
模拟:1s后获取用户数据,2s后获取订单信息,3s后获取商品信息
1 | function getUser() { |
for of
for of
循环可以获取一对键值中的键值,因为这个循环和迭代器息息相关,就放在这里一起说了。
一个数据结构只要部署了Symbol.iterator
属性,就被视为具有iterato
r接口,可以使用for of
,它可以循环可迭代对象。
JavaScript
默认有iterable
接口的数据结构:
- 数组Array
- Map
- Set
- String
- Arguments对象
- Nodelist对象,类数组 凡是部署了
iterator
接口的数据结构都可以使用数组的扩展运算符(…),和解构赋值等操作。
遍历数组
尝试用 for or 循环数组
既然数组是支持for...of
循环的,那数组肯定部署了 Iterator
接口,我们通过它来看看Iterator
的遍历过程。
从图中我们能看出:
Iterator
接口返回了一个有next
方法的对象。- 每调用一次 next,依次返回了数组中的项,直到它指向数据结构的结束位置。
- 返回的结果是一个对象,对象中包含了当前值
value
和 当前是否结束done
遍历对象
尝试遍历一下对象,我们会发现他报这个对象是不可迭代的,如下图
那我们可以使用上面的迭代器对象生成器让对象也支持for of
遍历
1 | obj[Symbol.iterator] = function* () { |
也可以使用Object.keys()
获取对象的key
值集合,再使用for of
1 | const obj = {name: 'youhun',age: 18} |
异步迭代
与同步可迭代对象部署了 [Symbol.iterator]
属性不同的是,异步可迭代对象的标志是部署了 [Symbol.asyncIterator]
这个属性。
1 | // 用生成器生成 |
这里的 asyncIterator
就是异步迭代器了。与同步迭代器 iterator
不同的是,在 asyncIterator
上调用 next
方法得到是一个 Promise 对象,其内部值是 { value: xx, done: xx }
的形式,类似于 Promise.resolve({ value: xx, done: xx })
。
为什么要有异步迭代?
如果同步迭代器数据获取需要时间(比如实际场景中请求接口),那么再用 for-of
遍历的话,就有问题。
1 | const obj = { |
可以把这里的每个 item
当成是接口请求,数据返回的时间不一定的。上面的打印结果就说明了问题所在:我们控制不了数据的处理顺序。
再来看看异步迭代器
1 | const obj = { |
注意,异步迭代器要声明在 [Symbol.asyncIterator]
属性里,使用 for-await-of
循环处理的。最终效果是,对任务挨个处理,上一个任务等待处理完毕后,再进入下一个任务。
因此,异步迭代器就是用来处理这种不能即时拿到数据的情况,还能保证最终的处理顺序等于遍历顺序,不过需要依次排队等待。
for-await-of
我们可以使用如下代码进行遍历:
1 | for await (const item of obj) { |
也就是说异步迭代遍历需要使用 for-await-of
语句。 除了能用在异步可迭代对象上,还能用在同步可迭代对象上。
1 | const obj = { |
注意:如果一个对象上同时部署了 [Symbol.asyncIterator]
和 [Symbol.iterator]
,那就会优先使用 [Symbol.asyncIterator]
生成的异步迭代器。这很好理解,因为 for-await-of
本来就是为异步迭代器而生的。
相反如果同时部署了两个迭代器,但使用的是for-or
那么优先使用同步迭代器。
1 | const obj = { |
总结
迭代器生成器逻辑可能有点绕,但是了解其原理是非常有必要的。可以自己尝试写一下,知其然知其所以然。这样才可以有需要的实现定义自己的迭代器来遍历对象,也可以应用在实际开发对应的场景中。