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

文章目录

  • 为什么引入Promise?
  • [1.promise 的基本认识](#1.promise 的基本认识)
  • 2.promise的API
    • [2.1promise 构造/创建类()](#2.1promise 构造/创建类())
    • [2.2 实例方法-promise.then/.catch](#2.2 实例方法-promise.then/.catch)
    • [2.3 Promise 组合类 APi](#2.3 Promise 组合类 APi)
      • [2.3.1 promise.race()](#2.3.1 promise.race())
      • [2.3.2 Promise.all()](#2.3.2 Promise.all())
  • [3. async/await](#3. async/await)
    • [3.1 async/await 是什么?](#3.1 async/await 是什么?)
    • [3.2 async语法](#3.2 async语法)
    • [3.3 await语法](#3.3 await语法)
    • [3.4 如何使用 async/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 (抽象)大致有这些内部槽

txt 复制代码
[[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)的框架,同步执行

javascript 复制代码
new Promise((resolve, reject) => {}
  • 调用 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 本身并不会立刻执行回调,

而是等待上一个 Promise 状态确定:

  • 若 Promise 变为 fulfilled(被 resolve),则执行 .then 中的 onFulfilled 回调

  • 若 Promise 变为 rejected(被 reject),则跳过 onFulfilled,失败状态继续向下传播

这里的 fulfilled / rejected 仅指 Promise 状态,与 HTTP 请求是否成功、业务是否成功无关。

.then() 一定会返回一个新的 Promise。这是 Promise 链能一直"接着写"的根本原因

.then 的完整语法其实是:

javascript 复制代码
promise.then(onFulfilled, onRejected)

所以

json 复制代码
.then(
  (res) => {
    console.log(res);
  },
  (reason) => {
    console.log(reason);
  }
);
位置 什么时候执行 参数来自哪里
第一个函数 Promise 成功 resolve(value)
第二个函数 Promise 失败 reject(reason)

.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

javascript 复制代码
function promiseRace(promises) {
  return new Promise((resolve, reject) => {
    for (const p of promises) {
      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

外部示例如下:

javascript 复制代码
const p1 = new Promise(r => setTimeout(() => r('A'), 1000));
const p2 = new Promise(r => setTimeout(() => r('B'), 500));
const p3 = new Promise(r => setTimeout(() => r('C'), 1500));

const raceP = 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"

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

常见使用场景:

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

来看一个小例子:

javascript 复制代码
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((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 是数组的情况

javascript 复制代码
        function promiseAll(promises)
        {
            // 如果参数不是数组,返回一个 JS 的类型错误
            if(!Array.isArray(promises))
            {
                return new TypeError("参数必须是一个数组");
            }
            //如果输入是空数组,就返回空数组,根据定义
            if(promises.length===0)
            {
                return Promise.resolve([]);
            }
            //定义一个输出结果和计数器记录数组长度,把最终结果存在这里,开始核心的逻辑
            const outputs=[];
            let resolveCounter=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是如果有一个失败,那么直接算失败
            return new Promise((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. 后面的逻辑继续按数组处理
javascript 复制代码
 // 1. 参数必须是 iterable
  if (promises == null || typeof promises[Symbol.iterator] !== 'function') {
    throw new TypeError("参数必须是一个可迭代对象");
  }

  // 2.最小关键改动:把 iterable 转成数组
  const list = Array.from(promises);

3. async/await

3.1 async/await 是什么?

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

3.2 async语法

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

  • 下方普通函式f1()直接返回字串"Hello! ExplainThis!"
javascript 复制代码
// 普通函式
function f1() {
  return "Hello! ExplainThis!";
}

f1(); // 輸出: "Hello! ExplainThis!"
  • 加上 async
javascript 复制代码
// 异步函数
async function f2() {
  return "Hello! ExplainThis!";
}

f2(); // 输出: Promise {<fulfilled>: 'Hello! ExplainThis!'}
  • 由于async函式总是返回一个 Promise 对象,如果要获取该 Promise 的解析值,可以使用.then()方法:
javascript 复制代码
async function f2() {
  return "Hello! ExplainThis!";
}

f2().then((result) => {
  console.log(result); // "Hello! ExplainThis!"
});

3.3 await语法

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

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

javascript 复制代码
async function getData() {
  // await 等待 fetch 这个非同步函数返回一个 Promise 并解析它
  const res = await fetch("https://example.com/data");

  // await 等待上一步的 Promise 解析后,再解析它的 JSON 资料
  const data = await res.json();

  // 前面两步都完成后,才会执行这一行并打印出资料
  console.log(data);
}

getData();
  • 需要注意的是await等待的 Promise 完成并回传结果后,其实是拿到 promise 对象的[[PromiseResult]] ,就比如 fetch就是得到一个 promise 对象,await 后拿到他的[[PromiseResult]]

使用 await 要注意的几点

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

    javascript 复制代码
    function f() {
      let promise = Promise.resolve("Hello! ExplainThis!");
      let result = await promise;
    }
    
    // Uncaught SyntaxError: await is only valid in async functions and the top level bodies of modules
  • top-level await:

    • ES Module(ESM)是 JavaScript 官方的模块系统,用 import / export,有文件级作用域,支持静态分析,是现在前端和 Node 的标准模块方案。

    在"没有顶层 await"的年代,模块里想拿异步数据,只能用 Promise + then 的绕法:

    javascript 复制代码
    //config.js
    import getData from "./getData.js";
    
    let data;
    
    getData().then((result) => {
      data = result;
      // ...使用data
    });
    
    export const dataPromise = getData();
    
    // main.js
    import { dataPromise } from "./config.js";
    
    const data = await dataPromise;
    console.log(data);
    • import getData from "./getData.js";从另一个模块引入一个函数 getData.其中getData 是一个返回 Promise 的异步函数

    • let data;先声明一个变量,此时data 还是 undefined

    • 调用异步函数,等 Promise resolve 之后,才把结果塞进 data

    模块加载时我没法直接 await,只能先声明一个变量,再等异步请求回来之后,在 then 里"补上"这个变量。

    模块加载完成时,data 并不是可用状态,模块"已经被别人 import 使用了",但数据还没准备好

    顶层 await 的时候

    javascript 复制代码
    //config.js
    const data = await getData();
    export { data }; 
    
    // main.js
    import { data } from './config.js';
    
    console.log(data); // ✅ 一定是已经准备好的数据
    • 不用再包一层 async 函数,模块在加载阶段会等待 await 完成后再继续执行依赖模块
    • config.js 在对外可用之前,必须先把 data 拉取完成。别的文件引入即可用
    • config.js 是一个 异步初始化模块

3.4 如何使用 async/await?

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

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

javascript 复制代码
function getData(url) {
  return new Promise((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 函式:

javascript 复制代码
async function getData(url) {
  try {
    const res = await fetch(url);
    const data = await res.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 的代码流程则较为不连贯
相关推荐
韩曙亮2 小时前
【Web APIs】移动端轮播图案例 ( 轮播图自动播放 | 设置无缝衔接滑动 | 手指滑动轮播图 | 完整代码示例 )
前端·javascript·css·html·轮播图·移动端·web apis
2501_946244782 小时前
Flutter & OpenHarmony OA系统图片预览组件开发指南
android·javascript·flutter
xu_duo_i3 小时前
vue3+element-plus图片上传,前端压缩(纯函数,无插件)
前端·javascript·vue.js
POLITE33 小时前
Leetcode 240. 搜索二维矩阵 II JavaScript (Day 9)
javascript·leetcode·矩阵
bigHead-3 小时前
前端双屏显示与通信
开发语言·前端·javascript
PieroPc3 小时前
Html +css+js 写的一个小商城系统(POS系统)
javascript·css·html
一颗小青松3 小时前
vue 腾讯地图经纬度转高德地图经纬度
前端·javascript·vue.js
怕浪猫11 小时前
第一章 JSX 增强特性与函数组件入门
前端·javascript·react.js