解锁 JavaScript 异步:Generator 与 Async/Await 核心解析

Promise 通过链式调用改善了回调地狱的问题,但面对复杂异步流程时,Promise 仍存在提升空间。本文是在对事件循环、Promise 探讨的基础上,聚焦于更高层抽象工具 Generator 和 Async/Await 的讲解,帮助开发者更全面地掌握 JavaScript 异步编程。

一、Generator 函数

为何需要 Generator 函数

在异步编程中,复杂的异步流程需要更灵活的控制方式。普通函数一旦开始执行就会一直运行到结束,无法暂停和恢复,对于异步操作的中间过程难以进行精细的控制,难以满足异步任务分步执行、按需生成数据等需求。Generator 函数提供了一种可中断和恢复执行的机制,为异步编程带来了新的思路。

定义方式

Generator 函数通过在function关键字后添加*来定义,例如:

javascript 复制代码
function* myGenerator() {
    // 函数体
}

这里的myGenerator就是一个 Generator 函数。

  • function*这种特殊的声明方式告诉 JavaScript 引擎,该函数具有可暂停和恢复执行的特殊行为。
  • 与普通函数定义function myFunction() {... }相比,*是 Generator 函数的标志性符号。

yield 关键字的关键作用

暂停执行与返回值 :Generator 函数在执行过程中,一旦遇到 yield,其执行流程就会在此处中断。

  • yield 右侧的值作为本次调用next() 返回对象的 value 属性,是 Generator 函数向外部输出数据的方式。

  • yield 表达式自身的返回值由下一次 next() 传入的参数决定,是外部代码向 Generator 函数内部输入数据的方式。

恢复执行与数据交互: 当 Generator 函数被调用时,会返回一个迭代器对象。该迭代器对象包含以下内容:

  • next方法 :用于控制 Generator 函数的执行。

    • 每次调用该方法,都会返回一个包含 valuedone 属性的对象。value 属性表示当前 yieldreturn 语句所产出的值,done 属性则是一个布尔值,用于指示 Generator 函数是否执行完毕。

    • 每次调用 next 方法,Generator 函数就会从上次暂停的位置(首次调用则从开始位置)继续执行,直至遇到下一个 yield 语句或者 return 语句。

    • 遇到 yield 语句时,函数暂停执行,next 方法返回的对象中,valueyield 右侧表达式的值,donefalse

    • 遇到 return 语句时,函数执行完毕,next 方法返回的对象中,valuereturn 后面表达式的值,donetrue

  • done属性 :用于标识 Generator 函数的执行状态。

    • 当 Generator 函数执行到return语句,或者执行到函数末尾且没有更多yield语句时,done属性值为true;否则为false

如果next方法传递了参数,这个参数会作为上一个yield语句的返回值,实现 Generator 函数与外部调用者之间的数据交互。

javascript 复制代码
function* fruitGenerator() {
    let firstFruit = yield '请选择第一种水果';
    let secondFruit = yield `你选择了 ${firstFruit},请选择第二种水果`;
    return `你选择的水果组合是 ${firstFruit} 和 ${secondFruit}`;
}

let fruitGen = fruitGenerator();
let firstPrompt = fruitGen.next();
console.log(firstPrompt.value); 

let firstChoice = fruitGen.next('苹果');
console.log(firstChoice.value); 

let secondChoice = fruitGen.next('香蕉');
console.log(secondChoice.value); 
  • 创建 Generator 对象

    • 调用 fruitGenerator() 函数并不会立即执行函数体中的代码,而是返回一个 Generator 对象 fruitGen。这个对象可以用来控制 Generator 函数的执行。
  • 第一次调用 next 方法

    • 当调用 fruitGen.next() 时,fruitGenerator 函数开始执行。

    • 函数执行到 yield '请选择第一种水果' 时暂停,yield 右侧的字符串 '请选择第一种水果' 会作为这次 next() 方法返回对象的 value 属性值。

    • firstPrompt 接收 next() 方法的返回值,它是一个对象,格式为 { value: '请选择第一种水果', done: false }

  • 第二次调用 next 方法 :让函数从暂停处继续执行, next() 传入的 '苹果' 就作为上一个 yield 表达式(即 yield '请选择第一种水果' )的返回值,这个返回值就会赋给 firstFruit

  • 第三次调用 next 方法 : 函数从第二个 yield 处继续执行,'香蕉' 作为上一个 yield 返回值赋给 secondFruit,函数执行 return 语句,返回 '你选择的水果组合是 苹果 和 香蕉'

Generator 函数与普通函数在执行上的本质区别

普通函数的执行模式 :普通函数一旦被调用,就会从函数的第一行代码开始,按照顺序依次执行,直到遇到return语句或者函数体结束。在执行过程中,它不会暂停,也无法在中途将执行权交还给调用者,并且其内部的变量作用域和状态在函数执行结束后就会被销毁(除了闭包情况)。例如:

javascript 复制代码
function add(a, b) {
    let result = a + b;
    return result;
}
let sum = add(2, 3);
console.log(sum); 

add函数被调用后,立即计算a + b,然后返回结果,整个过程一气呵成,没有中间暂停的可能。

Generator 函数的执行模式 :Generator 函数在调用后,并不会立即执行函数体中的代码,而是返回一个迭代器对象。当调用这个迭代器对象的next()方法时,Generator 函数才开始执行,直到遇到yield语句。

应用场景

异步任务分步执行

在处理异步操作时,Generator 函数可以将异步任务分成多个步骤,每个步骤通过yield暂停,等待异步操作完成后再通过next()方法恢复执行,使异步代码看起来更像同步代码。

  • 复杂异步流程控制:当有多个异步操作,且它们之间存在顺序依赖关系时,比如先读取配置文件,再根据配置信息建立数据库连接,最后查询数据库。
  • 异步操作并发限制 :有些场景下,需要限制同时进行的异步操作数量,以避免资源过度消耗。例如,同时向多个服务器发送请求获取数据,但服务器对并发请求数有限制。
    • 使用 Generator 函数可以每次yield一个请求操作,控制并发数量,当一个请求完成后,再通过next方法发送下一个请求。
  • 处理异步操作结果的中间过程 :如果在异步操作过程中,需要对中间结果进行处理或转换,然后再基于处理后的结果进行下一步异步操作,分步执行就很有必要。
    • 比如先从 API 获取原始数据,然后对数据进行清洗和格式化,再将处理后的数据保存到数据库中。通过 Generator 函数,可以方便地在yield之间插入对中间结果的处理逻辑。
  • 用户交互与异步操作结合 :在与用户交互的场景中,可能需要根据用户的输入或操作来决定下一步的异步操作。
    • 例如,在一个表单提交过程中,先验证用户输入的合法性(异步操作),如果合法,再根据用户选择的不同选项执行不同的异步保存操作。

惰性生成序列值

Generator 函数可以按需生成一系列的值,非常适合生成斐波那契数列、无限数据流等场景。以生成斐波那契数列为例:

javascript 复制代码
function* fibonacci() {
    let a = 0, b = 1;
    while (true) {
        yield a;
        [a, b] = [b, a + b];
    }
}
let fib = fibonacci();
console.log(fib.next().value); 

fibonacci函数通过yield按需生成斐波那契数列的每一项,避免一次性生成大量数据,节省内存。

二、Async/Await

为何需要 Async/Await

尽管 Promise 在一定程度上改善了异步编程体验,但链式调用在处理复杂异步流程时仍显繁琐。Async/Await 语法基于 Promise 和 Generator 进一步简化异步代码,让开发者可以用更接近同步代码的方式编写异步逻辑,同时优化了错误处理机制。

Async/Await 定义与使用

async 函数语法格式

  • async 函数用于定义一个异步函数,它的定义方式和普通函数类似,不过要在 function 关键字前面加上 async 关键字。其基本语法如下:
javascript 复制代码
// 函数声明方式
async function functionName(parameters) {
    // 函数体
    return value;
}

// 函数表达式方式
const functionName = async function(parameters) {
    // 函数体
    return value;
};

// 箭头函数方式
const functionName = async (parameters) => {
    // 函数体
    return value;
};

await 关键字用法

await 关键字只能在 async 函数内部使用,它的作用是暂停 async 函数的执行,等待其后跟随的 Promise 对象被解决(resolved)或被拒绝(rejected)。

  • Promise 被解决时,await 表达式会返回 Promise 的解决值。
  • Promise 被拒绝时,await 表达式会抛出异常。
javascript 复制代码
function asyncOperation() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('操作完成');
        }, 1000);
    });
}

async function main() {
    try {
        // await 暂停执行,等待 asyncOperation 返回的 Promise 被解决
        let result = await asyncOperation();
        console.log(result); // 输出: 操作完成
    } catch (error) {
        console.error('操作出错:', error.message);
    }
}

main();

asyncOperation 函数返回一个 Promiseawait asyncOperation() 会暂停 main 函数的执行,直到 asyncOperation 返回的 Promise 被解决,然后将解决值赋给 result 变量并继续执行后续代码。如果 Promise 被拒绝,await 会抛出异常,被 try...catch 捕获。

返回值特性

  • async 函数会返回一个 Promise 对象。

  • 返回值包装

    • 成功返回非 Promise :若函数内使用 return 语句返回一个非 Promise 的值,JavaScript 会自动把这个值包装成一个已解决(resolved)状态的 Promise 对象,其 value 属性就是返回的值。

      • 这意味着当调用这个 async 函数时,返回的 Promise 对象会立即进入已解决状态,并且可以通过 .then() 方法获取到返回的值。
    • 成功返回 Promise :若返回一个 Promise 对象,那么 async 函数返回的就是这个 Promise 对象本身。

      • Promise 对象的状态和结果取决于返回的 Promise 的执行情况。如果这个 Promise 被解决,async 函数返回的 Promise 也会进入已解决状态;如果被拒绝,则进入被拒绝状态。
    • 抛出异常 :若函数内部抛出异常,返回的 Promise 对象会处于被拒绝(rejected)状态,异常信息会作为 Promise 的拒绝原因。可以通过 .catch() 方法捕获这个异常信息。

示例 1:返回非 Promise

js 复制代码
async function sayHello() {
    return "Hello, World!";
}

// 调用 async 函数
const promise = sayHello();

// 打印返回的 Promise 对象
console.log(promise);

// 使用 .then 方法处理 Promise 的结果
promise.then((message) => {
    console.log(message);
});

示例 2:返回 Promise

js 复制代码
function asyncOperation() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('操作完成');
        }, 1000);
    });
}

async function returnPromise() {
    return asyncOperation();
}

(async () => {
    let result = await returnPromise();
    console.log(result);
})();

Async/Await 与 Generator、Promise 的关系

  • Async/Await 本质上是基于 Promise 和 Generator 的语法糖。它借助 Promise 处理异步操作的状态和结果,利用 Generator 函数可暂停恢复执行的特性,通过自动执行器实现无需手动调用next()的效果。
  • 与 Generator 函数相比,Async/Await 内置了错误冒泡机制,使得错误处理更加便捷。在async函数中,可直接使用try...catch捕获并处理await表达式抛出的异常,而无需像 Generator 函数那样在每次调用next()时手动检查错误。

Async/Await 的优势

代码更简洁易读

  • 普通Promise :在处理多个异步操作时,Promise通过链式调用.then () 来处理异步操作的结果,当有多个异步操作依赖时,代码会形成多层嵌套,可读性差。
javascript 复制代码
getData()
    .then(result1 => {
        return getAnotherData(result1.id)
            .then(result2 => {
                return getThirdData(result2.name)
                    .then(result3 => {
                        console.log(result3);
                    });
            });
    });
  • async/awaitasync/await使异步代码看起来更像同步代码,通过await暂停异步函数的执行,等待Promise被解决,让代码更清晰直观。
javascript 复制代码
async function getDataAndLog() {
    const result1 = await getData();
    const result2 = await getAnotherData(result1.id);
    const result3 = await getThirdData(result2.name);
    console.log(result3);
}
getDataAndLog();

错误处理更方便

  • 普通Promise :在Promise链中,错误处理需要在每个.then()方法中添加.catch(),或者在链的末尾统一添加.catch(),但如果中间某个.then()方法中返回了一个新的Promise且没有正确处理错误,可能会导致错误被忽略。
javascript 复制代码
getData()
    .then(result1 => {
        // 这里可能会抛出错误,但没有及时处理
        return getAnotherData(result1.id);
    })
    .catch(error => {
        console.error('全局错误处理:', error);
    });
  • async/awaitasync/await可以使用try...catch块来捕获错误,更加直观和方便,错误处理代码更集中。
javascript 复制代码
async function getDataAndLog() {
    try {
        const result1 = await getData();
        const result2 = await getAnotherData(result1.id);
        console.log(result2);
    } catch (error) {
        console.error('错误处理:', error);
    }
}
getDataAndLog();

Async/Await 的应用

表单提交与验证

  • 在处理表单提交时,通常需要验证用户输入,并在验证通过后将数据发送到服务器。async/await可以将验证和提交操作异步化,使代码更有条理。例如:
javascript 复制代码
async function submitForm(formData) {
    // 验证表单数据
    if (!formData.email.match(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/)) {
        throw new Error('无效的电子邮件地址');
    }

    // 发送表单数据到服务器
    const response = await fetch('https://example.com/api/submit-form', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(formData)
    });

    if (!response.ok) {
        throw new Error('表单提交失败');
    }

    return await response.json();
}

const formData = { email: '[email protected]', password: '123456' };
submitForm(formData).then(result => console.log(result));

数据获取与处理

  • 当从服务器获取数据并需要对数据进行进一步处理时,async/await可以让代码更清晰。例如,从 API 获取用户信息,然后根据用户信息获取用户的订单列表:
javascript 复制代码
async function getUserOrders() {
    // 获取用户信息
    const userResponse = await fetch('https://example.com/api/user');
    const userData = await userResponse.json();

    // 根据用户ID获取订单列表
    const ordersResponse = await fetch(`https://example.com/api/orders/${userData.id}`);
    const ordersData = await ordersResponse.json();

    return ordersData;
}
getUserOrders().then(result => console.log(result));
相关推荐
喜樂的CC11 分钟前
[react]Next.js之自适应布局和高清屏幕适配解决方案
javascript·react.js·postcss
天天扭码24 分钟前
零基础 | 入门前端必备技巧——使用 DOM 操作插入 HTML 元素
前端·javascript·dom
软件测试曦曦30 分钟前
16:00开始面试,16:08就出来了,问的问题有点变态。。。
自动化测试·软件测试·功能测试·程序人生·面试·职场和发展
咖啡虫1 小时前
css中的3d使用:深入理解 CSS Perspective 与 Transform-Style
前端·css·3d
烛阴1 小时前
手把手教你搭建 Express 日志系统,告别线上事故!
javascript·后端·express
拉不动的猪1 小时前
设计模式之------策略模式
前端·javascript·面试
旭久1 小时前
react+Tesseract.js实现前端拍照获取/选择文件等文字识别OCR
前端·javascript·react.js
独行soc1 小时前
2025年常见渗透测试面试题-红队面试宝典下(题目+回答)
linux·运维·服务器·前端·面试·职场和发展·csrf
uhakadotcom2 小时前
Google Earth Engine 机器学习入门:基础知识与实用示例详解
前端·javascript·面试
麓殇⊙2 小时前
Vue--组件练习案例
前端·javascript·vue.js