异步编程进阶:Generator 与 Async/Await

异步编程进阶:Generator 与 Async/Await

前言

本文将深入了解 ES6 引入的 Generator 函数和 ES2017 (ES7) 引入的 async/await 语法。这两种方式为解决异步流程控制提供了更优雅的方案。

开篇思考

  1. 调用 Generator 函数 (function*) 后,立即返回的是什么?
  2. async/await 相比 Promise 和 Generator 的优势体现在哪里?

一、Generator 函数 (ES6/ES2015)

1. 什么是 Generator 函数?

Generator 函数是一种特殊的函数,其定义形式为 function* funcName()。它允许函数在执行过程中暂停,并在稍后恢复执行。这种暂停和恢复的能力是其用于异步编程的基础。它通过 yield 关键字来控制暂停点。

2. 基本语法与执行

  • 定义 : 使用 function* 关键字。
  • 暂停与恢复 : 使用 yield 关键字暂停执行并产出一个值。使用 Generator 对象(调用 Generator 函数的返回值)的 .next() 方法恢复执行。

代码示例:

javascript 复制代码
// 1. 定义一个 Generator 函数
function* myGenerator() {
  console.log('Generator started...');
  const value1 = yield 'Result 1'; // 暂停点 1
  console.log('Received from next (1):', value1);
  const value2 = yield 'Result 2'; // 暂停点 2
  console.log('Received from next (2):', value2);
  console.log('Generator finished.');
  return 'Final Result'; // 函数最终返回值
}

// 2. 调用 Generator 函数 -> 不会立即执行,返回一个 Generator 对象(迭代器)
const generatorObject = myGenerator();
console.log('Generator object created.');

// 3. 启动/恢复执行:调用 .next()
console.log('Calling next() #1');
let result1 = generatorObject.next(); // 执行到第一个 yield,暂停
console.log(result1); // { value: 'Result 1', done: false }

console.log('\nCalling next() #2 with argument "Hello"');
let result2 = generatorObject.next('Hello'); // 恢复执行,'Hello' 成为上一个 yield 表达式的返回值 (value1)
                                            // 执行到第二个 yield,暂停
console.log(result2); // { value: 'Result 2', done: false }

console.log('\nCalling next() #3 with argument "World"');
let result3 = generatorObject.next('World'); // 恢复执行,'World' 成为上一个 yield 表达式的返回值 (value2)
                                            // 执行到函数末尾 (return)
console.log(result3); // { value: 'Final Result', done: true }

console.log('\nCalling next() #4 after completion');
let result4 = generatorObject.next(); // 函数已完成,再次调用 next 无效
console.log(result4); // { value: undefined, done: true }

关键点总结:

  1. 惰性执行 :调用 Generator 函数 (myGenerator()) 时,函数体内的代码 不会 立即执行,而是返回一个迭代器(Iterator)对象,也称为 Generator 对象。
  2. next() 驱动执行 :每次调用 Generator 对象的 .next(value) 方法时,函数会从上次暂停的地方(或函数开头)开始执行,直到遇到下一个 yield 语句或 return 语句。
  3. yield 暂停与产出 :yield 关键字会暂停函数执行,并将 yield 后面的表达式的值作为返回的迭代器结果对象的 value 属性。
  4. .next() 传值 :.next(value) 方法可以向 Generator 函数内部传递一个值,这个值会成为 上一个 yield 表达式的返回值。首次调用 next() 时传递的参数会被忽略。
  5. 返回结果 :.next() 方法返回一个包含 value 和 done 两个属性的对象。
    • value: yield 表达式产出的值,或者是 return 语句返回的值。
    • done: 布尔值,表示 Generator 函数是否已经执行完毕 (true 表示已完成,false 表示未完成)。

3. yield 关键字详解

yield 是 Generator 函数的核心。它不仅用于暂停和返回值,还构成了 Generator 函数与外部调用者之间的双向通信桥梁。

  • 向外传值 : yield expression 将 expression 的值发送给外部调用 next() 的代码。
  • 从外接收值 : const receivedValue = yield expression;,下一次调用 next(inputValue) 时,inputValue 会赋值给 receivedValue。

4. yield*:委托给另一个 Generator 或可迭代对象

yield* 表达式用于将执行权委托给另一个 Generator 函数或任何可迭代对象(如数组、字符串、Map、Set 等)。

代码示例:

javascript 复制代码
function* generatorA() {
  yield 'A1';
  yield 'A2';
}

function* generatorB() {
  yield 'B1';
  yield* generatorA(); // 委托给 generatorA
  yield* [ 'B2', 'B3' ]; // 委托给数组
  yield 'B4';
}

const genB = generatorB();

console.log(genB.next().value); // B1
console.log(genB.next().value); // A1 (来自 generatorA)
console.log(genB.next().value); // A2 (来自 generatorA)
console.log(genB.next().value); // B2 (来自数组)
console.log(genB.next().value); // B3 (来自数组)
console.log(genB.next().value); // B4
console.log(genB.next().done);  // true

二、Generator 与异步编程

Generator 本身是同步的,但其暂停/恢复的特性使其非常适合用来管理异步流程,以同步化的代码风格编写异步逻辑。

1. 面临的问题

直接使用 Generator 处理异步,需要手动在异步操作完成后调用 .next() 来恢复 Generator 的执行。这依然比较繁琐。

2. Thunk 函数(辅助工具)

概念 :Thunk 函数是一种高阶函数,它接收一些参数,并返回一个新的、无参数的函数。这个新函数封装了(或"延迟"了)一个计算或操作。在异步场景下,常用于将 Node.js 风格的回调函数(callback(err, data))转换成只接受一个回调函数的 Thunk 函数。

示例(简化版):

javascript 复制代码
// 一个简单的 Thunk 函数创建器:封装带回调的异步操作
function createThunk(asyncFunction, ...args) {
  return function(callback) { // 返回的这个函数就是 Thunk 函数
    args.push(callback); // 将真正的回调函数添加到参数列表末尾
    asyncFunction.apply(null, args); // 执行原始异步函数
  };
}

// 模拟一个异步读取文件的函数 (Node.js fs.readFile 风格)
function fakeReadFile(fileName, callback) {
  console.log(`Reading ${fileName}...`);
  setTimeout(() => {
    if (fileName === 'file1.txt') {
      callback(null, 'Content of file 1'); // 成功:error 为 null
    } else {
      callback(new Error(`File not found: ${fileName}`), null); // 失败:error 对象
    }
  }, 500);
}

// 使用 createThunk 包装 fakeReadFile
const readFileThunk = (fileName) => createThunk(fakeReadFile, fileName);

// 使用 Thunk
const thunk1 = readFileThunk('file1.txt');
thunk1((err, data) => { // 执行 Thunk 函数,传入最终的回调
  if (err) {
    console.error('Error reading file:', err);
  } else {
    console.log('Thunk Result:', data);
  }
});

Thunk 在 Generator 异步流程中的作用 :yield 一个 Thunk 函数,然后在 Thunk 的回调中调用 Generator 的 next() 方法,将结果传回。

3. Generator + Thunk 实现异步控制

javascript 复制代码
function* asyncFlowWithThunks() {
  try {
    console.log('Starting async flow...');
    const result1 = yield readFileThunk('file1.txt'); // yield Thunk
    console.log('File 1 content:', result1);
    const result2 = yield readFileThunk('file2.txt'); // yield another Thunk
    console.log('File 2 content:', result2); // 这行不会执行,因为 file2.txt 会出错
  } catch (err) {
    console.error('Caught error in Generator:', err.message);
  }
  console.log('Async flow finished.');
}

// 手动执行器(简化版)
function runThunkGenerator(generatorFunc) {
  const generator = generatorFunc(); // 创建 Generator 对象

  function nextStep(err, data) { // Thunk 的回调函数,用于驱动 Generator
    if (err) {
      // 如果 Thunk 返回错误,将错误 throw 进 Generator
      return generator.throw(err);
    }
    // 将数据传回 Generator,并获取下一个 yield 的结果
    const result = generator.next(data);

    if (result.done) {
      // Generator 执行完毕
      return;
    }

    // 如果 yield 的是 Thunk 函数,则执行它,并将 nextStep 作为回调
    if (typeof result.value === 'function') {
      result.value(nextStep); // 执行 Thunk
    } else {
      // 如果 yield 的不是 Thunk,可以继续执行或抛错
       console.warn('Yielded a non-thunk value:', result.value);
       nextStep(null, result.value); // 简单处理,直接把值传回去
    }
  }

  // 启动执行流程
  nextStep(null, undefined); // 初始调用
}

// 运行
runThunkGenerator(asyncFlowWithThunks);

这种手动执行器解决了异步流程控制,但如果嵌套层级深,代码依然复杂。

4. Generator + Promise 实现异步控制

Promise 提供了更标准的异步处理方式。Generator 可以 yield Promise 对象,执行器等待 Promise resolve 后再调用 next()。

javascript 复制代码
// 模拟返回 Promise 的异步函数
function readFilePromise(fileName) {
  return new Promise((resolve, reject) => {
    console.log(`Reading ${fileName} (Promise)...`);
    setTimeout(() => {
      if (fileName === 'file1.txt') {
        resolve(`Promise Content of ${fileName}`);
      } else {
        reject(new Error(`File not found (Promise): ${fileName}`));
      }
    }, 500);
  });
}

function* asyncFlowWithPromises() {
  try {
    console.log('Starting async flow (Promises)...');
    const result1 = yield readFilePromise('file1.txt'); // yield Promise
    console.log('File 1 content:', result1);
    const result2 = yield readFilePromise('file2.txt'); // yield another Promise
    console.log('File 2 content:', result2); // 这行不会执行
  } catch (err) {
    console.error('Caught error in Generator (Promise):', err.message);
  }
   console.log('Async flow (Promises) finished section.');
   return "All done successfully!"; // Final return value if no error
}

// 基于 Promise 的 Generator 执行器 (简化版)
function runPromiseGenerator(generatorFunc) {
    const generator = generatorFunc();

    function handleResult(result) {
        if (result.done) {
            // Generator 完成,返回最终结果 (包装在 Promise 中)
            return Promise.resolve(result.value);
        }

        // 确保 yield 的值是 Promise
        const promise = Promise.resolve(result.value); // 非 Promise 会被包装

        return promise.then(
            res => handleResult(generator.next(res)), // 成功,将结果传回,继续下一步
            err => handleResult(generator.throw(err))  // 失败,将错误 throw 进 Generator
        );
    }

    // 启动流程
    try {
        return handleResult(generator.next());
    } catch (err) {
        return Promise.reject(err); // 处理 Generator 内部同步错误
    }
}

// 运行
runPromiseGenerator(asyncFlowWithPromises)
  .then(finalValue => console.log("Generator Runner Promise Resolved:", finalValue)) // Logs if generator finishes without uncaught error
  .catch(err => console.error("Generator Runner Promise Rejected:", err.message)); // Logs if the runner catches an error

这个基于 Promise 的执行器是 async/await 实现的基础。


三、co 函数库 (历史性工具)

目的 : co 是 TJ Holowaychuk 创建的一个著名库,用于 自动执行 Generator 函数。它内部封装了类似上面 runPromiseGenerator 的逻辑,使得开发者无需手动编写执行器。

核心原理:

  1. 接收一个 Generator 函数作为参数。
  2. 内部调用 Generator 函数获取迭代器对象。
  3. 自动调用 next(),并处理 yield 出来的 Promise 或 Thunk。
  4. 等待 Promise/Thunk 完成后,将结果/错误传回给 Generator 的下一次 next() 或 throw()。
  5. 重复此过程直到 Generator 执行完毕 (done: true)。
  6. co 函数本身返回一个 Promise,该 Promise 在 Generator 成功完成时 resolve,在 Generator 内部出错时 reject。

使用示例:

javascript 复制代码
// 假设已安装 co: npm install co
const co = require('co'); // 在 Node.js 环境
// 在现代浏览器或打包环境中,可能需要 import co from 'co';

// 使用上面的 asyncFlowWithPromises Generator 函数
co(asyncFlowWithPromises)
  .then(finalResult => {
    console.log('co execution successful, final value:', finalResult);
  })
  .catch(err => {
    console.error('co caught an error:', err.message);
  });

现状 : co 库在 async/await 普及后,其使用场景已大大减少。async/await 提供了原生、更简洁的语法来完成同样的目标。


四、async/await (ES2017/ES7) - 终极解决方案

async/await 是基于 Promise 和 Generator 构建的语法糖,旨在以更接近同步代码的方式书写异步逻辑。

1. 基本语法

  • async关键字:用于声明一个函数是异步函数。异步函数会自动将其返回值包装成一个 Promise 对象。
  • await关键字:只能在 async 函数内部使用。它会暂停 async 函数的执行,等待 await 右侧的 Promise 对象变为 resolved 状态,然后将 Promise 的解决值作为 await 表达式的结果继续执行。如果 Promise 被 rejected,await 会抛出这个 rejection 的错误(可以被 try...catch 捕获)。

2. async/await 重写示例

javascript 复制代码
// 使用上面返回 Promise 的 readFilePromise 函数

async function mainAsyncAwait() {
  console.log('Starting async/await flow...');
  try {
    const result1 = await readFilePromise('file1.txt'); // 等待 Promise resolve
    console.log('File 1 content (await):', result1);

    const result2 = await readFilePromise('file2.txt'); // 等待 Promise resolve
    console.log('File 2 content (await):', result2); // 这行不会执行

    console.log('Async/await flow finished successfully.'); // 也不会执行
    return "All tasks completed via async/await!"; // 函数的返回值会被包装成 Promise

  } catch (err) {
    console.error('Caught error in async function:', err.message);
    // 即使捕获了错误,async 函数仍然会返回一个 rejected 的 Promise
    // 如果希望函数正常结束(返回 resolved Promise),可以在 catch 中 return 一个值
    return "Finished with errors handled.";
  }
}

// 调用 async 函数
mainAsyncAwait()
  .then(finalMsg => {
    console.log('Async function promise resolved with:', finalMsg);
  })
  .catch(err => {
    // 通常 async 函数内部的 catch 会处理错误,这里不会触发,除非 async 函数本身抛出未捕获异常
    console.error('Async function promise rejected:', err);
  });

3. async/await 对比 Generator + co 的优势

  1. 内置执行器 : async 函数自带执行器,无需像 Generator 那样依赖 co 或手动编写执行器。调用 async 函数就像调用普通函数一样简单。
  2. 适用性更广 : await 关键字后面理论上可以跟任何值。如果不是 Promise,该值会被自动包装成一个 resolved 的 Promise (Promise.resolve(value))。而 co 通常要求 yield 的是 Promise 或 Thunk。
  3. 语义更清晰 : async 和 await 的语义比 function* 和 yield 更直接地表达了异步操作的意图,代码可读性更好。
  4. 原生语法支持: 作为 ECMAScript 标准的一部分,无需引入任何第三方库。
  5. 错误处理 : 使用标准的 try...catch 语句捕获 await 操作中的同步和异步错误,非常自然。

4. async 函数的返回值

  • async 函数总是隐式地返回一个 Promise。
  • 如果 async 函数内部使用 return value;,则返回的 Promise 会以 value resolve。
  • 如果 async 函数内部抛出未捕获的错误,则返回的 Promise 会以该错误 reject。
  • 如果没有 return 语句,则返回的 Promise 会以 undefined resolve。

代码示例:

javascript 复制代码
async function simpleAsync() {
  console.log("Inside async function");
  return 42; // 返回值会被包装
}

async function errorAsync() {
    throw new Error("Something went wrong");
}

const promise1 = simpleAsync();
console.log(promise1); // Promise { <pending> } (之后会 resolve 为 42)
promise1.then(value => console.log('simpleAsync resolved:', value)); // 42

const promise2 = errorAsync();
console.log(promise2); // Promise { <pending> } (之后会 reject)
promise2.catch(err => console.error('errorAsync rejected:', err.message)); // Something went wrong

五、异步编程方式对比总结

特性 回调函数 (Callback Hell) Promise (ES6) Generator + Runner/co async/await (ES2017)
核心 函数嵌套调用 Promise 对象链 (.then) Generator + 执行器 async 函数 + await
写法风格 嵌套回调 链式调用 近似同步 (需执行器) 近似同步 (原生)
可读性 差 较好 好 最好
错误处理 分散在各回调参数 .catch() / try...catch try...catch (需执行器支持) try...catch (原生)
控制流 复杂 线性/并行 (Promise.all) 线性 线性
依赖 无 原生 (ES6+) 原生 Generator + 库/手动 原生 (ES2017+)
现代推荐 避免 基础,常与async/await结合 不常用 推荐

六、回答开篇思考

  1. 调用 Generator 函数 (function*) 后,立即返回的是什么? 答:调用 Generator 函数并不会立即执行函数体内的代码,而是立即返回一个 Generator 对象(迭代器 Iterator) 。你需要通过调用这个对象的 .next() 方法来启动或恢复函数的执行。

  2. async/await 相比 Promise 和 Generator 的优势体现在哪里? 答:async/await 的主要优势在于:

    • 语法简洁,可读性极高:代码看起来非常像同步代码,易于理解和维护。
    • 内置执行器 :不需要像 Generator 那样依赖 co 库或手动编写执行器来自动处理异步流程。
    • 错误处理自然 :可以使用标准的 try...catch 语句捕获 await 等待的 Promise 的 rejection,以及函数内的同步错误。
    • 原生标准:是 ECMAScript 语言规范的一部分,无需额外库。
    • 调试友好:在调试器中单步跟踪异步代码流更加直观。

相关推荐
FungLeo2 小时前
成为全栈·React 管理后台篇·总复盘:从“接口能调通”到“后台值得使用”
前端·react.js·前端框架·项目复盘·成为全栈
Csvn4 小时前
并发模式:让渲染学会排队、插队和让路
前端
可乐鸡翅yeah_4 小时前
新手梳理:M3U8 线上问题,哪些是前端锅,哪些是后端锅
前端·ios·音视频·实时音视频·m3u8·音视频在线播放
Flynt4 小时前
Linear 用 1000 个 PR 换掉 styled-components,我写了 200 个按钮,把这笔账复现了一遍
前端·css·preact
JavaGuide5 小时前
NVIDIA 又开源了!这次给 AI Agent 加上权限管控
前端·后端
excel5 小时前
prisma 如何处理数据库竞态
前端·数据库·后端
莪_幻尘6 小时前
Skill 体检:30 个 Skill 全凭感觉?体检器先自曝了 8 个“假 0 分
前端·人工智能·llm
风骏时光牛马6 小时前
AI模型综合能力评测:性能、指令遵循与多场景实测对比
前端
Frag0ut7 小时前
Chrome与Chromium内核浏览器在Windows 11上的新特性全景解析
前端·chrome·windows·web安全·chromium·gemini ai·playready drm
hiahiahia1237 小时前
实现完整 Tool Dispatcher
开发语言·前端