JavaScript 异步编程中的 Promise 机制解析

185 阅读5分钟

一、了解进程与线程

在不同场景中,进程都可以用来描述该场景中的一个效果,线程进程里面的一个更小的单位,通常多个线程配合工作构成一个进程

  • 进程:操作系统资源分配的基本单位,每个进程拥有独立的内存空间和资源。一个进程可以有多个线程。比如在Windows系统中,一个运行的xx.exe就是一个进程。它是一个运行中的程序实例,负责完成特定任务。进程之间相互独立,切换时需要较大的系统开销。
  • 线程:进程中的执行单元,CPU调度的基本单位。线程共享进程的内存和资源,但每个线程有自己的栈和程序计数器。线程切换的开销较小,因此被称为轻量级进程。

二、v8 引擎的异步执行机制

js 默认是单线程的语言,因为 js 设定是为了做浏览器的脚本语言,尽量少的开销用户设备的性能。因为 js 的这种执行规则,导致我们在开发过程中时而会出现代码异步的情况。

  • 同步代码:按顺序执行,阻塞后续代码直至完成。
  • 异步代码:被挂起并放入任务队列,待同步代码执行完毕后进入微任务/宏任务队列。

v8运行一份js代码,会创建一个进程,从上往下执行代码,遇到同步代码就直接执行,遇到异步代码就跳过,先去执行后面的同步代码,等到后面的同步代码全部执行完毕后,再回过头执行异步代码。
以相亲函数案例说明传统模式的缺陷:

function date() {
  setTimeout(() => {
    console.log("相亲成功");
    return true;
  }, 1000);
}

function marry() {
  console.log("结婚");
}

date();
marry();

上述代码执行结果为结婚 相亲成功,其根本原因是v8引擎跳过异步代码继续执行后续同步代码,导致逻辑顺序错乱。


三、回调函数:异步编程的基础

1. 什么是回调函数?

回调函数是指将一个函数作为参数传递给另一个函数,并在外部函数内部调用这个传入的函数。在异步编程中,回调函数通常用于处理异步操作完成后的结果。

function fetchData(callback) {
    console.log("开始获取数据...");
    setTimeout(() => {
        const data = "这里是从服务器获取的数据";
        callback(data); // 当数据获取完成后,调用回调函数
    }, 2000); // 模拟延迟2秒
}

function handleData(data) {
    console.log("获取的数据:", data);
}

// 调用fetchData并传入handleData作为回调
fetchData(handleData);

在这个示例中,fetchData函数模拟了从服务器获取数据的过程,使用setTimeout模拟网络延迟。callback参数是我们传入的处理函数,当数据准备好后,通过callback(data)调用这个处理逻辑。

2. 回调函数的问题

早期浏览器广泛支持回调函数,适合简单的异步操作。但当嵌套过深时,代码的可读性差,维护困难,排查问题困难,形成回调地狱
回调地狱:多层嵌套导致代码可读性灾难。

API1(() => {
  API2(() => {
    API3(() => {
      // 业务逻辑
    });
  });
});

代码结构随异步层级增加呈"括号金字塔"形态,严重降低维护性。


四、Promise:异步编程的救星

1. Promise的基本概念

promise 是 es6 新增的一个语法,用来解决回调地狱的问题。promise 是一个构造函数,用来封装一个异步操作,并且可以获取到异步操作的结果。
promise 有三种状态: 待定态(pending)、兑现态(fulfilled)、拒绝态(rejected)。

  • pending:初始状态,既不是成功,也不是失败;

  • fulfilled​:操作成功完成,通过 resolve 方法传递结果。

  • rejected​:操作失败,通过 reject 方法传递错误信息。

promise 状态的改变只有两种: 从待定态到兑现态或者从待定态到拒绝态。一旦状态变为 fulfilledrejected,就不会再变(不可逆性)。

function fetchData() {
    return new Promise((resolve, reject) => {
        console.log("开始获取数据...");
        setTimeout(() => {
            const data = "这里是从服务器获取的数据";
            resolve(data); // 成功,返回结果
            // 如果出错可以调用 reject(error)
        }, 2000);
    });
}

// 调用 fetchData 并处理返回值
// 1. 执行 fetchData 函数,立即返回一个promise实例对象,但是此时该对象的状态是 pending(等待状态)
// 2. .then 立即触发,但是 then 里面的回调函数没有触发
// 3.等待 fetchData 函数里面的 resolve() 执行完毕,此时实例对象的状态会变更为 fulfilled (成功状态),此时 .then 里面的回调函数会触发执行
fetchData() 
    .then((data) => {
        console.log("获取的数据:", data);
    })
    .catch((error) => {
        console.error("发生错误:", error);
    });

在这个实例中,fetchData返回一个Promise对象。在Promise内部,我们仍然使用setTimeout模拟异步操作。数据成功时调用resolve(data),将结果传递给.then()中的方法;如果发生错误,则调用reject(error)并通过.catch()捕获。
Promise.then() 方法返回一个新的 Promise 实例,从而支持链式调用。如果回调函数返回一个值,新Promise将以该值为结果resolve

2. Promise的核心优势

特性传统回调Promise机制
代码结构嵌套层级加深链式调用(.then().then())
错误处理分散在各回调中统一错误捕获(.catch())
并行操作手动协调Promise.all()统一管理
状态追踪黑箱操作明确的pending/resolved状态

五、展望:从Promise到async/await

asyncawait本质上是生成器(Generator)与Promise的组合封装
async 函数​:用 async 声明的函数会自动返回一个 Promise 对象。无论函数内部返回的是否是Promise,都会被包装成 Promise。

async function example() { 
  return 42; // 自动包装为Promise.resolve(42)
}

await 表达式​:await 会暂停当前 async 函数的执行,等待后面的Promise完成。如果是非Promise值,会使用Promise.resolve()包装。

async function foo() {
  const result = await somePromise; // 暂停直到 somePromise 解决
  return result;
}

相比Promise链式调用,async/await使异步代码呈现同步代码的线性结构,消除了回调地狱问题。

// Promise链式调用
fetchData()
  .then(data => processData(data))
  .then(result => displayResult(result))
  .catch(error => handleError(error));

// async/await版本
async function handleData() {
  try {
    const data = await fetchData();
    const result = await processData(data);
    displayResult(result);
  } catch (error) {
    handleError(error);
  }
}