为什么引入Promise?

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

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

第一种:callback函式 回调函数

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

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

1.promise 的基本认识

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

Promise的三种状态

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

image-20251229165028835

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

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

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

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

image-20251229173309721

拓展(便于理解):一个 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)的框架,同步执行

new Promise((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)

本质上可以理解为:
const p1 = p.then(fn1)
const p2 = p1.then(fn2)
const p3 = 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

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

外部示例如下:

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”

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

常见使用场景:

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

来看一个小例子:

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 是数组的情况

        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. 后面的逻辑继续按数组处理
 // 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!"
// 普通函式
function f1() {
  return "Hello! ExplainThis!";
}

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

f2(); // 输出: Promise {<fulfilled>: 'Hello! ExplainThis!'}

image-20251229173838458

  • 由于async函式总是返回一个 Promise 对象,如果要获取该 Promise 的解析值,可以使用.then()方法:
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 完成并回传结果后,才会继续往下执行。让我们透过下面的示例来了解:

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 的错误

    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:

模块异步初始化问题:

比如 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.js
export const dataPromise = getData();
// main.js
import { dataPromise } from './config.js';
const data = await dataPromise;

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

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

// config.js
const data = await getData();
export { data };

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

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

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

3.4 如何使用 async/await?

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

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

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 函式:

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 的代码流程则较为不连贯
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐