什么是异步迭代?如何自定义迭代?一文详解ES6的迭代器与生成器

2023-10-17 15:57:54 浏览数 (1)

迭代器

迭代器是一种有序、连续的、基于拉取的用于消耗数据的组织方式,用于以一次一步的方式控制行为。 简单的来说我们迭代循环一个可迭代对象,不是一次返回所有数据,而是调用相关方法分次进行返回。

迭代器是帮助我们对某个数据结构进行遍历的对象,这个object有一个next函数,该函数返回一个有valuedone属性的object,其中value指向迭代序列中当前next函数定义的值。

代码语言:javascript复制
{
  done: boolean, // 为 true 时代表迭代完毕
  value: any     // done 为 true 时取值为 undefined
}

迭代协议

ES6的迭代协议分为迭代器协议(iterator protocol)和可迭代协议(iterable protocol),迭代器基于这两个协议进行实现。

迭代器协议: iterator协议定义了产生value序列的一种标准方法。只要实现符合要求的next函数,该对象就是一个迭代器。相当遍历数据结构元素的指针,类似数据库中的游标。

可迭代协议: 一旦支持可迭代协议,意味着该对象可以用for-of来遍历,可以用来定义或者定制 JS 对象的迭代行为。常见的内建类型比如Array & Map都是支持可迭代协议的。对象必须实现@@iterator方法,意味着对象必须有一个带有@@iterator key的可以通过常量Symbol.iterator访问到的属性。

模拟实现一个迭代器

基于迭代器协议
代码语言:javascript复制
// 实现
function createArrayIterator(arr) {
  let index = 0
  return {
    next: () =>
    index < arr.length
    ? { value: arr[index  ], done: false }
    : { value: undefined, done: true },
  }
}

// 测试
const nums = [11, 22, 33, 44]
const numsIterator = createArrayIterator(nums)
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())
基于可迭代协议

实现了生成迭代器方法的对象称为 可迭代对象 也就是说这个对象中包含一个方法, 该方法返回一个迭代器对象

一般使用 Symbol.iterator来定义该属性, 学名叫做 @@iterator 方法

代码语言:javascript复制
// 一个可迭代对象需要具有[Symbol.iterator]方法,并且这个方法返回一个迭代器
const obj = {
  names: ['111', '222', '333'],
  [Symbol.iterator]() {
    let index = 0
    return {
      next: () =>
      index < this.names.length
      ? { value: this.names[index  ], done: false }
      : { value: undefined, done: true },
      return: () => {
        console.log('迭代器提前终止了...')
        return { value: undefined, done: true }
      },
    }
  },
}

// 测试
for (const item of obj) {
  console.log(item)
  if (item === '222') break
}

在上面两个模拟迭代器示例中,还是相对比较复杂,但是ES6引入了一个生成器对象,它可以让创建迭代器对象的过程变得简单很多。

生成器

生成器(Generator)是一种返回 迭代器函数,通过function关键字后星号(*)来表示,函数中会用到新的关键字yield

代码语言:javascript复制
// 生成器
function* creatIterator (){
    yield 1
    yield 2
    yield 3
}
const iterator = creatIterator()
console.log(iterator.next()) // {value:1,done:false}
console.log(iterator.next()) // {value:2,done:false}
console.log(iterator.next()) // {value:3,done:false}
console.log(iterator.next()) // {value:undefined,done:true}

上述示例中,creatIterator()前的星号* 表明它是一个生成器,通过yield关键字来指定调用迭代器的next()方法时的返回值和返回顺序。

每当执行完一条yield语句后函数就会自动停止执行。拿上面的例子来说,执行完语句yield 1之后,函数便不再执行其他任何语言,直到再次调用迭代器的next()方法才会继续执行 yield 2 语句。

注意:yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。

代码语言:javascript复制
(function (){
  yield 1;
})()
// SyntaxError: Unexpected number

注意:ES6 没有规定,function关键字与函数名之间的星号,写在哪个位置。这导致下面的写法都能通过。

代码语言:javascript复制
function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }

生成器传参

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

代码语言:javascript复制
function* dr(arg) {
  console.log(arg)
  let one = yield '111'
  console.log(one)
  yield '222'
  console.log('ccc')
}
let iterator = dr('aaa')
console.log(iterator.next())
console.log(iterator.next('bbb'))
console.log(iterator.next())

应用场景

日常开发中会出现,下一个接口依赖于上一个接口的数据的情况,就可以使用生成器,而无需考虑异步回调地狱嵌套的问题。

模拟:1s后获取用户数据,2s后获取订单信息,3s后获取商品信息

代码语言:javascript复制
function getUser() {
  setTimeout(() => {
    const data = '用户数据'
    iterator.next(data)
  }, 1000)
}

function getOrder() {
  setTimeout(() => {
    const data = '订单信息'
    iterator.next(data)
  }, 2000)
}

function getGoods() {
  setTimeout(() => {
    const data = '商品数据'
    iterator.next(data)
  }, 3000)
}


function* initData() {
  const user = yield getUser()
  console.log(user)
  const order = yield getOrder()
  console.log(order)
  const goods = yield getGoods()
  console.log(goods)
}
const iterator = initData()
iterator.next()

for of

for of 循环可以获取一对键值中的键值,因为这个循环和迭代器息息相关,就放在这里一起说了。

一个数据结构只要部署了Symbol.iterator属性,就被视为具有iterator接口,可以使用for of,它可以循环可迭代对象。

JavaScript默认有iterable接口的数据结构:

  • 数组Array
  • Map
  • Set
  • String
  • Arguments对象
  • Nodelist对象,类数组 凡是部署了iterator接口的数据结构都可以使用数组的扩展运算符(…),和解构赋值等操作。

遍历数组

尝试用 for or 循环数组

既然数组是支持for...of循环的,那数组肯定部署了 Iterator 接口,我们通过它来看看Iterator 的遍历过程。

从图中我们能看出:

  1. Iterator 接口返回了一个有next方法的对象。
  2. 每调用一次 next,依次返回了数组中的项,直到它指向数据结构的结束位置。
  3. 返回的结果是一个对象,对象中包含了当前值value 和 当前是否结束done

遍历对象

尝试遍历一下对象,我们会发现他报这个对象是不可迭代的,如下图

那我们可以使用上面的迭代器对象生成器让对象也支持for of遍历

代码语言:javascript复制
obj[Symbol.iterator] = function* () {
  yield* this.name
}

也可以使用Object.keys()获取对象的key值集合,再使用for of

代码语言:javascript复制
const obj = {name: 'youhun',age: 18}
for(const key of Object.keys(obj)){
    console.log(key, obj[key])
    // name youhun
    // age 18
}

异步迭代

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

代码语言:javascript复制
// 用生成器生成
const obj = {
  async *[Symbol.asyncIterator]() {
    yield 1;
    yield 2;
    yield 3;
  }
}

const 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}

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

为什么要有异步迭代?

如果同步迭代器数据获取需要时间(比如实际场景中请求接口),那么再用 for-of 遍历的话,就有问题。

代码语言:javascript复制
const 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 (const 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 当成是接口请求,数据返回的时间不一定的。上面的打印结果就说明了问题所在:我们控制不了数据的处理顺序。

再来看看异步迭代器

代码语言:javascript复制
const 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 (const 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

我们可以使用如下代码进行遍历:

代码语言:javascript复制
for await (const item of obj) {
  console.log(item)
}

也就是说异步迭代遍历需要使用 for-await-of 语句。 除了能用在异步可迭代对象上,还能用在同步可迭代对象上

代码语言:javascript复制
const obj = {
  *[Symbol.iterator]() {
    yield 1
    yield 2
    yield 3
  }
}

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

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

相反如果同时部署了两个迭代器,但使用的是for-or那么优先使用同步迭代器。

代码语言:javascript复制
const obj = {
  *[Symbol.iterator]() {
    yield 1
    yield 2
    yield 3
  },
  async *[Symbol.asyncIterator]() {
    yield 4
    yield 5
    yield 6
  }
}

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

// 同步
for (const item of obj) {
    console.log(item) // 1 -> 2 -> 3。优先使用由 [Symbol.iterator] 生成的同步迭代器
}

总结

迭代器生成器逻辑可能有点绕,但是了解其原理是非常有必要的。可以自己尝试写一下,知其然知其所以然。这样才可以有需要的实现定义自己的迭代器来遍历对象,也可以应用在实际开发对应的场景中。

0 人点赞