欢迎光临
我们一直在努力

JavaScript核心异步编程解决方案(Promise)

一、Promise核心概念

  • 什么是Promise?

    Promise是ES6引入的异步编程解决方案,用于解决传统回调函数嵌套(回调地狱)的问题。

    你可以把它理解为一个"承诺":

    • 承诺在未来某个时间点会给出结果(成功/失败)。

    • 一旦结果确定,就不会再改变。

    • 可以通过链式调用替代嵌套回调。

  • Promise的三种状态。

    Promise有且仅有三种状态,状态一旦改变就不可逆:

    • pending(进行中):初始状态。

    • fulfilled(已成功):异步操作完成。

    • rejected(已失败):异步操作出错。

  • 基本语法。

    // 创建Promise实例。
    const promise = new Promise((resolve, reject) => {
    // 异步操作(比如接口请求、文件读取)。
    setTimeout(() => {
    const success = true; // 模拟操作结果。
    if (success) {
    resolve("操作成功"); // 成功时调用,状态变为fulfilled。
    } else {
    reject("操作失败"); // 失败时调用,状态变为rejected。
    }
    }, 1000);
    });

    // 消费Promise。
    promise
    .then((result) => {
    // 成功回调:接收resolve的参数。
    console.log("成功:", result);
    })
    .catch((error) => {
    // 失败回调:接收reject的参数。
    console.log("失败:", error);
    })
    .finally(() => {
    // 无论成功/失败都会执行(ES2018新增)。
    console.log("操作结束");
    });

    // 多个then、catch时,链式调用该如何写?详情请参考本章第72.5节。

    代码解释:

    • new Promise()接收一个执行器函数,参数是resolve(成功回调)和reject(失败回调)。

    • then()处理成功状态,catch()处理失败状态,finally()处理收尾操作。

    • 执行器函数会立即执行,异步操作的结果通过resolve/reject传递。

  • 二、Promise核心方法

  • 链式调用(解决回调地狱)。

    // 传统回调地狱(嵌套)。
    ajax("url1", (res1) => {
    ajax("url2?data=" + res1, (res2) => {
    ajax("url3?data=" + res2, (res3) => {
    console.log(res3);
    });
    });
    });

    // Promise链式调用(扁平化)。
    function request(url) {
    return new Promise((resolve) => {
    ajax(url, (res) => resolve(res));
    });
    }

    request("url1")
    .then((res1) => request("url2?data=" + res1))
    .then((res2) => request("url3?data=" + res2))
    .then((res3) => console.log(res3))
    .catch((err) => console.log(err));

    // 每个then不返回值或返回值是非Promise值时怎么办?详情请参考本章第72.6节。

  • 常用静态方法。

    方法作用适用场景
    Promise.resolve(value) 快速创建一个已成功的Promise。 同步值转异步、统一返回值类型。
    Promise.reject(reason) 快速创建一个已失败的Promise。 主动抛出异常。
    Promise.all([p1,p2,p3]) 所有Promise都成功才成功,一个失败就失败。 并行请求多个接口,需全部返回后处理。
    Promise.race([p1,p2,p3]) 第一个完成(无论成功 / 失败)的Promise决定结果。 超时控制(比如请求5秒未响应则中断)。
    Promise.allSettled([p1,p2,p3]) 所有Promise都完成(无论成败),返回所有结果。 并行请求,需知道每个请求的结果(成功 / 失败)。
    Promise.any([p1,p2,p3]) 第一个成功的Promise决定结果,全部失败才失败。 多源请求(比如多个CDN地址,取第一个可用的)。

    示例:Promise.all实战。

    // 并行请求3个接口。
    const p1 = fetch("/api/user");
    const p2 = fetch("/api/order");
    const p3 = fetch("/api/goods");

    Promise.all([p1, p2, p3])
    .then((responses) => {
    // 所有请求成功,处理结果。
    return Promise.all(responses.map(res => res.json()));
    })
    .then(([user, order, goods]) => {
    console.log("用户数据:", user);
    console.log("订单数据:", order);
    console.log("商品数据:", goods);
    })
    .catch((err) => {
    // 任意一个请求失败,触发catch
    console.log("请求失败:", err);
    });

    示例:Promise.race 实现超时控制。

    // 封装超时函数。
    function timeout(delay) {
    return new Promise((_, reject) => {
    setTimeout(() => reject(new Error("请求超时")), delay);
    });
    }

    // 请求接口,5秒超时。
    function fetchWithTimeout(url) {
    return Promise.race([
    fetch(url),
    timeout(5000)
    ]);
    }

    // 使用。
    fetchWithTimeout("/api/data")
    .then(res => res.json())
    .then(data => console.log(data))
    .catch(err => console.log(err)); // 5秒未响应则输出"请求超时"。

  • 三、Promise实战场景

  • 封装异步API(比如定时器/文件读取)。

    // 封装setTimeout为Promise。
    function delay(ms) {
    return new Promise((resolve) => {
    setTimeout(resolve, ms);
    });
    }

    // 使用。
    delay(2000).then(() => {
    console.log("2秒后执行");
    });

    // 封装Node.js文件读取。
    const fs = require("fs");
    function readFile(path) {
    return new Promise((resolve, reject) => {
    fs.readFile(path, "utf8", (err, data) => {
    if (err) reject(err);
    else resolve(data);
    });
    });
    }

    // 使用。
    readFile("./test.txt")
    .then(data => console.log(data))
    .catch(err => console.log(err));

    /*
    根据宏任务/微任务机制,
    使用Nodejs文件读取时,异步的是Promise.then而不是Promise中的函数,
    文件读取时依旧会阻塞主进程吗?

    详情请参考本章第72.9节。
    */

  • 结合async / await(Promise语法糖)。

    async / await是ES2017引入的,基于Promise实现,让异步代码看起来像同步代码:

    // 基础用法。
    async function getData() {
    try {
    // 等待Promise完成。
    const res = await fetch("/api/data");
    const data = await res.json();
    console.log(data);
    } catch (err) {
    // 捕获所有异常(等价于catch)。
    console.log("请求失败:", err);
    }
    }

    // 并行执行(优化性能)。
    async function getMultiData() {
    // 先发起所有请求(并行)。
    const p1 = fetch("/api/user");
    const p2 = fetch("/api/order");

    // 再等待所有结果。
    const userRes = await p1;
    const orderRes = await p2;

    const user = await userRes.json();
    const order = await orderRes.json();

    return { user, order };
    }

    // async / await具体用法请参考本章第72.7节。

  • 实际项目中的常见用法。

    // 场景1:表单提交(先验证,再请求,最后提示)。
    async function submitForm(formData) {
    try {
    // 1. 前端验证。
    if (!formData.username) throw new Error("请输入用户名");

    // 2. 提交请求。
    const res = await fetch("/api/submit", {
    method: "POST",
    body: JSON.stringify(formData)
    });

    const data = await res.json();

    // 3. 处理结果。
    if (data.code === 200) {
    alert("提交成功");
    window.location.href = "/success";
    } else {
    alert(data.message);
    }
    } catch (err) {
    alert("提交失败:" + err.message);
    }
    }

    // 场景2:数据缓存(避免重复请求)。
    const cache = new Map();
    async function getCachedData(key) {
    // 有缓存则直接返回。
    if (cache.has(key)) return cache.get(key);

    // 无缓存则请求并缓存。
    const res = await fetch(`/api/data?key=${key}`);
    const data = await res.json();
    cache.set(key, data);

    return data;
    }

  • 四、常见坑点与避坑技巧

  • 忘记catch异常:未捕获的Promise异常会导致程序崩溃,务必在链式调用末尾加catch,或用try/catch包裹await。

  • 错误的链式调用:then中未返回新Promise会导致后续then立即执行,而非等待异步操作。

    // 错误写法。
    request("url1").then(() => {
    request("url2"); // 未返回,后续then会立即执行。
    }).then(() => {
    console.log("url2 请求完成"); // 实际url2还在请求中。
    });

    // 正确写法。
    request("url1").then(() => {
    return request("url2"); // 返回Promise,等待完成。
    }).then(() => {
    console.log("url2 请求完成");
    });

  • Promise.all快速失败:只要有一个Promise失败,整个all就会失败,若需全部结果,改用allSettled。

  • 五、Promise链式调用异常捕获

    • 问题:

      在一串全是then的Promise链式调用中,该如何正确编写catch来捕获整个链路上的异常?

    • 解决:

      在链式调用末尾加一个catch。

      这是最推荐的写法 —— 无论链式调用中有多少个then,只需要在整个链条的最后添加一个catch,就能捕获链路上任意位置抛出的异常(包括任意then里的同步错误、任意Promise的reject)。

    • 原因:

      Promise链式调用的本质是“异常冒泡”:链路上任意一个环节抛出异常(reject或同步错误),都会跳过后续所有then,直接走到最近的catch。因此,末尾的一个catch就能兜底整个链路的异常。

    • 进阶场景(局部捕获 + 全局兜底):

      如果你想对某个then的异常做“局部处理”,处理完后继续执行后续链路,同时保留全局兜底catch,可以在对应then后紧跟一个局部catch(处理后需返回新的Promise或值,让链路继续)。

      示例代码(局部+全局):

      request1()
      .then((res1) => {
      console.log("第一步结果:", res1);
      return request2(res1);
      })
      // 局部catch:只捕获request2的异常。
      .catch((err) => {
      console.error("request2 异常(局部处理):", err);
      return "request2 异常后的默认值"; // 返回值,让链路继续。
      })
      .then((res2) => {
      console.log("第二步结果(含异常兜底):", res2);
      return request3(res2);
      })
      .then((finalRes) => {
      console.log("最终结果:", finalRes);
      })
      // 全局兜底:捕获其他环节的异常(比如request3、后续then)。
      .catch((err) => {
      console.error("全局异常兜底:", err);
      });

    • 常见错误写法(避坑):

      • 错误 1:每个then后都加catch(冗余且易断链)。

        // 不推荐!冗余且可能导致链路中断。
        request1()
        .then((res1) => request2(res1))
        .catch((err) => console.log("err1:", err)) // 捕获后未返回值,后续then拿到undefined。
        .then((res2) => request3(res2)) // res2是undefined,可能导致request3出错。
        .catch((err) => console.log("err2:", err));

      • 错误 2:把catch写在中间(无法捕获后续异常)。

        // 错误!catch写在中间,无法捕获后续then的异常。
        request1()
        .then((res1) => request2(res1))
        .catch((err) => console.log("中间 catch:", err))
        .then((res2) => request3(res2)) // 这里的异常无法被前面的catch捕获。
        .then((finalRes) => console.log(finalRes));

    • 建议:

      结合async / await的写法(更易读)。

      如果你的环境支持ES2017+,用async/await + try/catch替代纯then链式调用,代码会更直观(本质还是Promise,只是语法糖)。

      async function fetchData() {
      try {
      const res1 = await request1();
      console.log("第一步结果:", res1);

      const res2 = await request2(res1);
      console.log("第二步结果:", res2);

      const finalRes = await request3(res2);
      console.log("最终结果:", finalRes);
      } catch (err) {
      // 一个try/catch捕获所有await环节的异常。
      console.error("捕获到异常:", err);
      }
      }

      fetchData();

    六、Promise链式调用返回值

    如果在Promise链式调用的then里不返回Promise,而是返回普通值,后续还能不能调用then —— 答案是:完全可以!

    无论then里返回的是普通值(数字、字符串、对象等)还是undefined,Promise都会自动把这个值包装成一个已成功的Promise,因此后续依然可以正常调用then。

    举例说明:

    • 示例1:返回普通值(字符串 / 数字)。

      // 初始Promise。
      Promise.resolve(10)
      // 第一个then:返回普通数字。
      .then((num) => {
      console.log("第一步:", num); // 输出:第一步:10。
      return num * 2; // 返回普通值20(非Promise)。
      })
      // 第二个then:依然可以调用,接收上一步的返回值。
      .then((num) => {
      console.log("第二步:", num); // 输出:第二步:20。
      return `最终结果:${num}`; // 返回普通字符串。
      })
      // 第三个then:继续调用。
      .then((str) => {
      console.log("第三步:", str); // 输出:第三步:最终结果:20。
      })
      .catch((err) => console.error("异常:", err));

    • 示例2:返回对象/不返回值(默认返回undefined)。

      Promise.resolve("初始值")
      .then((val) => {
      // 返回普通对象。
      return { name: "测试", value: val };
      })
      .then((obj) => {
      console.log("对象值:", obj); // 输出:对象值:{ name: '测试', value: '初始值' }。
      // 不写return,默认返回undefined。
      })
      .then((emptyVal) => {
      console.log("空值:", emptyVal); // 输出:空值:undefined。
      });

    • 示例3:混合返回(普通值 + Promise)。

      Promise.resolve(1)
      .then((num) => {
      return num + 1; // 普通值2。
      })
      .then((num) => {
      // 返回Promise。
      return new Promise((resolve) => setTimeout(() => resolve(num * 10), 500));
      })
      .then((num) => {
      console.log("混合结果:", num); // 输出:混合结果:20。
      return "结束"; // 普通值。
      })
      .then((str) => {
      console.log(str); // 输出:结束。
      });

    背后的原理(Promise的“同化”机制):

    Promise设计了一套返回值同化规则,确保then的返回值始终是Promise,因此链式调用能一直延续。

  • 如果then的回调返回普通值(非Promise、非抛出异常)→ 自动包装为Promise.resolve(返回值)。

  • 如果then的回调返回Promise → 直接使用这个Promise,等待其状态变更。

    所以说,不返回Promise时,then执行完后会立即执行下一个then。

  • 如果then的回调抛出异常(throw new Error())→ 自动包装为Promise.reject(异常)。

  • 简单来说:then的返回值永远会被“转换成”一个Promise,这就是为什么不管返回什么,后续都能调用then/catch。

    异常场景(返回普通值但抛出错误):

    即使返回普通值,只要then里抛出异常,依然会被后续的catch捕获。

    Promise.resolve(5)
    .then((num) => {
    if (num === 5) {
    throw new Error("手动抛出异常"); // 抛出错误。
    }
    return num + 1;
    })
    .then((num) => {
    console.log("这一步不会执行"); // 异常会跳过后续then。
    })
    .catch((err) => {
    console.error("捕获异常:", err.message); // 输出:捕获异常:手动抛出异常。
    });

    和返回Promise的对比(清晰版):

    then回调的返回值实际等价于后续then接收的值
    return 100。 return Promise.resolve(100)。 100。
    return {a: 1}。 return Promise.resolve({a: 1})。 {a: 1}。
    return undefined(不写return)。 return Promise.resolve(undefined)。 undefined。
    return Promise.resolve(200)。 直接返回该Promise。 200。
    return Promise.reject("失败")。 直接返回该Promise。 无(触发catch)。
    throw new Error("错误")。 return Promise.reject(Error)。 无(触发catch)。

    七、async/await用法

    async/await核心本质:

    async/await是ES2017(ES8)引入的异步编程语法糖,完全基于Promise实现,目的是让异步代码的写法更接近同步代码,解决Promise链式调用(then/catch)的可读性问题。

    核心结论:

    • async函数的返回值永远是一个Promise(即使你返回普通值,也会被自动包装成Promise.resolve(值))。

    • await只能在async函数内部使用,作用是“暂停”函数执行,等待右侧Promise完成(成功/失败)后再继续。

    基础语法(入门):

  • async关键字(修饰函数)。

    // 1. 声明 async 函数。
    async function fn1() {
    return 100; // 等价于return Promise.resolve(100)
    }

    // 2. 箭头函数形式。
    const fn2 = async () => {
    return "hello"; // 等价于return Promise.resolve("hello")。
    };

    // 3. 验证返回值是Promise。
    fn1().then(res => console.log(res)); // 输出:100。
    fn2().then(res => console.log(res)); // 输出:hello。

    // 4. 若async函数抛出异常,返回失败的Promise。
    async function fn3() {
    throw new Error("手动抛出错误"); // 等价于return Promise.reject(Error)。
    }
    fn3().catch(err => console.log(err.message)); // 输出:手动抛出错误。

  • await关键字(等待Promise完成)。

    await后可以跟3类值:

    • Promise对象:等待其状态变为fulfilled,并返回resolve的值。

    • 普通值(数字、字符串等):直接返回该值(无等待)。

    • 异步函数(本质还是返回Promise):等待其执行完成并返回结果。

    // 模拟异步函数(返回Promise)。
    function delay(ms) {
    return new Promise(resolve => setTimeout(() => resolve(ms), ms));
    }

    // async/await基本使用。
    async function testAwait() {
    console.log("开始执行");

    /*
    等待Promise完成,拿到结果。
    1、执行delay(1000):立即返回一个“pending状态”的Promise,同时启动1秒的定时器。
    2、遇到await:暂停testAwait函数的执行(但不会阻塞外部主线程),主线程继续执行后续同步代码(比如下述示例中的console.log("主线程同步代码:…"))。
    3、1秒后:delay(1000)的Promise状态变为fulfilled,await拿到resolve的值(1000),赋值给res1。
    4、恢复执行:testAwait函数从暂停处继续,执行console.log("第一个延迟完成:", 1000)。
    */

    const res1 = await delay(1000);
    console.log("第一个延迟完成:", res1); // 1秒后输出:1000。

    // 等待普通值(直接返回)。
    const res2 = await 200;
    console.log("普通值:", res2); // 立即输出:200。

    // 等待另一个async函数。
    const res3 = await testAwait2();
    console.log("async 函数结果:", res3); // 输出:testAwait2 结果。

    return "最终结果"; // 等价于Promise.resolve("最终结果")。
    }

    async function testAwait2() {
    return "testAwait2 结果";
    }

    /*
    testAwait()是async函数,调用后返回Promise,因此需要用then接收其返回值。
    如果testAwait内部抛出异常,需用.catch()捕获(或在函数内部用try/catch)。
    */

    testAwait().then(res => console.log(res)); // 最终输出:最终结果。

    // 同步代码:验证await不阻塞外部主线程。
    console.log("主线程同步代码:testAwait调用后立即执行");

    /*
    输出顺序:
    开始执行
    主线程同步代码:testAwait调用后立即执行
    第一个延迟完成: 1000
    普通值: 200
    async 函数结果: testAwait2 结果
    最终结果
    */

    // 问题:上述delay()函数中resolve(ms)不应该是调用的是.then(() => {})中的函数吗?为什么会是直接返回ms值给res1?
    /*
    先明确核心结论:
    await是Promise的then回调的语法糖:"await promise"等价于「把后续代码塞进promise.then()的回调里,并自动提取resolve的参数作为返回值」。
    简单说:你不用写then,是因为await帮你“偷偷”完成了then的工作,直接把resolve(ms)的参数(ms)返回给你。
    对比两种写法(原生Promise vs async/await):
    1. 原生Promise(手动写then)。
    这是你认知中“resolve调用then回调”的写法:
    function delay(ms) {
    return new Promise(resolve => setTimeout(() => resolve(ms), ms));
    }
    // 原生Promise:必须写then才能拿到resolve的值。
    delay(1000).then(res1 => {
    console.log("第一个延迟完成:", res1); // res1就是resolve(ms)的ms。
    });
    2. async/await(语法糖,省略then)。
    async function testAwait() {
    console.log("开始执行");
    // await帮你做了then的工作,直接拿到resolve的值。
    const res1 = await delay(1000);
    console.log("第一个延迟完成:", res1);
    }
    底层等价转换(await → then)。
    await本质上是把代码“拆解”成了Promise的then链式调用,上面的testAwait等价于:
    function testAwait() {
    console.log("开始执行");
    // await被拆解为then回调。
    return delay(1000).then(res1 => {
    console.log("第一个延迟完成:", res1);
    // 如果有后续代码,会继续链式调用then。
    });
    }
    详细拆解"await delay(1000)"的执行过程:
    1. 执行delay(1000),返回一个pending状态的Promise,同时启动1秒定时器。
    2. 遇到await,JS引擎会:
    2.1. 暂停当前testAwait函数的执行。
    2.2. 给这个Promise绑定一个「隐式的then回调」,回调的逻辑是:“把resolve的参数赋值给res1,然后恢复testAwait函数执行”。
    3. 1秒后,定时器触发,执行resolve(1000)。
    3.1. Promise状态变为fulfilled。
    3.2. 触发步骤2中绑定的「隐式then回调」。
    3.3. 回调把1000作为返回值,赋值给res1。
    4. testAwait函数恢复执行,继续执行console.log("第一个延迟完成:", res1)。
    补充:reject对应的逻辑(异常捕获)。
    同理,await也帮你处理了catch的逻辑:
    // 模拟失败的 Promise
    function delayError(ms) {
    return new Promise((_, reject) => setTimeout(() => reject("超时了"), ms));
    }
    async function test() {
    try {
    // await会捕获reject的值,抛出异常。
    const res = await delayError(1000);
    } catch (err) {
    // 等价于delayError(1000).catch(err => { … })。
    console.log("捕获异常:", err); // 输出:捕获异常:超时了。
    }
    }
    总结:
    resolve(ms)本身确实是触发then回调,但await帮你隐式绑定了这个then回调,并自动把回调的参数(ms)作为返回值。
    "await promise"的本质:等待promise状态变为fulfilled,然后提取resolve的参数作为自己的返回值(省去了手动写then的步骤)。
    记忆要点:await是Promise的then回调的语法糖,核心功能就是「自动提取resolve的值 + 暂停函数执行」。
    这也是async/await比纯Promise链式调用更简洁的原因 —— 它帮你封装了then的底层逻辑,让代码看起来像同步代码。

  • 异常处理:

    await等待的Promise若变为rejected,会直接抛出异常,需用try/catch捕获(替代Promise的catch)。

  • 基础捕获:单个await异常。

    async function testError() {
    try {
    // 等待一个失败的Promise。
    const res = await Promise.reject("请求失败");
    console.log(res); // 这行不会执行。
    } catch (err) {
    console.error("捕获异常:", err); // 输出:捕获异常:请求失败。
    }
    }
    testError();

  • 批量捕获:多个await异常。

    async function testMultiError() {
    try {
    const res1 = await delay(1000);
    const res2 = await Promise.reject("第二个请求失败"); // 抛出异常。
    const res3 = await delay(2000); // 这行不会执行。
    } catch (err) {
    console.error("捕获异常:", err); // 输出:捕获异常:第二个请求失败。
    } finally {
    console.log("无论成败都会执行"); // 输出:无论成败都会执行。
    }
    }
    testMultiError();

  • 局部捕获:特定await异常。

    若想让某个 await 异常不影响后续代码,可给单个 await 加独立的 try/catch:

    async function testPartialError() {
    // 第一个await独立捕获异常。
    let res1;
    try {
    res1 = await Promise.reject("请求1失败");
    } catch (err) {
    console.error("请求1异常:", err);
    res1 = "请求1默认值"; // 兜底默认值。
    }

    // 异常不影响后续执行。
    const res2 = await delay(500);
    console.log("res1:", res1); // 输出:请求1默认值。
    console.log("res2:", res2); // 输出:500。
    }
    testPartialError();

  • 实战场景:

  • 并行执行(优化性能)。

    注意:默认情况下await是串行执行(等上一个完成再执行下一个),若多个异步操作无依赖,需手动改为并行,避免性能浪费。

    // 反面示例:串行执行(总耗时1000+2000=3000ms)。
    async function serialExec() {
    console.time("serial");
    const res1 = await delay(1000);
    const res2 = await delay(2000);
    console.timeEnd("serial"); // 输出:serial: 3000+ms。
    }

    // 正确示例:并行执行(总耗时2000ms,取最长的那个)。
    async function parallelExec() {
    console.time("parallel");
    // 先发起所有异步请求(并行)。
    const p1 = delay(1000);
    const p2 = delay(2000);

    // 再等待所有结果。
    const res1 = await p1;
    const res2 = await p2;
    console.timeEnd("parallel"); // 输出:parallel: 2000+ms。
    }

    serialExec();
    parallelExec();

    /*
    问题:
    Promise不是要调用才会执行吗?"const p1/p2 = delay(1000/2000);"只是返回了一个Promise,怎么就成了并行执行呢?
    解答:
    Promise的执行器函数(new Promise里的代码)在创建Promise时就会立即同步执行,而非等到await/then调用时才执行。

    核心原理:Promise“创建即执行”。
    Promise有一个极其关键的特性:当你调用返回Promise的函数(比如delay(1000))时,
    函数内部的new Promise(executor)会立即执行「执行器函数(executor)」。
    也就是说,异步操作(比如setTimeout)在delay(1000)调用的瞬间就已经启动,而非等到await时才启动。
    后续的"await p1/await p2"只是“等待它们完成(即订阅结果)”,而非“启动它们”。
    总结:
    核心原因:Promise执行器函数“创建即执行” —— 调用delay(1000)时,定时器立即启动,"const p1 = delay(1000)"只是接收返回的Promise,而非“延迟执行”。
    并行关键:先调用所有返回Promise的函数(启动所有异步操作),再用await等待结果,此时异步操作已经并行运行。
    串行 vs 并行:
    串行:await紧跟函数调用(先等完成,再启动下一个)。
    并行:先调用所有函数(启动所有操作),再await结果。
    简单记:Promise的异步操作在“创建时启动”,await只负责“等结果”,不负责“启动操作” —— 这就是为什么"const p1 = delay(1000)"能触发并行执行。
    */

  • 结合Promise.all批量并行。

    async function testPromiseAll() {
    try {
    // 并行发起多个请求。
    const promises = [delay(500), delay(1000), delay(1500)];
    const results = await Promise.all(promises);
    console.log("批量结果:", results); // 输出:[500, 1000, 1500]。
    } catch (err) {
    console.error("任意一个请求失败:", err);
    }
    }
    testPromiseAll();

  • 超时控制(结合Promise.race)。

    // 封装超时函数。
    function timeout(delay) {
    return new Promise((_, reject) => setTimeout(() => reject(new Error("超时")), delay));
    }

    // 带超时的请求。
    async function fetchWithTimeout(url) {
    try {
    // race:谁先完成就取谁的结果。
    const res = await Promise.race([fetch(url), timeout(5000)]);
    return res.json();
    } catch (err) {
    console.error("请求失败:", err.message);
    }
    }

  • 异步遍历(for…of + await)。

    不要在forEach中用await(forEach不等待异步操作,会直接遍历完),改用for…of。

    // 错误示例:forEach不等待await。
    async function wrongForEach() {
    const arr = [1000, 2000, 500];
    arr.forEach(async (ms) => {
    const res = await delay(ms);
    console.log("forEach 结果:", res); // 输出顺序混乱(500 → 1000 → 2000)。
    });
    }

    // 正确示例:for…of等待await。
    async function correctForOf() {
    const arr = [1000, 2000, 500];
    for (const ms of arr) {
    const res = await delay(ms);
    console.log("for…of 结果:", res); // 输出顺序:1000 → 2000 → 500。
    }
    }

    wrongForEach();
    correctForOf();

  • async/await vs Promise:

    特性async/awaitPromise(then/catch)
    代码风格 同步式写法,可读性高。 链式调用,嵌套少但仍有回调感。
    异常处理 try/catch(同步式)。 catch方法(回调式)。
    调试体验 可直接断点调试(和同步代码一致)。 断点需跳转到then回调,体验差。
    并行执行 需手动处理(先创建Promise再await)。 直接用Promise.all / Promise.race。
    兼容性 ES8(Node.js 7.6+ / 浏览器ES8+)。 ES6(兼容性更广)。

    八、总结

    Promise:

  • 核心本质:Promise是异步操作的"容器",通过状态管理解决回调地狱,状态一旦改变不可逆。

  • 核心用法:then/catch/finally处理结果,all/race/allSettled处理多Promise场景,async/await简化语法。

  • 实战关键:封装异步API统一返回Promise,结合async/await编写易读的异步代码,注意异常捕获和性能优化(如并行请求)。

  • async/await:

  • 核心本质:async/await是Promise的语法糖,async函数返回Promise,await暂停函数执行等待Promise完成。

  • 异常处理:必须用try/catch捕获await抛出的异常(替代Promise的catch)。

  • 性能优化:无依赖的异步操作需手动并行(先创建Promise再await / 结合Promise.all),避免串行浪费时间。

  • 避坑要点:不要在forEach中用await,改用for…of。await只能在async函数内使用。

  • 九、问题思考

    // 封装Node.js文件读取。
    const fs = require("fs");
    function readFile(path) {
    return new Promise((resolve, reject) => {
    fs.readFile(path, "utf8", (err, data) => {
    if (err) reject(err);
    else resolve(data);
    });
    });
    }

    // 使用。
    readFile("./test.txt")
    .then(data => console.log(data))
    .catch(err => console.log(err));

    /*
    根据宏任务/微任务机制,
    使用Nodejs文件读取时,异步的是Promise.then而不是Promise中的函数,
    文件读取时依旧会阻塞主进程吗?
    */

    结合宏任务/微任务机制,Node.js中fs.readFile封装成Promise后,是否会阻塞主进程?

    核心结论:fs.readFile本身是异步的,不会阻塞Node.js主进程;Promise执行器函数里的异步IO操作和then的微任务特性,都是非阻塞的。

    下述从「阻塞本质」「Node.js异步IO原理」「宏/微任务和执行时机」三个维度来进行讲解。

  • 明确核心概念:Node.js中“阻塞”的本质。

    Node.js是单线程事件循环模型,但它的“单线程”仅指「JavaScript执行线程」(主进程),而文件IO、网络请求等耗时操作,会被Node.js交给「libuv底层线程池」处理。这是Node.js异步IO的核心。

    阻塞操作:占用JavaScript主线程,导致事件循环卡住(比如fs.readFileSync同步读取文件)。

    非阻塞操作:JavaScript主线程仅发起请求,底层线程池处理耗时操作,完成后通过回调通知主线程(比如fs.readFile异步读取)。

  • 关键结论:fs.readFile不会阻塞主进程。

    function readFile(path) {
    return new Promise((resolve, reject) => {
    // Promise执行器函数(同步执行)。
    fs.readFile(path, "utf8", (err, data) => { // 异步IO。
    if (err) reject(err);
    else resolve(data);
    });
    });
    }

    执行器函数是“同步调用异步方法”,而非“同步阻塞”。

    • Promise执行器函数((resolve, reject) => {…})会立即同步执行,但执行的是fs.readFile这个「异步方法的调用」—— 这个调用本身只做一件事:把“文件读取任务”交给libuv线程池,然后主线程立即退出执行器函数,继续处理后续代码。

    • 文件读取的耗时操作(磁盘IO)是在libuv线程池中完成的,和JavaScript主线程无关,因此不会阻塞主进程。

    对比同步读取(真正的阻塞)。

    • 如果用fs.readFileSync(同步读取),才会真正阻塞主线程。

      // 同步读取:阻塞主线程,事件循环卡住。
      function readFileSync(path) {
      return new Promise((resolve) => {
      // 执行器函数里的同步阻塞操作!
      const data = fs.readFileSync(path, "utf8");
      resolve(data);
      });
      }

      此时,fs.readFileSync会占用主线程直到文件读取完成,期间Node.js无法处理任何其他请求(比如新的HTTP请求、定时器)。

  • 宏任务/微任务视角:异步的不是then,而是IO本身。

    上述提到的问题:“异步的是 Promise.then 而不是 Promise 中的函数”。这个理解需要修正:

    完整执行流程(结合事件循环):

  • #mermaid-svg-enVGEbkA4lcCWUij{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-enVGEbkA4lcCWUij .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-enVGEbkA4lcCWUij .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-enVGEbkA4lcCWUij .error-icon{fill:#552222;}#mermaid-svg-enVGEbkA4lcCWUij .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-enVGEbkA4lcCWUij .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-enVGEbkA4lcCWUij .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-enVGEbkA4lcCWUij .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-enVGEbkA4lcCWUij .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-enVGEbkA4lcCWUij .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-enVGEbkA4lcCWUij .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-enVGEbkA4lcCWUij .marker{fill:#333333;stroke:#333333;}#mermaid-svg-enVGEbkA4lcCWUij .marker.cross{stroke:#333333;}#mermaid-svg-enVGEbkA4lcCWUij svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-enVGEbkA4lcCWUij p{margin:0;}#mermaid-svg-enVGEbkA4lcCWUij .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-enVGEbkA4lcCWUij .cluster-label text{fill:#333;}#mermaid-svg-enVGEbkA4lcCWUij .cluster-label span{color:#333;}#mermaid-svg-enVGEbkA4lcCWUij .cluster-label span p{background-color:transparent;}#mermaid-svg-enVGEbkA4lcCWUij .label text,#mermaid-svg-enVGEbkA4lcCWUij span{fill:#333;color:#333;}#mermaid-svg-enVGEbkA4lcCWUij .node rect,#mermaid-svg-enVGEbkA4lcCWUij .node circle,#mermaid-svg-enVGEbkA4lcCWUij .node ellipse,#mermaid-svg-enVGEbkA4lcCWUij .node polygon,#mermaid-svg-enVGEbkA4lcCWUij .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-enVGEbkA4lcCWUij .rough-node .label text,#mermaid-svg-enVGEbkA4lcCWUij .node .label text,#mermaid-svg-enVGEbkA4lcCWUij .image-shape .label,#mermaid-svg-enVGEbkA4lcCWUij .icon-shape .label{text-anchor:middle;}#mermaid-svg-enVGEbkA4lcCWUij .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-enVGEbkA4lcCWUij .rough-node .label,#mermaid-svg-enVGEbkA4lcCWUij .node .label,#mermaid-svg-enVGEbkA4lcCWUij .image-shape .label,#mermaid-svg-enVGEbkA4lcCWUij .icon-shape .label{text-align:center;}#mermaid-svg-enVGEbkA4lcCWUij .node.clickable{cursor:pointer;}#mermaid-svg-enVGEbkA4lcCWUij .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-enVGEbkA4lcCWUij .arrowheadPath{fill:#333333;}#mermaid-svg-enVGEbkA4lcCWUij .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-enVGEbkA4lcCWUij .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-enVGEbkA4lcCWUij .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-enVGEbkA4lcCWUij .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-enVGEbkA4lcCWUij .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-enVGEbkA4lcCWUij .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-enVGEbkA4lcCWUij .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-enVGEbkA4lcCWUij .cluster text{fill:#333;}#mermaid-svg-enVGEbkA4lcCWUij .cluster span{color:#333;}#mermaid-svg-enVGEbkA4lcCWUij div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-enVGEbkA4lcCWUij .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-enVGEbkA4lcCWUij rect.text{fill:none;stroke-width:0;}#mermaid-svg-enVGEbkA4lcCWUij .icon-shape,#mermaid-svg-enVGEbkA4lcCWUij .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-enVGEbkA4lcCWUij .icon-shape p,#mermaid-svg-enVGEbkA4lcCWUij .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-enVGEbkA4lcCWUij .icon-shape rect,#mermaid-svg-enVGEbkA4lcCWUij .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-enVGEbkA4lcCWUij .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-enVGEbkA4lcCWUij .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-enVGEbkA4lcCWUij :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    调用readFile('./test.txt')

    同步执行Promise执行器函数

    调用fs.readFile,交给libuv线程池处理

    主线程立即返回,继续执行后续代码(非阻塞)

    libuv线程池完成文件读取

    将回调加入IO回调宏任务队列

    事件循环到IO回调阶段

    执行回调,调用resolve(data)

    将then回调加入微任务队列

    当前宏任务执行完

    执行微任务队列中的then回调

    关键细节(宏/微任务与阻塞的关系)。

    • Promise执行器函数:同步执行,但仅“发起异步IO请求”,不阻塞。

    • fs.readFile的异步性:核心是「IO操作在底层线程池执行」,和Promise无关 —— 即使不用Promise封装,直接用回调的fs.readFile也不会阻塞。

    • then的微任务特性:resolve触发后,then回调会被加入微任务队列,等待当前宏任务执行完后执行 —— 这是“异步执行回调”,而非“异步执行 IO”。

    • 是否阻塞:判断是否阻塞的核心是「耗时操作是否占用主线程」,而非「回调是否异步」。fs.readFile的IO操作在底层线程池,因此无论是否封装Promise,都不阻塞。

  • 补充:容易混淆的“同步/异步”点。

    操作同步 / 异步是否阻塞主线程所属任务类型
    Promise执行器函数。 同步。 仅执行“发起请求”,不阻塞。 主线程同步代码。
    fs.readFile。 异步IO。 不阻塞(底层线程池处理)。 IO回调宏任务。
    fs.readFileSync。 同步IO。 阻塞(主线程处理)。 主线程同步代码。
    Promise.then回调。 异步。 不阻塞(微任务队列)。 微任务。
    setTimeout回调。 异步。 不阻塞(定时器宏任务)。 宏任务。
  • 赞(0)
    未经允许不得转载:171主机测评 » JavaScript核心异步编程解决方案(Promise)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址