JS `for await…of` 循环:遍历异步可迭代对象

大家好!欢迎来到今天的异步JavaScript探险之旅,今天我们要聊的是一个超级酷炫的循环结构:for await...of。 准备好了吗?我们要开始咯!

第一章:什么是异步可迭代对象?

在开始 for await...of 的旅程之前,我们得先搞清楚它的“食物”——异步可迭代对象。 想象一下,你有个快递员,他送的不是普通的包裹,而是需要时间才能准备好的“异步包裹”。 这些“异步包裹”可能需要从服务器下载数据,或者等待某个耗时的操作完成。

那什么是可迭代对象呢? 简单来说,就是可以用 for...of 循环遍历的对象。 它必须有一个 Symbol.iterator 方法,这个方法返回一个迭代器对象。 迭代器对象又必须有一个 next() 方法,每次调用 next() 方法,它会返回一个包含 valuedone 属性的对象。 value 是当前迭代的值,done 是一个布尔值,表示是否迭代完成。

异步可迭代对象,顾名思义,就是把这个过程异步化了。 它的 Symbol.asyncIterator 方法返回一个异步迭代器对象。 异步迭代器对象也有一个 next() 方法,但是这个 next() 方法返回一个 Promise。 这个 Promise resolve 的值,才包含 valuedone 属性。

是不是有点绕? 没关系,我们用代码来解释一下:

// 一个简单的异步可迭代对象
const asyncIterable = {
  [Symbol.asyncIterator]() {
    let i = 0;
    return {
      async next() {
        await new Promise(resolve => setTimeout(resolve, 500)); // 模拟异步操作
        if (i < 3) {
          return { value: i++, done: false };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
};

// 测试一下
(async () => {
  const iterator = asyncIterable[Symbol.asyncIterator]();
  let result = await iterator.next();
  while (!result.done) {
    console.log(result.value);
    result = await iterator.next();
  }
  console.log('Done!');
})();

在这个例子中,asyncIterable 是一个异步可迭代对象。 它的 Symbol.asyncIterator 方法返回一个异步迭代器。 异步迭代器的 next() 方法返回一个 Promise,这个 Promise 在 500 毫秒后 resolve,resolve 的值是包含 valuedone 属性的对象。

第二章:for await...of 的闪亮登场

现在,我们有了异步可迭代对象,终于可以请出今天的主角了:for await...of 循环!

for await...of 循环专门用来遍历异步可迭代对象。 它可以自动等待每个 Promise resolve,然后取出 value 值。 这样,我们就可以像遍历普通数组一样,遍历异步可迭代对象了。

还是上面的例子,我们可以用 for await...of 循环来简化代码:

const asyncIterable = {
  [Symbol.asyncIterator]() {
    let i = 0;
    return {
      async next() {
        await new Promise(resolve => setTimeout(resolve, 500)); // 模拟异步操作
        if (i < 3) {
          return { value: i++, done: false };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
};

(async () => {
  for await (const value of asyncIterable) {
    console.log(value);
  }
  console.log('Done!');
})();

是不是感觉清爽多了? for await...of 循环帮我们处理了 Promise 的等待和 done 属性的判断,让我们专注于处理每个 value 值。

第三章:for await...of 的使用场景

for await...of 循环在处理异步数据流时非常有用。 想象一下,你需要从一个 API 逐页获取数据,或者从一个 WebSocket 连接接收实时数据。 这些场景都涉及到异步操作,使用 for await...of 循环可以让我们更优雅地处理这些数据。

我们来看几个具体的例子:

1. 从 API 逐页获取数据:

假设我们有一个 API,每次只能返回 10 条数据,我们需要分页获取所有数据。 可以这样实现:

async function* getPagedData(url, pageSize = 10) {
  let page = 1;
  let hasMore = true;

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

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

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

    page++;
  }
}

(async () => {
  const dataStream = getPagedData('https://api.example.com/data'); // 替换成你的API地址
  for await (const item of dataStream) {
    console.log(item);
  }
  console.log('All data fetched!');
})();

在这个例子中,getPagedData 是一个异步生成器函数。 它使用 yield 关键字来逐个返回数据,而不是一次性返回所有数据。 这样可以减少内存占用,并且可以更早地开始处理数据。

2. 从 WebSocket 连接接收实时数据:

假设我们有一个 WebSocket 连接,需要实时接收服务器推送的数据。 可以这样实现:

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

  ws.onmessage = (event) => {
    // 注意: 这里不能直接 yield event.data, 因为 onmessage 不是 async 函数
    // 需要用 resolve 包裹
    return new Promise(resolve => {
      resolve(event.data);
    });
  };

  ws.onclose = () => {
    console.log('WebSocket connection closed.');
  };

  ws.onerror = (error) => {
    console.error('WebSocket error:', error);
  };

  // 监听open事件
  ws.onopen = () => {
    console.log("WebSocket connected")
  }

  while (true) {
    // 关键: 模拟一个异步迭代, 等待数据
    const message = await new Promise(resolve => {
      ws.onmessage = (event) => {
        resolve(event.data);
      };
    });
    yield message;
  }
}

(async () => {
  try {
    const dataStream = createWebSocketStream('wss://echo.websocket.events'); // 替换成你的WebSocket地址
    for await (const message of dataStream) {
      console.log('Received message:', message);
    }
  } catch (error) {
    console.error('Error processing WebSocket stream:', error);
  }
})();

这个例子稍微复杂一些,因为 WebSocket 的 onmessage 事件是同步的,不能直接在里面使用 yield 关键字。 我们需要用 Promise 来包装 onmessage 事件,然后才能在 while 循环中使用 await 关键字。

3. 读取大型文件:

假设我们需要读取一个大型文件,但是不想一次性把整个文件加载到内存中。 可以使用 Node.js 的 readline 模块和 for await...of 循环来实现:

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

async function* readFileByLine(filePath) {
  const fileStream = fs.createReadStream(filePath);

  const rl = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity // 识别所有换行符 (CR LF, CR, 和 LF)
  });

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

(async () => {
  const filePath = 'large_file.txt'; // 替换成你的文件路径

  try {
    const lineStream = readFileByLine(filePath);
    let lineNumber = 1;
    for await (const line of lineStream) {
      console.log(`Line ${lineNumber}: ${line}`);
      lineNumber++;
    }
    console.log('File reading complete!');
  } catch (error) {
    console.error('Error reading file:', error);
  }
})();

在这个例子中,readFileByLine 函数使用 readline 模块逐行读取文件,并使用 yield 关键字逐行返回。 这样可以避免一次性加载整个文件到内存中,从而提高性能。

第四章:for await...of 的注意事项

虽然 for await...of 循环很方便,但是也有一些需要注意的地方:

  • 必须在 async 函数中使用: for await...of 循环只能在 async 函数中使用。 如果在非 async 函数中使用,会报错。
  • 只能遍历异步可迭代对象: for await...of 循环只能遍历异步可迭代对象。 如果遍历普通的可迭代对象,会报错。
  • 错误处理:for await...of 循环中,如果异步操作抛出错误,循环会立即停止,并抛出错误。 可以使用 try...catch 语句来处理错误。
  • 性能考虑: 虽然 for await...of 循环很方便,但是每次迭代都需要等待 Promise resolve,所以性能可能不如普通的 for...of 循环。 在性能敏感的场景中,需要仔细考虑是否使用 for await...of 循环。

第五章:for await...of vs. for...of + await

你可能会问,既然可以用 for await...of 循环遍历异步可迭代对象,那为什么不直接用 for...of 循环 + await 关键字呢?

// 使用 for...of + await 的方式
(async () => {
  const asyncIterable = {
    [Symbol.asyncIterator]() {
      let i = 0;
      return {
        async next() {
          await new Promise(resolve => setTimeout(resolve, 500)); // 模拟异步操作
          if (i < 3) {
            return { value: i++, done: false };
          } else {
            return { value: undefined, done: true };
          }
        }
      };
    }
  };

  const iterator = asyncIterable[Symbol.asyncIterator]();
  for (let i = 0; i < 3; i++) {
    const result = await iterator.next();
    console.log(result.value);
  }
  console.log('Done!');
})();

虽然这种方式也能达到相同的效果,但是它有以下几个缺点:

  • 代码更冗长: 需要手动调用 next() 方法,并且需要手动判断 done 属性。
  • 可读性更差: 代码逻辑更复杂,不容易理解。
  • 容易出错: 容易忘记 await 关键字,或者忘记判断 done 属性。

for await...of 循环可以简化代码,提高可读性,减少出错的可能性。

第六章:总结

for await...of 循环是一个强大的工具,可以让我们更优雅地处理异步数据流。 它可以简化代码,提高可读性,减少出错的可能性。 在处理异步数据时,不妨考虑一下 for await...of 循环,也许它能给你带来惊喜。

特性 for await...of for...of + await
适用对象 异步可迭代对象 普通可迭代对象,但需要手动处理异步操作
代码简洁度 更简洁,自动处理 Promisedone 属性 更冗长,需要手动处理 Promisedone 属性
可读性 更高,更易于理解异步迭代的逻辑 较低,需要手动处理异步操作,逻辑更复杂
错误处理 自动传播 Promise 的 rejected 状态,可以使用 try...catch 需要手动处理 Promise 的 rejected 状态,容易出错
性能 每次迭代都需要等待 Promise resolve,可能稍慢 如果不需要异步操作,性能可能更高,但异步操作的性能取决于实现方式
是否必须在 async 函数中使用 否,但如果使用 await 关键字,则必须在 async 函数中使用

希望今天的探险之旅对你有所帮助。 记住,编程就像探险,只有不断尝试,才能发现新的宝藏! 祝你编程愉快!

发表回复

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