JS `Symbol.asyncIterator`:自定义异步可迭代对象

各位听众,早上好/下午好/晚上好!今天咱们来聊聊一个可能你听过,但总觉得有点儿神秘的家伙:Symbol.asyncIterator。别担心,我会用最接地气的方式,把这个“异步迭代器”给扒个精光,保证你听完能上手写出自己的异步可迭代对象!

一、啥是迭代?先来个热身

在深入Symbol.asyncIterator之前,咱们先回顾一下迭代的概念。简单来说,迭代就是按顺序访问一个集合中的元素的过程。JavaScript中,我们通常用for...of循环来迭代数组、字符串、Set、Map等。

const myArray = [1, 2, 3];
for (const element of myArray) {
  console.log(element); // 输出 1, 2, 3
}

这里的myArray就是一个可迭代对象 (iterable)。它之所以能被for...of循环遍历,是因为它有一个Symbol.iterator属性,这个属性是一个函数,返回一个迭代器 (iterator)

迭代器是一个对象,它有一个next()方法,每次调用next()方法都会返回一个包含valuedone属性的对象。value表示当前迭代的值,done表示迭代是否完成(true表示完成,false表示未完成)。

const myArray = [1, 2, 3];
const iterator = myArray[Symbol.iterator]();

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 }

二、异步迭代:等等,数据还没来!

现在,想象一下,如果你的数据不是立刻就能拿到的,而是需要从网络请求、数据库查询等等异步操作中获取,那该怎么办? for...of循环可等不了你!

这就是Symbol.asyncIterator闪亮登场的时候了。它就是为异步数据流的迭代而生的。

三、Symbol.asyncIterator:异步迭代的钥匙

就像Symbol.iterator定义了同步可迭代对象的行为一样,Symbol.asyncIterator定义了异步可迭代对象 (async iterable) 的行为。

一个对象如果拥有一个Symbol.asyncIterator属性,并且该属性的值是一个函数,返回一个异步迭代器 (async iterator),那么它就是一个异步可迭代对象。

异步迭代器跟普通迭代器很像,但也有关键区别:

  • 它的next()方法返回的是一个 Promise
  • 它使用 for await...of 循环进行迭代。

四、代码说话:自定义异步可迭代对象

咱们来写一个例子,模拟一个从网络获取数据的异步可迭代对象。为了简化,我们不用真的发起网络请求,而是用setTimeout来模拟异步延迟。

class AsyncCounter {
  constructor(limit) {
    this.limit = limit;
    this.count = 0;
  }

  [Symbol.asyncIterator]() {
    return {
      next: async () => {
        if (this.count < this.limit) {
          await new Promise(resolve => setTimeout(resolve, 500)); // 模拟异步延迟
          this.count++;
          return { value: this.count, done: false };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
}

// 使用 for await...of 循环
async function main() {
  const counter = new AsyncCounter(3);
  for await (const value of counter) {
    console.log(value); // 输出 1, 2, 3 (每隔500ms)
  }
  console.log("Done!");
}

main();

代码详解:

  1. AsyncCounter 类: 这就是我们的异步可迭代对象。
  2. constructor(limit) 构造函数,接收一个limit参数,表示要迭代的最大次数。
  3. [Symbol.asyncIterator]() 关键所在!这个方法返回一个异步迭代器对象。
  4. next: async () => { ... } 异步迭代器的next()方法,注意它前面有async关键字,表示它是一个异步函数,返回一个Promise。
  5. await new Promise(resolve => setTimeout(resolve, 500)); 模拟异步延迟,让每次迭代之间有500毫秒的间隔。
  6. return { value: this.count, done: false }; 返回迭代的值和done状态。
  7. return { value: undefined, done: true }; 当迭代完成时,返回done: true
  8. async function main() { ... } 一个异步函数,用来演示如何使用for await...of循环来迭代异步可迭代对象。
  9. for await (const value of counter) { ... } for await...of循环,专门用来迭代异步可迭代对象。 注意 await 关键字,它会等待counter的每次next()方法返回的Promise resolve后,再执行循环体。

五、for await...of:异步迭代的正确姿势

for await...of循环是迭代异步可迭代对象的唯一正确方式。如果你尝试用普通的for...of循环来迭代异步可迭代对象,你会得到一个TypeError

// 错误示例:
const counter = new AsyncCounter(3);
try {
  for (const value of counter) { // TypeError: counter[Symbol.iterator] is not a function
    console.log(value);
  }
} catch (e) {
  console.error(e);
}

六、异步生成器函数:更简洁的写法

除了上面这种手动创建异步迭代器的方式,我们还可以使用异步生成器函数 (async generator function) 来更简洁地创建异步可迭代对象。

异步生成器函数跟普通生成器函数很像,但也有关键区别:

  • 函数声明前需要加上 async 关键字。
  • 可以使用 yield 关键字来产生异步值。
async function* asyncGenerator(limit) {
  for (let i = 1; i <= limit; i++) {
    await new Promise(resolve => setTimeout(resolve, 500));
    yield i;
  }
}

async function main() {
  const generator = asyncGenerator(3);
  for await (const value of generator) {
    console.log(value); // 输出 1, 2, 3 (每隔500ms)
  }
  console.log("Done!");
}

main();

代码详解:

  1. *`async function asyncGenerator(limit) { … }:** 异步生成器函数,注意async*` 关键字。
  2. yield i; yield 关键字产生一个异步值。 每次遇到yield,函数会暂停执行,并将i的值包装成一个Promise并resolve。
  3. const generator = asyncGenerator(3); 调用异步生成器函数会返回一个异步可迭代对象。

异步生成器函数让代码更简洁易懂,推荐使用。

七、实际应用场景:数据流处理、事件订阅

Symbol.asyncIterator 在处理异步数据流的场景下非常有用。 比如:

  • 读取大型文件: 可以逐行读取文件内容,而不用一次性加载整个文件到内存。
  • 处理服务器推送的数据流: 例如 WebSocket 连接,可以持续接收服务器推送的数据。
  • 事件订阅: 可以订阅特定的事件,并在事件发生时异步地处理。

示例:读取大型文件

const fs = require('fs');
const readline = require('readline');

async function* readLines(filePath) {
  const fileStream = fs.createReadStream(filePath);
  const rl = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity // 确保正确处理 Windows 换行符
  });

  for await (const line of rl) {
    yield line;
  }
}

async function main() {
  const filePath = 'large_file.txt'; // 替换成你的文件路径
  for await (const line of readLines(filePath)) {
    console.log(`Line: ${line}`);
  }
  console.log("File reading complete!");
}

main();

八、Symbol.asyncIterator vs Promise.allPromise.race

你可能会问,既然都能用Promise处理异步操作了,为什么还需要Symbol.asyncIteratorPromise.allPromise.race 也能处理多个Promise,但它们跟 Symbol.asyncIterator 有本质区别。

特性 Promise.all Promise.race Symbol.asyncIterator
处理方式 并行执行所有Promise,等待全部完成 并行执行所有Promise,只要有一个完成就返回 顺序执行异步操作,每次迭代等待上一次操作完成
结果 返回一个包含所有Promise结果的数组 返回第一个完成的Promise的结果 允许逐个访问异步产生的值,适合处理数据流
适用场景 所有Promise都需要完成才能进行下一步操作 只需要一个Promise完成即可进行下一步操作 需要按顺序处理异步数据流,例如读取大型文件、处理服务器推送的数据
内存占用 所有Promise的结果都需要保存在内存中 只需要保存第一个完成的Promise的结果 每次只保存一个迭代的值,内存占用更小,尤其适合处理大型数据流
取消/中断 难以取消或中断正在执行的Promise 难以取消或中断正在执行的Promise 可以通过提前退出循环来中断迭代

总结:

  • Promise.all 适合并行执行独立的异步操作,并需要等待所有操作完成。
  • Promise.race 适合并行执行异步操作,只需要一个操作完成即可。
  • Symbol.asyncIterator 适合按顺序处理异步数据流,可以逐个访问异步产生的值,并且可以中断迭代。

九、兼容性

Symbol.asyncIterator 是 ES2018 (ES9) 引入的特性。现代浏览器和 Node.js 版本都支持。如果需要兼容旧版本浏览器,可以使用 Babel 等工具进行转译。

十、总结

Symbol.asyncIterator 是处理异步数据流的利器。 掌握它,你可以:

  • 自定义异步可迭代对象,处理各种异步数据源。
  • 使用 for await...of 循环,轻松迭代异步数据流。
  • 使用异步生成器函数,更简洁地创建异步可迭代对象。

希望今天的讲座能让你对Symbol.asyncIterator有一个更清晰的认识。现在,轮到你动手实践了!去尝试用它来解决你遇到的实际问题吧!

本次讲座到此结束,谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注