JavaScript 中的 Promise 详解

前言

在现代 Web 开发中,异步编程是一个相当重要的概念。异步编程是一种编程模式,旨在处理耗时操作而不阻塞主线程。与同步编程不同,异步编程允许程序在等待某些操作完成时继续执行其他任务。JavaScript 中的 Promise 是一种用于处理异步操作的强大工具。本文将深入探讨 Promise 的概念、用法及其在实际开发中的应用。

什么是 Promise?

Promise 是 JavaScript 中用于表示异步操作最终完成(或失败)及其结果值的对象。它可以看作是一个容器,封装了一个未来可能会被填充的值。Promise 有三种状态:

  1. Pending(进行中):初始状态,既不是成功也不是失败。
  2. Fulfilled(已成功):操作成功完成,Promise 有了一个结果值。
  3. Rejected(已失败):操作失败,Promise 有了一个失败原因。

创建 Promise

要创建一个 Promise,可以使用Promise构造函数。该构造函数接受一个执行器函数作为参数,该函数包含两个参数:resolvereject。当异步操作成功时,调用resolve,当失败时,调用reject

javascript 复制代码
const myPromise = new Promise((resolve, reject) => {
  // 模拟异步操作
  setTimeout(() => {
    const success = true; // 模拟操作结果
    if (success) {
      resolve("操作成功!");
    } else {
      reject("操作失败!");
    }
  }, 2000);
});

使用 Promise

一旦创建了 Promise,可以使用then()方法来处理成功的结果,使用catch()方法来处理失败的情况。

javascript 复制代码
myPromise
  .then((result) => {
    console.log(result); // 输出: 操作成功!这里的result是resolve传递的值
  })
  .catch((error) => {
    console.error(error); // 输出: 操作失败!这里的error是reject传递的值
  });

Promise 原理

有限状态机

Promise 本质上是一个有限状态机。它有且仅有三种状态:Pending、Fulfilled 和 Rejected。状态的转换只能从 Pending 到 Fulfilled 或 Rejected,且状态改变是不可逆的,一旦状态改变就不会再变。

执行器与同步执行

new Promise(executor) 时,传入的函数executor是立即同步执行的。这意味着 Promise 的状态转换(调用resolvereject)也是同步发生的。

javascript 复制代码
const promise = new Promise((resolve, reject) => {
  console.log("1. Executor是同步的,立即执行");
  // 模拟异步操作
  resolve("2. Promise状态变为Fulfilled"); //状态改变
});

promise.then((value) => {
  console.log(value);
});

console.log("3. 这行代码在Executor之后执行");

/*
上面的代码输出顺序为:

1. Executor是同步的,立即执行
3. 这行代码在Executor之后执行
2. Promise状态变为Fulfilled

*/

发布-订阅模式

什么是发布-订阅模式?

发布-订阅模式(Publish-Subscribe Pattern)通常简称为 Pub-Sub,是一种消息范式。发布-订阅模式 (Publish-Subscribe Pattern),通常简称为 Pub-Sub,是一种消息范式。用最通俗的话来概括:它是一种"一对多"的依赖关系,让多个观察者(订阅者)同时监听某一个主题对象(发布者),当这个主题对象的状态发生变化时,会通知所有订阅者。这是 Promise 实现异步处理的核心机制。

Promise 中的发布-订阅模式

当你调用promise.then(callback)时,如果 Promise 还处于 Pendding 状态,callback 函数不能被立即执行。所以,Promise 会将 callback 函数存储在一个数组中,这实际上就是订阅。当异步操作结束后,调用resolve(value)reject(value)时,Promise 会遍历这个数组,依次将 value 传递给每个 callback 函数并执行它们,这就是发布。和一般的发布-订阅模式不同,promise 的发布-订阅稍微特殊一点,它的状态一旦改变(发布了一次),就凝固了。后续再有新的"订阅者"进来(再调用 .then),会直接拿到结果,而不需要再次触发发布动作。

下面是一个简单的示例,展示了 Promise 中发布-订阅模式的实现原理:

javascript 复制代码
class EventEmitter {
  constructor() {
    //1.缓存列表,用来存放所有的订阅回调函数
    //结构类似于{'click': [fn1, fn2], 'upload':[fn3]}
    this.events = {};
  }

  //2.订阅(Subscribe)
  //相当于点击了"关注"按钮
  on(eventName, callback) {
    //如果还没有这个事件的队列,就初始化一个空数组
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    //把回调函数推入队列
    this.events[eventName].push(callback);
  }

  //3.发布(Publish)
  //相当于发布了一条动态,通知所有关注我的人
  emit(eventName, ...args) {
    //取出该事件所对应的所有回调函数
    const callbacks = this.events[eventName];

    if (callbacks) {
      //依次执行回调函数,并传入参数
      callbacks.forEach((callback) => {
        callback(...args);
      });
    }
  }

  //4.取消订阅(Unsubscribe)
  //相当于取关
  off(eventName, callback) {
    if (!this.events[eventName]) return;
    //过滤掉要取消的那个函数
    this.events[eventName] = this.events[eventName].filter(
      (cb) => cb !== callback
    );
  }
}

//使用示例

const bus = new EventEmitter();

bus.on("eat", (food) => {
  console.log(`我正在吃${food}`);
});

bus.on("eat", (food) => {
  console.log(`他也想吃${food}`);
});

console.log("还没开饭");

bus.emit("eat", "咖喱饭"); // 发布事件

/*
输出结果:
还没开饭
我正在吃咖喱饭
他也想吃咖喱饭
*/

在上面的代码中,EventEmitter类实现了一个简单的发布-订阅系统。on方法用于订阅事件,emit方法用于发布事件,off方法用于取消订阅。通过这种方式,Promise 能够有效地管理异步操作的回调函数,实现异步处理。

微任务队列

微任务队列导入

上面的例子中提到了队列的概念。在 JavaScript 中,微任务队列(Microtask Queue)是一个特殊的任务队列,用于存放需要在当前执行栈清空后立即执行的任务。即使resolve是同步调用的,.then中的代码也不会立即执行。Promise 的回调函数(通过thencatch注册的函数)会被放入微任务队列中。要解释清楚这个机制,下面我来详细介绍一下 JavaScript 的事件循环(Event Loop)。

JavaScript 的事件循环

JavaScript 是单线程语言,这意味着它一次只能执行一个任务。为了处理异步操作,JavaScript 引入了事件循环机制。可以将三者的关系理解为:银行柜台办业务。

宏任务

宏任务(MacroTask)是"主流"的异步任务,可以理解为银行柜台等待办理业务的一个个独立的客户。每次执行栈(主线程)清空后,事件循环会从宏任务队列中取出下一个宏任务来执行,且每次只能执行一个宏任务,执行完一个后,浏览器会去检查微任务队列,或者进行页面渲染。常见的宏任务包括:

  • setTimeout
  • setInterval
  • setImmediate (Node.js 环境)
  • I/O 操作
  • UI 渲染
微任务

微任务(MicroTask)是依附于当前宏任务的紧急任务,它的优先级要高于宏任务,当前宏任务结束后,下一个宏任务开始前,会先清空微任务队列。可以理解为当前正在办业务的客户存完钱后又想办点别的业务,这时候这个客户具有"插队"的权利来办理业务。一旦开始执行微任务,就会把微任务队列清空为止,包括在执行微任务期间新产生的微任务。常见的微任务包括:

  • Promise的回调函数(通过thencatch注册的函数)
  • MutationObserver
  • process.nextTick (Node.js 环境)
事件循环

事件循环(Event Loop)是 JavaScript 处理异步操作的核心机制。它不断地检查执行栈和任务队列,当执行栈为空时,它会从宏任务队列中取出下一个宏任务来执行。在所有同步代码或每个宏任务执行完后,事件循环会检查微任务队列,并执行所有的微任务,如果执行微任务产生了新的微任务,那新的微任务就会被加到队尾并在本轮一起执行完,直到微任务队列为空,然后才会继续处理下一个宏任务。可以将事件循环理解为银行的叫号系统和柜员的工作流程。事件循环的标准流程大致如下:

  • 执行栈 (Call Stack): 首先执行主线程中的同步代码。
  • 清空微任务队列: 当同步代码执行完毕(或一个宏任务执行完毕),立即检查微任务队列。
  • 如果有微任务,全部执行完(清空队列)。
  • 如果在执行微任务时产生了新的微任务,继续加到队尾并在本轮一起执行完。
  • 渲染 UI (可选): 浏览器判断是否需要更新页面视图。
  • 执行一个宏任务: 从宏任务队列中取出一个(最老的)任务执行。
  • 回到步骤 2(循环往复)。
事件循环示例
javascript 复制代码
console.log("1. Script Start (同步)");

//宏任务1
setTimeout(() => {
  console.log("4. setTimeout (宏任务)");

  Promise.resolve().then(() => {
    console.log("5. Promise in setTimeout (微任务)");
  });
}, 0);

// 微任务1
new Promise((resolve) => {
  console.log("2. Promise Executor (同步)");
  resolve();
}).then(() => {
  console.log("3. Promise then (微任务)");
});

console.log("6. Script End (同步)");

/*输出结果:
1. Script Start (同步)
2. Promise Executor (同步)
6. Script End (同步)
3. Promise then (微任务)
4. setTimeout (宏任务)
5. Promise in setTimeout (微任务)
*/
时间节点 正在执行 (Call Stack) 微任务队列 (Micro Queue) 宏任务队列 (Macro Queue)
1. 启动 Script (Macro #1) [] []
2. 运行中 console.log(1)... [Promise回调] [setTimeout回调]
3. Script 结束 (空) [Promise回调] [setTimeout回调]
4. 清算微任务 执行 Promise 回调 [] [setTimeout回调]
5. 第一轮结束 (空) [] [setTimeout回调]
6. 启动第二轮 执行 setTimeout [] []
7. 运行中 console.log(4)... [内部新回调] []
8. 宏任务结束 (空) [内部新回调] []
9. 清算微任务 执行 内部新回调 [] []

Promise 链式调用

Promise 支持.then()链式调用,这是因为then()catch()方法内部会返回一个新的 Promise 实例(不是原来那个),使得可以在前一个 Promise 处理完成后继续处理下一个 Promise。如果前一个thencatch中返回一个值,这个值会被传递给下一个then的回调函数;如果返回一个 Promise,则下一个then会等待这个 Promise 状态改变后再执行。

javascript 复制代码
const promise = new Promise((resolve, reject) => {
  resolve(1);
});

promise
  .then((value) => {
    console.log(value); // 输出: 1
    return value + 1;
  })
  .then((value) => {
    console.log(value); // 输出: 2
    return new Promise((resolve) => {
      setTimeout(() => resolve(value + 1), 1000);
    });
  })
  .then((value) => {
    console.log(value); // 输出: 3 (延迟1秒后)
  });

Promise 的简易版原理实现

下面是一个简易版的 Promise 实现,展示了其基本原理:

javascript 复制代码
class MyPromise {
    constructor(executor) {
        this.state = 'pending';// 初始状态
        this.value = undefined;// 成功的值
        this.callbacks = [];//订阅者队列

        const resolve = (value) => {
            if(this.state === 'pending') {
                this.state = 'fulfilled';
                this.value = value;
                //发布:执行所有订阅的回调(需放入微任务队列,此处简化直接调用)
                this.callbacks.forEach(cb = > cb.onFulfilled(value));
            }
        }

        const reject = (reason) => {
            if(this.state === 'pending') {
                this.state = 'rejected';
                this.value = reason;
                //发布:执行所有订阅的回调(需放入微任务队列,此处简化直接调用)
                this.callbacks.forEach(cb => cb.onRejected(reason));
            }
        }

        //立即执行 executor
        try {
            executor(resolve, reject);
        } catch (error) {
            reject(error);
        }
    }

    then(onFulfilled, onRejected) {
        return new MyPromise((resolve, reject) => {
            //如果是pending状态,存入订阅队列
            if(this.state === 'pending') {
                this.callbacks.push({
                    onFulfilled: val => {
                        try {
                            const result = onFulfilled(val);
                            resolve(result);//处理链式调用
                        } catch (err) { reject(err); }
                    }
                    onRejected: reason => {
                        try {
                            const result = onRejected(reason);
                            resolve(result);
                        } catch (err) { reject(err); }
                    }
                })
            } else if(this.state === 'fulfilled') {
                //如果已经是fulfilled状态,利用微任务执行
                queueMicrotask(() => {
                    const result = onFulfilled(this.value);
                    resolve(result);//处理链式调用
                })
            }
        })
    }
}

Async/Await 简介

async 和 await 是基于 Promise 的语法糖,能够让我们用同步代码的风格编写异步代码,这样可以提高代码的可读性。

async 和 await 解决了什么问题?

Promise 虽然解决了"回调地狱"的问题,但引入了链式复杂性,使得代码在处理多个异步操作时任然显得冗长和难以阅读。async/await 通过让异步代码看起来像同步代码,极大地提升了代码的可读性和维护性。

如果链式调用太长

javascript 复制代码
fetchData()
  .then((data) => processData(data))
  .then((processedData) => saveData(processedData))
  .then(() => console.log("All done!"))
  .catch((error) => console.error(error));

这样的代码虽然避免了回调地狱,但仍然显得冗长,且可读性差。

使用 async/await 重写

javascript 复制代码
async function handleData() {
  try {
    const data = await fetchData();
    const processedData = await processData(data);
    await saveData(processedData);
    console.log("All done!");
  } catch (error) {
    console.error(error);
  }
}

handleData();

通过使用 async/await,代码变得更加简洁和易读,逻辑流程也更加清晰。

async 和 await 的使用

使用示例

要使用 async/await,只需在函数前加上async关键字,并在需要等待 Promise 结果的地方使用await关键字。

javascript 复制代码
async function fetchData() {
  // 模拟异步数据获取
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("数据已获取");
    }, 1000);
  });
}

async function main() {
  console.log("开始获取数据...");
  const data = await fetchData();
  console.log(data); // 输出: 数据已获取
  console.log("数据处理完成");
}

main();

async 和 await 原理

async 函数的原理

async关键字的作用是为函数提供一个 Promise 包装器和状态机环境。当一个函数被定义为async函数时,JS 引擎会保证该函数的返回值永远是一个 Promise 对象。如果函数内部返回一个非 Promise 值,JS 引擎会自动将其包装成一个已解决的 Promise。

javascript 复制代码
async function f() {
  return 1;
}

async function g() {
  throw new Error("出错了");
}

上面的代码等同于下面的实现:

javascript 复制代码
function f() {
  return Promise.resolve(1);
}

function g() {
  return Promise.reject(new Error("出错了"));
}

await 的原理

await是实现暂停和恢复的关键。当 JS 引擎遇到await时,它会执行以下三个核心步骤:

等待 Promise 结果

引擎首先评估await右侧的表达式,如果右侧的是一个非 Promise 值(如 await 10),引擎会立即将其视为Promise.resolve(10),并以微任务的形式继续执行(当在大多数现代 JS 引擎中,这种情况下会优化成同步执行)。如果右侧是一个 Promise 对象,JS 引擎会"暂停"当前 async 函数的执行,指导该 Promise 状态确定。

让出控制权(Yield)

这是await关键的动作。首先引擎暂停函数,将当前 async 函数的执行状态保存在内部的"状态机"中,标记为暂停。然后让出线程,主线程立即恢复执行async函数后面的同步代码。这保证了await不会阻塞整个 JS 进程。

作为微任务恢复执行

await等待的 Promise 状态确定(无论是 fulfilled 还是 rejected)后,引擎会调度,将async函数中await后面的所有代码作为一个回调,放入微任务队列。之后在 Event Loop 的下一个微任务清算周期,该回调被执行,async函数从上次暂停的地方恢复执行,并拿到 Promise 的结果。

核心机制

要真正理解 async 和 await 的原理,需要知道它在底层是如何被转换的。在概念上,async/await 代码被编译成了一个使用Generator函数和Promise包装的函数。

转换示例
javascript 复制代码
async function fetchUser() {
  const id = await getId(); //暂停点1
  const info = await getInfo(id); //暂停点2
  return info;
}

// 转换后等同于:

function fetchUser() {
  //1.返回一个Promise包装整个逻辑
  return new Promise((resolve, reject) => {
    //2.启动一个Generator状态机
    const generator = actualFunctionGenerator();

    //3.核心驱动函数
    function step(nextPromise) {
      const next = generator.next(nextPromise); //启动/恢复函数
      if (next.done) {
        //如果运行到函数末尾,Promise成功
        return resolve(next.value);
      }
      //如果遇到yield(相当于await),继续驱动
      Promise.resolve(next.value).then(step, reject);
    }

    //首次运行
    step();
  });
}

这里await对应着yield,标记暂停点,让出控制权。Promise.resolve.then()对应着恢复执行,确保await后面的代码以微任务的形式被调度。因此我们可以得出,async/await 的原理就是引擎将同步的写法,在编译阶段高效地转化成了基于 Promise 和微任务的异步流程控制。

结语

到此为止,我们已将 JavaScript 中的 Promise 及其相关概念进行了全面的介绍。从 Promise 的基本概念、状态管理,到其背后的发布-订阅模式和微任务队列机制,再到 async/await 的语法糖及其原理,我们深入探讨了异步编程在 JavaScript 中的实现方式。现在我们可以把这三个概念穿通一下:Event Loop 是底层机制->Promise 是核心对象->Async/Await 是最佳写法。希望本文对你有所帮助。

相关推荐
jason_yang13 分钟前
vue3中createApp多个实例共享状态
javascript·vue.js
_瑶瑶_14 分钟前
浅记一下ElementPlus中的虚拟化表格(el-table-v2)的简单使用
前端·javascript
拉不动的猪31 分钟前
Axios 请求取消机制详解
前端·javascript·面试
Heo2 小时前
关于XSS和CSRF,面试官更喜欢这样的回答!
前端·javascript·面试
徐小夕2 小时前
耗时一周,我把可视化+零代码+AI融入到了CRM系统,使用体验超酷!
javascript·vue.js·github
5***a9753 小时前
React Native性能优化技巧
javascript·react native·react.js
A3608_(韦煜粮)3 小时前
深入理解React Hooks设计哲学与实现原理:从闭包陷阱到并发模式
javascript·性能优化·react·前端开发·react hooks·并发模式·自定义hooks
玉宇夕落3 小时前
🔁 字符串反转 × 两数之和:前端面试高频题深度拆解(附5种反转写法 + 哈希优化)
javascript
神秘的猪头3 小时前
🧱 深入理解栈(Stack):原理、实现与实战应用
前端·javascript·面试
StockPP3 小时前
印度尼西亚股票多时间框架K线数据可视化页面
前端·javascript·后端