JS `async` `Generator` 函数 (`async function*`):异步迭代器

各位靓仔靓女,老少爷们,大家好!我是你们的老朋友,今天咱们来聊聊 JavaScript 中一个有点酷,但可能又有点陌生的家伙——async Generator 函数。别怕,听我慢慢道来,保证让你听得懂,用得上,还能在小伙伴面前秀一把。

啥是 Generator?

在深入 async Generator 之前,咱们先回顾一下普通的 Generator 函数。它就像一个可以暂停和恢复执行的函数。想象一下,你正在读一本书,读到一半,突然想去喝杯咖啡,然后回来继续读。Generator 函数就能做到类似的事情。

定义 Generator 函数需要用到 function* 语法。在函数体内部,使用 yield 关键字来暂停函数的执行,并返回一个值。

function* numberGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

const generator = numberGenerator();

console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: undefined, done: true }

简单解释一下:

  • function* numberGenerator():定义了一个 Generator 函数。注意 * 号。
  • yield 1;:暂停函数执行,并返回 1 作为 valuedonefalse 表示迭代还未完成。
  • generator.next():调用 next() 方法会恢复函数的执行,直到遇到下一个 yield
  • 当所有 yield 都执行完毕,再次调用 next() 会返回 { value: undefined, done: true },表示迭代完成。

Generator 函数返回的是一个迭代器对象,可以使用 for...of 循环遍历。

function* evenNumbers(max) {
  for (let i = 0; i <= max; i += 2) {
    yield i;
  }
}

for (const number of evenNumbers(10)) {
  console.log(number); // 0 2 4 6 8 10
}

异步操作的痛点

在 JavaScript 中,异步操作非常常见,比如从服务器获取数据。通常我们会使用 Promise 或者 async/await 来处理异步操作。但是,如果需要在一个循环中进行多个异步操作,代码可能会变得比较复杂。

async function fetchAndProcessData(urls) {
  for (const url of urls) {
    try {
      const response = await fetch(url);
      const data = await response.json();
      // 处理数据
      console.log('Data:', data);
    } catch (error) {
      console.error('Error:', error);
    }
  }
}

const urls = [
  'https://jsonplaceholder.typicode.com/todos/1',
  'https://jsonplaceholder.typicode.com/todos/2',
  'https://jsonplaceholder.typicode.com/todos/3',
];

//fetchAndProcessData(urls); // 取消注释以运行
//注意:由于跨域问题,直接运行这段代码可能会报错。请在支持 CORS 的环境下运行,或者使用代理。

虽然 async/await 已经简化了异步代码,但如果我们需要更精细的控制,例如在每次异步操作后暂停执行,等待某个条件满足后再继续,async Generator 就派上用场了。

async Generator 函数:异步迭代的救星

async Generator 函数结合了 async/awaitGenerator 的优点,允许我们在 Generator 函数中使用 await 关键字,从而实现异步迭代。

定义 async Generator 函数需要用到 async function* 语法。

async function* asyncNumberGenerator() {
  yield await Promise.resolve(1);
  yield await Promise.resolve(2);
  yield await Promise.resolve(3);
}

async function main() {
  const generator = asyncNumberGenerator();

  console.log(await generator.next()); // { value: 1, done: false }
  console.log(await generator.next()); // { value: 2, done: false }
  console.log(await generator.next()); // { value: 3, done: false }
  console.log(await generator.next()); // { value: undefined, done: true }
}

main();

在这个例子中,asyncNumberGenerator 是一个 async Generator 函数。yield await Promise.resolve(1) 会暂停函数的执行,等待 Promise resolve 后,将结果 1 作为 value 返回。

async Generator 的优势

  • 简化异步迭代代码: 可以更清晰地表达异步迭代的逻辑,避免回调地狱。
  • 精细的控制: 可以在每次异步操作后暂停执行,等待特定条件满足后再继续。
  • 可读性更强: 代码结构更清晰,更容易理解和维护。

实际应用场景

  1. 分页加载数据: 从服务器分批加载数据,每次加载一部分,并显示在页面上。

    async function* fetchPagedData(url, pageSize) {
      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;
        } else {
          yield data;
          page++;
        }
      }
    }
    
    async function displayData() {
      const dataGenerator = fetchPagedData('https://jsonplaceholder.typicode.com/posts', 10); // 假设API支持分页
    
      for await (const page of dataGenerator) {
        // 处理每一页的数据
        console.log('Page data:', page);
        // 将数据渲染到页面上
      }
      console.log('All data loaded.');
    }
    
    //displayData(); // 取消注释以运行
    //注意:由于跨域问题,直接运行这段代码可能会报错。请在支持 CORS 的环境下运行,或者使用代理。

    在这个例子中,fetchPagedData 函数会从服务器分页加载数据,每次加载 pageSize 条数据。displayData 函数使用 for await...of 循环遍历 dataGenerator,处理每一页的数据。 for await...of 专门用于异步迭代器。

  2. 处理 WebSocket 流: 从 WebSocket 连接接收数据流,并进行处理。

    async function* processWebSocketStream(socket) {
      try {
        while (true) {
          const message = await new Promise((resolve, reject) => {
            socket.onmessage = (event) => {
              resolve(event.data);
            };
            socket.onerror = (error) => {
              reject(error);
            };
          });
          yield message;
        }
      } catch (error) {
        console.error('WebSocket error:', error);
      } finally {
        socket.close();
      }
    }
    
    async function handleWebSocketMessages() {
      const socket = new WebSocket('wss://echo.websocket.events'); // 使用一个公共的 WebSocket echo 服务
    
      socket.onopen = async () => {
        console.log('WebSocket connected.');
        const messageProcessor = processWebSocketStream(socket);
    
        for await (const message of messageProcessor) {
          console.log('Received message:', message);
          // 处理消息
          socket.send(`Server received: ${message}`); // Echo back to the server
        }
        console.log('WebSocket closed.');
      };
    
      socket.onclose = () => {
        console.log('WebSocket disconnected.');
      };
    }
    
    //handleWebSocketMessages(); // 取消注释以运行

    在这个例子中,processWebSocketStream 函数会从 WebSocket 连接接收消息,并使用 yield 将消息发送出去。handleWebSocketMessages 函数使用 for await...of 循环遍历 messageProcessor,处理每一条消息。

  3. 处理大型文件: 读取大型文件,并逐行处理。

    async function* processFile(file) {
      const fileStream = file.stream();
      const decoder = new TextDecoder();
      let reader = fileStream.getReader();
    
      try {
        while (true) {
          const { done, value } = await reader.read();
          if (done) {
            break;
          }
          const chunk = decoder.decode(value);
          const lines = chunk.split('n');
    
          for (const line of lines) {
            yield line;
          }
        }
      } finally {
        reader.releaseLock();
      }
    }
    
    async function handleFileProcessing(file) {
      const lineProcessor = processFile(file);
    
      for await (const line of lineProcessor) {
        console.log('Line:', line);
        // 处理每一行
      }
      console.log('File processing complete.');
    }
    
    // 使用示例 (需要一个 File 对象)
    // const fileInput = document.getElementById('fileInput');
    // fileInput.addEventListener('change', async (event) => {
    //   const file = event.target.files[0];
    //   await handleFileProcessing(file);
    // });

    在这个例子中,processFile 函数会读取文件,并将每一行作为 yield 的值发送出去。handleFileProcessing 函数使用 for await...of 循环遍历 lineProcessor,处理每一行。

async Generator 的使用场景总结

为了方便大家理解,我们用表格总结一下 async Generator 常见的应用场景:

应用场景 描述
分页加载数据 从服务器分批加载数据,每次加载一部分,并在页面上显示。可以使用 async Generator 函数来简化分页加载的逻辑,避免回调地狱。
处理 WebSocket 流 从 WebSocket 连接接收数据流,并进行处理。async Generator 函数可以方便地处理 WebSocket 消息,并在接收到消息后暂停执行,等待特定条件满足后再继续。
处理大型文件 读取大型文件,并逐行处理。async Generator 函数可以逐行读取文件内容,并在读取每一行后暂停执行,从而避免一次性将整个文件加载到内存中,导致内存溢出。
任何需要异步迭代的场景 任何需要异步迭代的场景都可以考虑使用 async Generator 函数。例如,从数据库中逐条读取数据,或者从多个 API 接口并发获取数据,并进行处理。

async Generator 的注意事项

  • for await...of 循环: 只能用于遍历 async Generator 函数返回的异步迭代器。
  • 错误处理: 可以使用 try...catch 语句来处理异步操作中的错误。
  • 兼容性: async Generator 函数是 ES2018 的新特性,需要注意浏览器的兼容性。如果需要支持旧版本的浏览器,可以使用 Babel 等工具进行转换。
  • throw 方法: 迭代器对象还支持 throw 方法,可以向 Generator 函数内部抛出一个错误。

    async function* errorGenerator() {
      try {
        yield 1;
        yield 2;
        yield 3;
      } catch (error) {
        console.error('Error caught:', error);
      }
    }
    
    async function main() {
      const generator = errorGenerator();
    
      console.log(await generator.next()); // { value: 1, done: false }
      console.log(await generator.throw(new Error('Something went wrong!'))); // Error caught: Error: Something went wrong!  { value: undefined, done: true }
      console.log(await generator.next()); // { value: undefined, done: true }
    }
    
    main();

    在这个例子中,generator.throw(new Error('Something went wrong!')) 会向 errorGenerator 函数内部抛出一个错误,错误会被 try...catch 语句捕获。之后,迭代器就完成了,再调用 next() 就会返回 { value: undefined, done: true }

  • return 方法: 迭代器对象还支持 return 方法,可以提前结束 Generator 函数的执行。

    async function* returnGenerator() {
      yield 1;
      yield 2;
      yield 3;
    }
    
    async function main() {
      const generator = returnGenerator();
    
      console.log(await generator.next()); // { value: 1, done: false }
      console.log(await generator.return('Early exit!')); // { value: 'Early exit!', done: true }
      console.log(await generator.next()); // { value: undefined, done: true }
    }
    
    main();

    在这个例子中,generator.return('Early exit!') 会提前结束 returnGenerator 函数的执行,并返回 { value: 'Early exit!', done: true }

总结

async Generator 函数是 JavaScript 中一个强大的工具,可以简化异步迭代代码,提高代码的可读性和可维护性。虽然它可能看起来有点复杂,但只要掌握了它的基本概念和使用方法,就能在实际开发中发挥巨大的作用。

希望今天的讲座能帮助你更好地理解 async Generator 函数。下次有机会,我们再聊聊其他有趣的 JavaScript 特性。 拜拜!

发表回复

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