从 Promise 到 async/await:一次把 JavaScript 异步模型讲透
文章目录为什么引入Promise1.promise 的基本认识2.promise的API2.1promise 构造/创建类()2.2 实例方法-promise.then/.catch2.3 Promise 组合类 APi2.3.1 promise.race()2.3.2 Promise.all()3. async/await3.1 async/await 是什么3.2 async语法3.3 await语法3.4 如何使用 async/await?为什么引入PromiseJavaScript 有一个重要的概念——异步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]] // 是否已处理 rejection2.promise的API2.1promise 构造/创建类()用来创建或得到 PromiseAPI作用new Promise(executor)创建一个可手动控制状态的 PromisePromise.resolve(value)把任意值转换为 fulfilled PromisePromise.reject(reason)创建一个 rejected PromisePromise(executor)的框架同步执行newPromise((resolve,reject){}Promise 构造函数会自动传给你两个函数参数用来手动改变这个新 Promise 的状态。这里传进去的这个函数通常叫执行器函数调用 resolve()函数会让 promise 对象的状态从 pendingPromise.resolve(value)作用把任何值 x 转换成一个 Promise如果 x 本来就是 Promise → 原样“接管”如果 x 是普通值 → 包装成 fulfilled Promise返回值Promise2.2 实例方法-promise.then/.catchAPI作用.then()处理 fulfilled.catch()处理 rejected.finally()不关心结果做清理.then() 为什么能链式调用.then() 本身不会立刻执行回调它会等当前 Promise 状态确定后再执行• 如果 Promise 是 fulfilled执行 onFulfilled• 如果 Promise 是 rejected执行 onRejected如果提供了的话并且.then() 一定会返回一个新的 Promise。这就是 Promise 可以不断链式调用的根本原因。例如p.then(fn1).then(fn2).then(fn3)本质上可以理解为constp1p.then(fn1)constp2p1.then(fn2)constp3p2.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 变成了 rejectedB不会执行但是p2 仍然是 rejected同理 c 也是所以到 D 这D 执行2.3 Promise 组合类 APi2.3.1 promise.race()Promise.race 会返回一个新的 Promise采用最先完成的那个 Promise 的状态和值用 promise 内部槽的观点来理解Promise.race 会创建一个新的 Promise并在内部采用adopt最先 settle 的Promise 的[[PromiseState]] 与 [[PromiseResult]]而不继承其余内部槽。settle 是指不是 pedding 状态手写 promiseRacefunctionpromiseRace(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)表示这个外部函数是多个 promisefor (const p of promises)同步遍历所有 Promise给它们注册 .then / .catch 回调然后将最快的返回赋值给新的 promise外部示例如下constp1newPromise(rsetTimeout(()r(A),1000));constp2newPromise(rsetTimeout(()r(B),500));constp3newPromise(rsetTimeout(()r(C),1500));constracePpromiseRace([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”定义中有提到如果输入为空例如空数组就返回一个空数组常见使用场景多接口并发请求全部成功再渲染页面初始化依赖多个异步资源批量任务统一完成后再继续来看一个小例子constpromise1Promise.resolve(3);constpromise242;constpromise3newPromise((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.length0){returnPromise.resolve([]);}//定义一个输出结果和计数器记录数组长度把最终结果存在这里开始核心的逻辑constoutputs[];letresolveCounter0;//1.promise.all最终要返回一个新的 promise 对象最外层应该是 promise executorresolve的返回值应该是 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(resolveCounterpromises.length){resolve(outputs);}}).catch(reject);});});}(完整版)如果代码要支持 Iterable核心思路只加 2 行把传入的 Iterable 转成数组后面的逻辑继续按数组处理// 1. 参数必须是 iterableif(promisesnull||typeofpromises[Symbol.iterator]!function){thrownewTypeError(参数必须是一个可迭代对象);}// 2.最小关键改动把 iterable 转成数组constlistArray.from(promises);3. async/await3.1 async/await 是什么在 JavaScript 中async/await是一种让异步非同步操作更容易理解和管理的语法。它建立在 Promise 的基础上但提供了更简洁、更直观的方式来处理异步操作。3.2 async语法使用async关键字声明的函数式为异步函数式异步函数式会返回一个 Promise 对象而不直接返回函数式执行的结果。让我们通过示例来了解下方普通函式f1()直接返回字串Hello! ExplainThis!// 普通函式functionf1(){returnHello! ExplainThis!;}f1();// 輸出: Hello! ExplainThis!加上 async// 异步函数asyncfunctionf2(){returnHello! ExplainThis!;}f2();// 输出: Promise {fulfilled: Hello! ExplainThis!}由于async函式总是返回一个 Promise 对象如果要获取该 Promise 的解析值可以使用.then()方法asyncfunctionf2(){returnHello! ExplainThis!;}f2().then((result){console.log(result);// Hello! ExplainThis!});3.3 await语法await是一个关键字用于等待一个承诺完成或拒绝。它通常与async函式一起使用因为只有在async函式内部或模组的配件才能使用await。使用await时程序会暂停执行该async函式直到await等待的 Promise 完成并回传结果后才会继续往下执行。让我们透过下面的示例来了解asyncfunctiongetData(){// await 等待 fetch 这个非同步函数返回一个 Promise 并解析它constresawaitfetch(https://example.com/data);// await 等待上一步的 Promise 解析后再解析它的 JSON 资料constdataawaitres.json();// 前面两步都完成后才会执行这一行并打印出资料console.log(data);}getData();需要注意的是await等待的 Promise 完成并回传结果后其实是拿到 promise 对象的[[PromiseResult]] 就比如 fetch就是得到一个 promise 对象,await 后拿到他的[[PromiseResult]]使用 await 要注意的几点在非 async 函数中使用 await 会报 SyntaxError 的错误functionf(){letpromisePromise.resolve(Hello! ExplainThis!);letresultawaitpromise;}// Uncaught SyntaxError: await is only valid in async functions and the top level bodies of modulestop-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.jsexportconstdataPromisegetData();// main.jsimport{dataPromise}from./config.js;constdataawaitdataPromise;这种方式虽然可行但会让模块的使用方也要关心异步初始化过程。而有了 top-level await 之后模块可以在加载阶段直接等待异步结果// config.jsconstdataawaitgetData();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{constresawaitfetch(url);constdataawaitres.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 都是用于处理异步操作的方式但它们有以下一些差异:语法:async/await提供了更简洁、更直观的语法使得异步代码更易读和维护。Promise 则需要使用then和catch方法来处理结果和错误语法上较为冗长。错误处理: 在async/await中可以直接使用try...catch来捕获错误而在 Promise 中需要使用catch方法。代码流程:async/await可以使异步代码看起来更像同步代码更容易阅读和理解。Promise 的代码流程则较为不连贯