异步编程进阶: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() 方法返回一个包含 valuedone 两个属性的对象。
    • value: yield 表达式产出的值,或者是 return 语句返回的值。
    • done: 布尔值,表示 Generator 函数是否已经执行完毕 (true 表示已完成,false 表示未完成)。

3. yield 关键字详解

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

  • 向外传值 : yield expressionexpression 的值发送给外部调用 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. 语义更清晰 : asyncawait 的语义比 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 语言规范的一部分,无需额外库。
    • 调试友好:在调试器中单步跟踪异步代码流更加直观。

相关推荐
Mores9 分钟前
开源 | ImageMinify:轻量级智能图片压缩工具,为你的项目瘦身加速
前端
CHQIUU10 分钟前
PDF.js 生态中如何处理“添加注释\添加批注”以及 annotations.contents 属性
开发语言·javascript·pdf
执梦起航11 分钟前
webpack理解与使用
前端·webpack·node.js
ai大师11 分钟前
Cursor怎么使用,3分钟上手Cursor:比ChatGPT更懂需求,用聊天的方式写代码,GPT4、Claude 3.5等先进LLM辅助编程
前端
Json_14 分钟前
使用vue2技术写了一个纯前端的静态网站商城-鲜花销售商城
前端·vue.js·html
1024熙14 分钟前
【Qt】——理解信号与槽,学会使用connect
前端·数据库·c++·qt5
少糖研究所15 分钟前
ColorThief库是如何实现图片取色的?
前端
冴羽16 分钟前
SvelteKit 最新中文文档教程(22)—— 最佳实践之无障碍与 SEO
前端·javascript·svelte
ZYLAB18 分钟前
我写了一个简易的 SEO 教程,希望能让新手朋友看完以后, SEO 能做到 80 分
前端·seo
小桥风满袖24 分钟前
Three.js-硬要自学系列4 (阵列立方体和相机适配、常见几何体、高光材质、lil-gui库)
前端·css