5

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

 1 year ago
source link: https://www.iyouhun.com/post-256.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

什么是异步迭代?如何自定义迭代?一文详解ES6的迭代器与生成器 - 游魂博客-分享技术,资源共享

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

迭代器是一种有序、连续的、基于拉取的用于消耗数据的组织方式,用于以一次一步的方式控制行为。

简单的来说我们迭代循环一个可迭代对象,不是一次返回所有数据,而是调用相关方法分次进行返回。

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

{
  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访问到的属性。

模拟实现一个迭代器

基于迭代器协议

// 实现
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 方法

// 一个可迭代对象需要具有[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

// 生成器
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 函数里面,用在其他地方都会报错。

(function (){
  yield 1;
})()
// SyntaxError: Unexpected number

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

function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }

生成器传参

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

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())
image-20230712112350523

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

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

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()

image-20230712113415590

for of

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

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

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

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

尝试用 for or 循环数组

image-20230707172254195

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

image-20230707171931935

从图中我们能看出:

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

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

image-20230707172407016

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

obj[Symbol.iterator] = function* () {
  yield* this.name
}
image-20230707173001749

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

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] 这个属性。

// 用生成器生成
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 遍历的话,就有问题。

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

再来看看异步迭代器

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

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

for await (const item of obj) {
  console.log(item)
}

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

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那么优先使用同步迭代器。

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] 生成的同步迭代器
}

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK