当前位置: 首页 > news >正文

从 Promise 到 async/await:一次把 JavaScript 异步模型讲透

文章目录

  • 为什么引入Promise?
  • 1.promise 的基本认识
  • 2.promise的API
    • 2.1promise 构造/创建类()
    • 2.2 实例方法-promise.then/.catch
    • 2.3 Promise 组合类 APi
      • 2.3.1 promise.race()
      • 2.3.2 Promise.all()
  • 3. async/await
    • 3.1 async/await 是什么?
    • 3.2 async语法
    • 3.3 await语法
    • 3.4 如何使用 async/await?

为什么引入Promise?

JavaScript 有一个重要的概念——异步(async),它允许我们在执行运行任务时,不一定等待进程完成,而是继续执行下面的代码,直到任务完成再通知。常用的异步操作有:文件操作、数据库操作、AJAX 以及定时器等。

JavaScript 有两种实现异步的方式:

第一种:callback函式 回调函数

在 ES6 Promise 出现之前,通常使用回调函数式(callback)实现异步操作。但使用回调函数式回调存在一个明显的缺点,当需要执行多个异步操作方案时,代码会不断往内调用,这种情况通常被称为「回调地狱」(callback hell)。

所以为了解决此类问题,就出现了第二种方法 - Promise

1.promise 的基本认识

Promise是用来表示一个异步操作的最终完成(或失败)及其结果值

Promise的三种状态

  • 待定状态(pending) :初始状态,既没有被兑现,也没有被拒绝
  • 已兑现(fulfilled):意味着,操作成功完成
  • 已拒绝(rejected):意味着,操作失败

根据ES Spec 标准,Promise 是一个带内部槽(internal slots)的对象

他的一个最小抽象的模型如下:状态与结果值

Promise { [[PromiseState]]: "fulfilled", [[PromiseResult]]: 32(一个值) }

比如打印一个 promise 对象,就能看到这两个内容槽,其他的内容槽对于开发调试没什么作用,不展示

拓展(便于理解):一个 promise (抽象)大致有这些内部槽

[[PromiseState]] // pending / fulfilled / rejected [[PromiseResult]] // value 或 reason [[PromiseFulfillReactions]] // then 成功回调队列 [[PromiseRejectReactions]] // then/catch 失败回调队列 [[PromiseIsHandled]] // 是否已处理 rejection

2.promise的API

2.1promise 构造/创建类()

用来创建或得到 Promise

API作用
new Promise(executor)创建一个可手动控制状态的 Promise
Promise.resolve(value)把任意值转换为 fulfilled Promise
Promise.reject(reason)创建一个 rejected Promise

Promise(executor)的框架,同步执行

newPromise((resolve,reject)=>{}

Promise 构造函数会自动传给你两个函数参数,用来手动改变这个新 Promise 的状态。
这里传进去的这个函数,通常叫:执行器函数

  • 调用 resolve()函数,会让 promise 对象的状态从 pending

Promise.resolve(value)

作用:把任何值 x 转换成一个 Promise

  • 如果 x 本来就是 Promise → 原样“接管”
  • 如果 x 是普通值 → 包装成 fulfilled Promise

返回值:Promise

2.2 实例方法-promise.then/.catch

API作用
.then()处理 fulfilled
.catch()处理 rejected
.finally()不关心结果,做清理

.then() 为什么能链式调用?

.then() 本身不会立刻执行回调,它会等当前 Promise 状态确定后再执行:
• 如果 Promise 是 fulfilled,执行 onFulfilled
• 如果 Promise 是 rejected,执行 onRejected(如果提供了的话)
并且,.then() 一定会返回一个新的 Promise。这就是 Promise 可以不断链式调用的根本原因。

例如:

p.then(fn1).then(fn2).then(fn3)本质上可以理解为:constp1=p.then(fn1)constp2=p1.then(fn2)constp3=p2.then(fn3)

为什么前面 .then() 出错,后面的 .catch() 能接住?

因为 .then() 返回的是一个新的 Promise。
如果某个 .then() 的回调函数内部:
• 抛出了错误
• 或者返回了一个 rejected Promise

那么这个 .then() 返回的新 Promise 就会变成 rejected,错误会沿着 Promise 链继续向后传播,直到被后面的 .catch() 捕获。


.catch 的完整语法其实是:

promise.then(undefined, onRejected)

那其实 promise本质就是一直链式调用.then,为什么只要上面有一步.then 出错就报了 catch?

Promise 规范里还有一条非常关键的规则:Promise 链的“状态传播规则”

如果一个 .then 的回调没有被执行(因为 Promise 是 rejected),

那么这个 .then 会“原样返回一个 rejected 的 Promise”。

来看一个例子

p .then(A) .then(B) .then(C) .catch(D) 内部等价于: p1 = p.then(A) p2 = p1.then(B) p3 = p2.then(C) p4 = p3.then(undefined, D)
  • 假如 a 抛出了错误,p1 变成了 rejected

  • B不会执行,但是p2 仍然是 rejected

  • 同理 c 也是,所以到 D 这,D 执行

2.3 Promise 组合类 APi

2.3.1 promise.race()

  • Promise.race 会返回一个新的 Promise

  • 采用最先完成的那个 Promise 的状态和值

用 promise 内部槽的观点来理解,Promise.race 会创建一个新的 Promise,并在内部采用(adopt)最先 settle 的Promise 的[[PromiseState]] 与 [[PromiseResult]],而不继承其余内部槽。

settle 是指不是 pedding 状态

手写 promiseRace

functionpromiseRace(promises){returnnewPromise((resolve,reject)=>{for(constpofpromises){p.then((val)=>{resolve(val);}).catch((e)=>{reject(e);});}});}
  • 首先明确(resolve, reject) => {和resolve(val);/ reject(e);的关系
    • new Promise((resolve, reject) => {}) 中的 resolve 和 reject,是用来用来改变这个 Promise 状态的控制函数,当调用 resolve(value) 时,Promise 从 pending 变为 fulfilled,并将 value 作为结果传递给后续的 .then 回调。
  • 其中的resolve(val) 会将 val 写入新 Promise 的 [[PromiseResult]];这里的 val来源于外部 Promise 在其 fulfilled 时传入的值
  • promiseRace(promises),表示这个外部函数是多个 promise,for (const p of promises),同步遍历所有 Promise,给它们注册 .then / .catch 回调,然后将最快的返回赋值给新的 promise

外部示例如下:

constp1=newPromise(r=>setTimeout(()=>r('A'),1000));constp2=newPromise(r=>setTimeout(()=>r('B'),500));constp3=newPromise(r=>setTimeout(()=>r('C'),1500));constraceP=promiseRace([p1,p2,p3]);
  • 只执行一次、只吃到 ‘B’。

2.3.2 Promise.all()

Promise.all 是什么?

Promise.all 接收一组 Promise 的Iterable(可遍历对象,例如Array、Map、Set),返回一个新的 Promise;

只有当所有 Promise 都 fulfilled 时,新 Promise 才 fulfilled,

并且结果是一个按顺序排列的结果数组;

只要有一个 Promise rejected,整体立刻 rejected。

  • Promise.all 返回的结果数组顺序,严格等于“传入数组的顺序”,与各个 Promise 实际完成(执行)顺序无关。

  • Promise.all 的参数不要求“全是 Promise 对象”,它会先对每一项执行 Promise.resolve(x),

    所以普通值(如 42)会被当成“已完成的 Promise”

  • 定义中有提到,如果输入为空,例如空数组,就返回一个空数组

常见使用场景:

  • 多接口并发请求,全部成功再渲染
  • 页面初始化依赖多个异步资源
  • 批量任务统一完成后再继续

来看一个小例子:

constpromise1=Promise.resolve(3);constpromise2=42;constpromise3=newPromise((resolve,reject)=>{setTimeout(resolve,100,"foo");});Promise.all([promise1,promise2,promise3]).then((values)=>{console.log(values);});// 预期输出结果: Array [3, 42, "foo"]

补充:setTimeout 的写法:

写法发生时间结果
setTimeout(resolve, 100, “foo”)100ms 后resolve(“foo”)
setTimeout(() => resolve(“foo”), 100)100ms 后resolve(“foo”)
setTimeout(resolve(“foo”), 100)立刻Promise 立刻 resolve(❌)

手写 promise.all,简单实现 promise.all,只考虑 Iterable 是数组的情况

functionpromiseAll(promises){// 如果参数不是数组,返回一个 JS 的类型错误if(!Array.isArray(promises)){returnnewTypeError("参数必须是一个数组");}//如果输入是空数组,就返回空数组,根据定义if(promises.length===0){returnPromise.resolve([]);}//定义一个输出结果和计数器记录数组长度,把最终结果存在这里,开始核心的逻辑constoutputs=[];letresolveCounter=0;//1.promise.all最终要返回一个新的 promise 对象,最外层应该是 promise executor,resolve的返回值应该是 outputs数组//2.遍历外部的 promises,拿到 promises 中的每一个 promise//3.每一个 promise 对象的处理// (1)因为参数不保证一定是 promise 对象,所以应该将参数处理成 promise对象// (2)调用.then,如果这个 promise 对象fulfilled,会给他传入一个参数 value,将这个结果加入到 outputs 数组// (3)如果这个 promise 对象 rejected,直接结束整个函数,因为 promise.all是如果有一个失败,那么直接算失败returnnewPromise((resolve,reject)=>{promises.forEach((promise,index)=>{Promise.resolve(promise).then((val)=>{outputs[index]=val;resolveCounter++;if(resolveCounter==promises.length){resolve(outputs);}}).catch(reject);});});}

(完整版)如果代码要支持 Iterable:

核心思路(只加 2 行)

  1. 把传入的 Iterable 转成数组
  2. 后面的逻辑继续按数组处理
// 1. 参数必须是 iterableif(promises==null||typeofpromises[Symbol.iterator]!=='function'){thrownewTypeError("参数必须是一个可迭代对象");}// 2.最小关键改动:把 iterable 转成数组constlist=Array.from(promises);

3. async/await

3.1 async/await 是什么?

在 JavaScript 中,async/await是一种让异步(非同步)操作更容易理解和管理的语法。它建立在 Promise 的基础上,但提供了更简洁、更直观的方式来处理异步操作。

3.2 async语法

使用async关键字声明的函数式为异步函数式,异步函数式会返回一个 Promise 对象,而不直接返回函数式执行的结果。让我们通过示例来了解:

  • 下方普通函式f1()直接返回字串"Hello! ExplainThis!"
// 普通函式functionf1(){return"Hello! ExplainThis!";}f1();// 輸出: "Hello! ExplainThis!"
  • 加上 async
// 异步函数asyncfunctionf2(){return"Hello! ExplainThis!";}f2();// 输出: Promise {<fulfilled>: 'Hello! ExplainThis!'}

  • 由于async函式总是返回一个 Promise 对象,如果要获取该 Promise 的解析值,可以使用.then()方法:
asyncfunctionf2(){return"Hello! ExplainThis!";}f2().then((result)=>{console.log(result);// "Hello! ExplainThis!"});

3.3 await语法

await是一个关键字,用于等待一个承诺完成或拒绝。它通常与async函式一起使用,因为只有在async函式内部或模组的配件,才能使用await

使用await时,程序会暂停执行该async函式,直到await等待的 Promise 完成并回传结果后,才会继续往下执行。让我们透过下面的示例来了解:

asyncfunctiongetData(){// await 等待 fetch 这个非同步函数返回一个 Promise 并解析它constres=awaitfetch("https://example.com/data");// await 等待上一步的 Promise 解析后,再解析它的 JSON 资料constdata=awaitres.json();// 前面两步都完成后,才会执行这一行并打印出资料console.log(data);}getData();
  • 需要注意的是await等待的 Promise 完成并回传结果后,其实是拿到 promise 对象的[[PromiseResult]] ,就比如 fetch就是得到一个 promise 对象,await 后拿到他的[[PromiseResult]]

使用 await 要注意的几点

  • 在非 async 函数中使用 await 会报 SyntaxError 的错误

    functionf(){letpromise=Promise.resolve("Hello! ExplainThis!");letresult=awaitpromise;}// Uncaught SyntaxError: await is only valid in async functions and the top level bodies of modules
  • top-level await:

模块异步初始化问题:

比如 config.js中要首先拉接口配置,初始化 SDK,是异步的.但是别的文件中使用是 import ,从使用方的语义上看,import 一个模块时,默认希望这个模块导出的值已经准备,所以如果直接用那么可能会导致,别的文件中拿到的是 undefined,无法使用,如果让使用方去接受 promise.then 等待,又不是很合适,属于是强加给了使用方不合适的心理体验.有了 await 后,就可以直接在 config.js中直接拿到返回的结果,供使用方使用了

为什么不能在模块提供方中使用 promise.then等着返回呢?

是因为 promise.then还是不能阻塞这个过程,最后提供的值可能是空的,但是也会被导出

top-level await 指的是在 ES Module 的顶层作用域中直接使用 await,而不需要再包一层 async 函数。

它主要解决的是模块异步初始化的问题。

在没有 top-level await 的时候,如果模块内部需要异步获取数据,通常只能:
1. 使用方承担可能是空的风险,模块提供方的值不一定准备好
2. 或者直接导出一个 Promise,让使用方自己用 promise 自己等待

例如:

// config.jsexportconstdataPromise=getData();
// main.jsimport{dataPromise}from'./config.js';constdata=awaitdataPromise;

这种方式虽然可行,但会让模块的使用方也要关心异步初始化过程。

而有了 top-level await 之后,模块可以在加载阶段直接等待异步结果:

// config.jsconstdata=awaitgetData();export{data};

样 config.js 会先完成异步初始化,再对外导出结果。
其他模块在 import 时,拿到的就是已经准备好的数据。

因此,top-level await 可以理解为:

让模块自身具备异步初始化能力,而不是把等待逻辑交给外部。

3.4 如何使用 async/await?

使用async/await可以将异步代码写成类似同步的形式,使其更易读、且更易维护。让我们先看一个使用 Promise 写的 getData 函数例子:

先来看用 Promise 来写一个getData函式的例子:

functiongetData(url){returnnewPromise((resolve, reject)=>{fetch(url).then((res)=>res.json()).then((data)=>resolve(data)).catch((error)=>reject(error));});}getData("https://example.com/data").then((data)=>console.log(data)).catch((error)=>console.error(error));

在这个例子中,getData函式使用 Promise 来处理异步操作。我们需要使用.then().catch()方法来获取结果或错误。

现在,我们使用async/await来重写getData函式:

asyncfunctiongetData(url){try{constres=awaitfetch(url);constdata=awaitres.json();console.log(data);}catch(error){console.error(error);}}getData("https://example.com/data");
  • 使用async关键字定义一个异步函式,该函式会返回一个 Promise 对象。
  • 在异步函式中,使用await等待 Promise 的完成,并直接返回结果。
  • 使用try...catch捕获错误,使得错误处理更加方便和直观。
  • 可以看到,使用async/await后,代码变得更加清晰和易于理解。

async/await 与 Promise 的差别?

async/await和 Promise 都是用于处理异步操作的方式,但它们有以下一些差异:

  1. 语法:async/await提供了更简洁、更直观的语法,使得异步代码更易读和维护。Promise 则需要使用thencatch方法来处理结果和错误,语法上较为冗长。
  2. 错误处理: 在async/await中,可以直接使用try...catch来捕获错误,而在 Promise 中需要使用catch方法。
  3. 代码流程:async/await可以使异步代码看起来更像同步代码,更容易阅读和理解。Promise 的代码流程则较为不连贯
http://www.cnnetsun.cn/news/1713717.html

相关文章:

  • JAVA无人共享无人机赁柜预约小程序源码代码
  • OpenClaw数据清洗:Qwen3-14b_int4_awq智能修复残缺Excel表格
  • Qwen3.5-Plus Apache Tomcat 9、10 和 11 的核心区别在于支持的规范版本、命名空间(Package Name)以及最低 JDK 要求
  • OpenClaw配置优化:Qwen2.5-VL-7B的vLLM参数调优指南
  • OpenClaw+Qwen3-4B旅行规划:自动生成行程与预订建议
  • 从“单模型黑箱”到“多智能体博弈”:PediaMind 架构选型与核心优势解析
  • 在kali上创建DVWA靶机实验
  • 嵌入式开发者必看:GitHub高星项目实战解析
  • SEO_资深运营揭秘,长期稳定排名的SEO策略介绍
  • OpenClaw极限测试:Qwen3-14B镜像连续处理1000份文档报告
  • 运放稳定性补偿实战:从Riso到双反馈,如何为你的MOSFET驱动电路‘降噪’
  • 万物皆可skill
  • 为什么顶尖公司的工程师越来越难追上?
  • 零代码自动化:OpenClaw+Qwen3-14B可视化规则配置
  • 避坑指南:ESP32-S3驱动ILI9488屏显示OV2640画面,这些时序和内存问题你遇到了吗?
  • SAP增强中多线程STARTING NEW TASK实现BAPI事务提交的实践指南
  • 逻辑器件设计中的总线保持(Bus Hold)功能解析与实战案例
  • 告别手动抄表!用Python+ADS一键导出TwinCAT3数组到Excel表格
  • 避开网络限制:用Docker在本地或内网服务器部署Gemini Pro Chat的完整指南
  • 微信小程序地图气泡实战:从callout到customCallout的性能与兼容性深度解析
  • 手把手教你为FAST-LIVO2配置海康相机:MVS SDK封装、时间戳同步与catkin_make编译要点
  • 绘画人必备!PureRef+Snipaste组合使用技巧,让你的参考图管理效率翻倍
  • 免费域名会不会对网站SEO造成影响_免费域名对网站性能和访问速度有影响吗
  • 形式验证实战:5个降低状态空间复杂度的黑科技(附内存控制器案例)
  • 智能能耗管理系统如何助力轨道交通实现绿色低碳运营
  • 保姆级教程:用Google Antigravity和Gemini 3 Pro,5分钟搞定一个加密货币看板React应用
  • Python实战:用ARIMA-LSTM混合模型预测股票价格(附完整代码)
  • 避坑指南:用ModelScope玩转speech_campplus_sv声纹识别,别再踩‘model_cfg‘这个坑了
  • 全网最透彻:JWT Token 到底是什么?原理+结构+流程图+面试考点
  • 嵌入式开源项目解析与工程化实践