中文

探索 JavaScript 异步迭代器助手,彻底改变流处理。学习如何使用 map、filter、take、drop 等高效处理异步数据流。

JavaScript 异步迭代器助手:为现代应用提供强大的流处理能力

在现代 JavaScript 开发中,处理异步数据流是一项常见需求。无论您是从 API 获取数据、处理大文件还是处理实时事件,高效地管理异步数据都至关重要。JavaScript 的异步迭代器助手提供了一种强大而优雅的方式来处理这些流,为数据操作提供了一种函数式和可组合的方法。

什么是异步迭代器和异步可迭代对象?

在深入了解异步迭代器助手之前,让我们先理解其基本概念:异步迭代器(Async Iterators)和异步可迭代对象(Async Iterables)。

异步可迭代对象 (Async Iterable) 是一个定义了如何异步遍历其值的对象。它通过实现 @@asyncIterator 方法来做到这一点,该方法返回一个异步迭代器 (Async Iterator)

异步迭代器 (Async Iterator) 是一个提供 next() 方法的对象。此方法返回一个 promise,该 promise 会解析为一个包含两个属性的对象:

这里有一个简单的例子:


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 500)); // 模拟一个异步操作
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  for await (const value of asyncIterable) {
    console.log(value); // 输出:1, 2, 3, 4, 5(每项之间有 500 毫秒延迟)
  }
})();

在此示例中,generateSequence 是一个异步生成器函数,它异步地生成一个数字序列。for await...of 循环用于消费来自异步可迭代对象的值。

介绍异步迭代器助手

异步迭代器助手扩展了异步迭代器的功能,提供了一套用于转换、过滤和操作异步数据流的方法。它们支持函数式和可组合的编程风格,使得构建复杂的数据处理管道变得更加容易。

核心的异步迭代器助手包括:

让我们通过示例来探索每个助手。

map()

map() 助手使用提供的函数转换异步可迭代对象的每个元素。它返回一个包含转换后值的新异步可迭代对象。


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

const doubledIterable = asyncIterable.map(x => x * 2);

(async () => {
  for await (const value of doubledIterable) {
    console.log(value); // 输出:2, 4, 6, 8, 10(有 100 毫秒延迟)
  }
})();

在此示例中,map(x => x * 2) 将序列中的每个数字加倍。

filter()

filter() 助手根据提供的条件(谓词函数)从异步可迭代对象中选择元素。它返回一个仅包含满足条件的元素的新异步可迭代对象。


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(10);

const evenNumbersIterable = asyncIterable.filter(x => x % 2 === 0);

(async () => {
  for await (const value of evenNumbersIterable) {
    console.log(value); // 输出:2, 4, 6, 8, 10(有 100 毫秒延迟)
  }
})();

在此示例中,filter(x => x % 2 === 0) 仅从序列中选择偶数。

take()

take() 助手从异步可迭代对象中返回前 N 个元素。它返回一个仅包含指定数量元素的新异步可迭代对象。


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

const firstThreeIterable = asyncIterable.take(3);

(async () => {
  for await (const value of firstThreeIterable) {
    console.log(value); // 输出:1, 2, 3(有 100 毫秒延迟)
  }
})();

在此示例中,take(3) 从序列中选择前三个数字。

drop()

drop() 助手跳过异步可迭代对象的前 N 个元素并返回其余部分。它返回一个包含剩余元素的新异步可迭代对象。


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

const afterFirstTwoIterable = asyncIterable.drop(2);

(async () => {
  for await (const value of afterFirstTwoIterable) {
    console.log(value); // 输出:3, 4, 5(有 100 毫秒延迟)
  }
})();

在此示例中,drop(2) 跳过序列中的前两个数字。

toArray()

toArray() 助手消费整个异步可迭代对象并将所有元素收集到一个数组中。它返回一个 promise,该 promise 会解析为一个包含所有元素的数组。


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const numbersArray = await asyncIterable.toArray();
  console.log(numbersArray); // 输出:[1, 2, 3, 4, 5]
})();

在此示例中,toArray() 将序列中的所有数字收集到一个数组中。

forEach()

forEach() 助手为异步可迭代对象中的每个元素执行一次提供的函数。它*不*返回新的异步可迭代对象,而是执行具有副作用的函数。这对于执行日志记录或更新 UI 等操作非常有用。


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(3);

(async () => {
  await asyncIterable.forEach(value => {
    console.log("Value:", value);
  });
  console.log("forEach completed");
})();
// 输出:Value: 1, Value: 2, Value: 3, forEach completed

some()

some() 助手测试异步可迭代对象中是否至少有一个元素通过了由提供的函数实现的测试。它返回一个 promise,该 promise 解析为一个布尔值(如果至少有一个元素满足条件,则为 true,否则为 false)。


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const hasEvenNumber = await asyncIterable.some(x => x % 2 === 0);
  console.log("Has even number:", hasEvenNumber); // 输出:Has even number: true
})();

every()

every() 助手测试异步可迭代对象中的所有元素是否都通过了由提供的函数实现的测试。它返回一个 promise,该 promise 解析为一个布尔值(如果所有元素都满足条件,则为 true,否则为 false)。


async function* generateSequence(end) {
  for (let i = 2; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(4);

(async () => {
  const areAllEven = await asyncIterable.every(x => x % 2 === 0);
  console.log("Are all even:", areAllEven); // 输出:Are all even: true
})();

find()

find() 助手返回异步可迭代对象中满足所提供测试函数的第一个元素。如果没有值满足测试函数,则返回 undefined。它返回一个 promise,该 promise 解析为找到的元素或 undefined


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const firstEven = await asyncIterable.find(x => x % 2 === 0);
  console.log("First even number:", firstEven); // 输出:First even number: 2
})();

reduce()

reduce() 助手对异步可迭代对象的每个元素按顺序执行用户提供的“reducer”回调函数,并将前一个元素计算的返回值传入。在所有元素上运行 reducer 的最终结果是单个值。它返回一个 promise,该 promise 解析为最终的累积值。


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const sum = await asyncIterable.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
  console.log("Sum:", sum); // 输出:Sum: 15
})();

实际示例与用例

异步迭代器助手在多种场景中都很有价值。让我们探讨一些实际示例:

1. 处理来自流式 API 的数据

假设您正在构建一个实时数据可视化仪表板,该仪表板从流式 API 接收数据。API 持续发送更新,您需要处理这些更新以显示最新信息。


async function* fetchDataFromAPI(url) {
  let response = await fetch(url);

  if (!response.body) {
    throw new Error("ReadableStream not supported in this environment");
  }

  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) {
        break;
      }
      const chunk = decoder.decode(value);
      // 假设 API 发送由换行符分隔的 JSON 对象
      const lines = chunk.split('\n');
      for (const line of lines) {
        if (line.trim() !== '') {
          yield JSON.parse(line);
        }
      }
    }
  } finally {
    reader.releaseLock();
  }
}

const apiURL = 'https://example.com/streaming-api'; // 替换为您的 API URL
const dataStream = fetchDataFromAPI(apiURL);

// 处理数据流
(async () => {
  for await (const data of dataStream.filter(item => item.type === 'metric').map(item => ({ timestamp: item.timestamp, value: item.value }))) {
    console.log('Processed Data:', data);
    // 使用处理后的数据更新仪表盘
  }
})();

在此示例中,fetchDataFromAPI 从流式 API 获取数据,解析 JSON 对象,并将其作为异步可迭代对象产生。filter 助手仅选择指标数据,map 助手在更新仪表板之前将数据转换为所需格式。

2. 读取和处理大文件

假设您需要处理一个包含客户数据的大型 CSV 文件。您可以使用异步迭代器助手逐块处理它,而不是将整个文件加载到内存中。


async function* readLinesFromFile(filePath) {
  const file = await fsPromises.open(filePath, 'r');

  try {
    let buffer = Buffer.alloc(1024);
    let fileOffset = 0;
    let remainder = '';

    while (true) {
      const { bytesRead } = await file.read(buffer, 0, buffer.length, fileOffset);
      if (bytesRead === 0) {
        if (remainder) {
          yield remainder;
        }
        break;
      }

      fileOffset += bytesRead;
      const chunk = buffer.toString('utf8', 0, bytesRead);
      const lines = chunk.split('\n');

      lines[0] = remainder + lines[0];
      remainder = lines.pop() || '';

      for (const line of lines) {
        yield line;
      }
    }
  } finally {
    await file.close();
  }
}

const filePath = './customer_data.csv'; // 替换为您的文件路径
const lines = readLinesFromFile(filePath);

// 处理行数据
(async () => {
  for await (const customerData of lines.drop(1).map(line => line.split(',')).filter(data => data[2] === 'USA')) {
    console.log('Customer from USA:', customerData);
    // 处理来自美国的客户数据
  }
})();

在此示例中,readLinesFromFile 逐行读取文件,并将每一行作为异步可迭代对象产生。drop(1) 助手跳过标题行,map 助手将行拆分为列,filter 助手仅选择来自美国的客户。

3. 处理实时事件

异步迭代器助手也可用于处理来自 WebSocket 等源的实时事件。您可以创建一个异步可迭代对象,在事件到达时发出事件,然后使用助手处理这些事件。


async function* createWebSocketStream(url) {
  const ws = new WebSocket(url);

  yield new Promise((resolve, reject) => {
      ws.onopen = () => {
          resolve();
      };
      ws.onerror = (error) => {
          reject(error);
      };
  });

  try {
    while (ws.readyState === WebSocket.OPEN) {
      yield new Promise((resolve, reject) => {
        ws.onmessage = (event) => {
          resolve(JSON.parse(event.data));
        };
        ws.onerror = (error) => {
          reject(error);
        };
        ws.onclose = () => {
           resolve(null); // 连接关闭时解析为 null
        }
      });

    }
  } finally {
    ws.close();
  }
}

const websocketURL = 'wss://example.com/events'; // 替换为您的 WebSocket URL
const eventStream = createWebSocketStream(websocketURL);

// 处理事件流
(async () => {
  for await (const event of eventStream.filter(event => event.type === 'user_login').map(event => ({ userId: event.userId, timestamp: event.timestamp }))) {
    console.log('User Login Event:', event);
    // 处理用户登录事件
  }
})();

在此示例中,createWebSocketStream 创建一个异步可迭代对象,用于发出从 WebSocket 接收的事件。filter 助手仅选择用户登录事件,map 助手将数据转换为所需格式。

使用异步迭代器助手的好处

浏览器和运行时支持

异步迭代器助手在 JavaScript 中仍是一个相对较新的功能。截至 2024 年底,它们处于 TC39 标准化过程的第 3 阶段,这意味着它们很可能在不久的将来被标准化。但是,并非所有浏览器和 Node.js 版本都原生支持它们。

浏览器支持:现代浏览器如 Chrome、Firefox、Safari 和 Edge 正在逐步增加对异步迭代器助手的支持。您可以在 Can I use... 等网站上查看最新的浏览器兼容性信息,以了解哪些浏览器支持此功能。

Node.js 支持:最新版本的 Node.js(v18 及以上)提供了对异步迭代器助手的实验性支持。要使用它们,您可能需要使用 --experimental-async-iterator 标志运行 Node.js。

Polyfills:如果您需要在不原生支持异步迭代器助手的环境中使用它们,您可以使用 polyfill。polyfill是一段提供缺失功能的代码。有几个可用于异步迭代器助手的 polyfill 库;一个流行的选择是 core-js 库。

实现自定义异步迭代器

虽然异步迭代器助手提供了一种处理现有异步可迭代对象的便捷方式,但有时您可能需要创建自己的自定义异步迭代器。这使您能够以流式方式处理来自各种来源(如数据库、API 或文件系统)的数据。

要创建自定义异步迭代器,您需要在对象上实现 @@asyncIterator 方法。此方法应返回一个具有 next() 方法的对象。next() 方法应返回一个 promise,该 promise 解析为一个具有 valuedone 属性的对象。

这是一个从分页 API 获取数据的自定义异步迭代器的示例:


async function* fetchPaginatedData(baseURL) {
  let page = 1;
  let hasMore = true;

  while (hasMore) {
    const url = `${baseURL}?page=${page}`;
    const response = await fetch(url);
    const data = await response.json();

    if (data.results.length === 0) {
      hasMore = false;
      break;
    }

    for (const item of data.results) {
      yield item;
    }

    page++;
  }
}

const apiBaseURL = 'https://api.example.com/data'; // 替换为您的 API URL
const paginatedData = fetchPaginatedData(apiBaseURL);

// 处理分页数据
(async () => {
  for await (const item of paginatedData) {
    console.log('Item:', item);
    // 处理项目
  }
})();

在此示例中,fetchPaginatedData 从分页 API 获取数据,在检索到每个项目时将其产生。异步迭代器处理分页逻辑,使其易于以流式方式消费数据。

潜在的挑战与注意事项

虽然异步迭代器助手提供了许多好处,但了解一些潜在的挑战和注意事项也很重要:

使用异步迭代器助手的最佳实践

要充分利用异步迭代器助手,请考虑以下最佳实践:

高级技巧

组合自定义助手

您可以通过组合现有助手或从头开始构建新助手来创建自己的自定义异步迭代器助手。这使您可以根据特定需求定制功能并创建可重用组件。


async function* takeWhile(asyncIterable, predicate) {
  for await (const value of asyncIterable) {
    if (!predicate(value)) {
      break;
    }
    yield value;
  }
}

// 示例用法:
async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(10);
const firstFive = takeWhile(asyncIterable, x => x <= 5);

(async () => {
  for await (const value of firstFive) {
    console.log(value);
  }
})();

组合多个异步可迭代对象

您可以使用 zipmerge 等技术将多个异步可迭代对象组合成一个单一的异步可迭代对象。这使您可以同时处理来自多个来源的数据。


async function* zip(asyncIterable1, asyncIterable2) {
    const iterator1 = asyncIterable1[Symbol.asyncIterator]();
    const iterator2 = asyncIterable2[Symbol.asyncIterator]();

    while (true) {
        const result1 = await iterator1.next();
        const result2 = await iterator2.next();

        if (result1.done || result2.done) {
            break;
        }

        yield [result1.value, result2.value];
    }
}

// 示例用法:
async function* generateSequence1(end) {
    for (let i = 1; i <= end; i++) {
        yield i;
    }
}

async function* generateSequence2(end) {
    for (let i = 10; i <= end + 9; i++) {
        yield i;
    }
}

const iterable1 = generateSequence1(5);
const iterable2 = generateSequence2(5);

(async () => {
    for await (const [value1, value2] of zip(iterable1, iterable2)) {
        console.log(value1, value2);
    }
})();

结论

JavaScript 异步迭代器助手提供了一种强大而优雅的方式来处理异步数据流。它们为数据操作提供了一种函数式和可组合的方法,使得构建复杂的数据处理管道变得更加容易。通过理解异步迭代器和异步可迭代对象的核心概念并掌握各种助手方法,您可以显著提高异步 JavaScript 代码的效率和可维护性。随着浏览器和运行时支持的不断增长,异步迭代器助手有望成为现代 JavaScript 开发人员的重要工具。