从“回调地狱”到“Promise天堂”:JavaScript异步编程的奇妙之旅

106 阅读8分钟

一、为什么需要 Promise?(背景)

JavaScript 是一门单线程语言,也就是说它一次只能执行一个任务。如果某个操作非常耗时(比如网络请求、文件读取),直接同步执行会阻塞整个页面,用户就什么都做不了了。

同步 vs 异步:

  • 同步任务:按顺序执行,当前任务没完成,后面的代码就得等。
  • 异步任务:交给浏览器其他线程去处理,完成后通过回调通知主线程继续执行。
console.log("1. 开始");

setTimeout(() => {
  console.log("2. 定时器");
}, 1000);

console.log("3. 结束");

// 输出顺序:
// 1. 开始 → 3. 结束 → 2. 定时器

所以我们发现:异步任务不会阻塞主线程,但执行顺序和写代码的顺序不一致。


二、Promise 解决了什么问题?

在 Promise 出现之前,我们使用回调函数来处理异步任务,容易出现“回调地狱”(Callback Hell)。

🔍 回调地狱是什么?

当你有多个异步操作需要依次执行时,每个操作都需要等待前一个操作完成才能开始。由于 JavaScript 中的异步操作是通过回调函数实现的,这就导致了层层嵌套的回调函数结构,这种结构被称为“回调地狱”。

示例:回调地狱

doSomething(function(result) {
  doNextThing(result, function(newResult) {
    doAnotherThing(newResult, function(finalResult) {
      // ...
    });
  });
});
  • 难以阅读:随着嵌套层级增加,代码变得难以理解和维护。
  • 错误处理复杂:每一层都需要单独处理错误,增加了代码量和出错概率。
  • 扩展性差:添加新的逻辑或修改现有逻辑变得困难。

使用 Promise 改善回调地狱

Promise 就是为了解决这个问题而生的:

  • 更清晰的流程控制
  • 更好的错误处理
  • 支持链式调用

三、Promise 的基本结构与状态

✅ 创建一个 Promise

const p = new Promise((resolve, reject) => {
  // 这里写异步任务
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve("成功啦!");
    } else {
      reject("失败啦!");
    }
  }, 1000);
});

📦 Promise 的三种状态:

状态说明
pending初始状态,未完成
fulfilled成功,调用了 resolve()
rejected失败,调用了 reject()

一旦状态变为 fulfilledrejected,就不能再改变了。


四、then 和 catch 的作用

.then().catch() 是 Promise 提供的两个原型方法,用于处理结果或错误。

p.then(res => {
  console.log("成功:", res);
}).catch(err => {
  console.error("失败:", err);
});

你可以把它想象成:

  • .then():如果事情办成了,就执行这个;
  • .catch():如果出错了,就在这里处理。

五、Promise 底层机制详解(简单模拟实现)

为了更好地理解 Promise 的底层工作原理,我们将手动实现一个极简版的 Promise。

🧩 Step 1:定义构造函数

function MyPromise(executor) {
  let self = this;

  self.status = "pending";     // 当前状态
  self.value = undefined;      // 成功的值
  self.reason = undefined;     // 失败的原因

  self.onFulfilledCallbacks = []; // 存储成功的回调
  self.onRejectedCallbacks = [];  // 存储失败的回调

  function resolve(value) {
    if (self.status === "pending") {
      self.status = "fulfilled";
      self.value = value;
      self.onFulfilledCallbacks.forEach(fn => fn());
    }
  }

  function reject(reason) {
    if (self.status === "pending") {
      self.status = "rejected";
      self.reason = reason;
      self.onRejectedCallbacks.forEach(fn => fn());
    }
  }

  try {
    executor(resolve, reject); // 立即执行传入的函数
  } catch (e) {
    reject(e);
  }
}

解释:

  • MyPromise 构造函数接收一个执行器 executor,该执行器是一个函数,立即被调用。
  • self 保存当前实例的引用,便于在内部函数中访问。
  • status 代表当前的状态,默认为 "pending"(待定)。
  • valuereason 分别存储成功的结果和失败的原因。
  • onFulfilledCallbacksonRejectedCallbacks 分别存储成功和失败的回调函数列表,以便在状态改变后调用它们。
  • resolve 函数用于将状态改为 "fulfilled" 并触发所有已注册的成功回调。
  • reject 函数用于将状态改为 "rejected" 并触发所有已注册的失败回调。
  • 如果执行器抛出异常,则直接调用 reject 方法。

🧩 Step 2:实现 then 方法

MyPromise.prototype.then = function(onFulfilled, onRejected) {
  let self = this;

  if (self.status === "fulfilled") {
    onFulfilled(self.value);
  }

  if (self.status === "rejected") {
    onRejected(self.reason);
  }

  if (self.status === "pending") {
    self.onFulfilledCallbacks.push(() => {
      onFulfilled(self.value);
    });

    self.onRejectedCallbacks.push(() => {
      onRejected(self.reason);
    });
  }
};

解释:

  • then 方法接收两个参数:onFulfilledonRejected,分别表示成功和失败的回调函数。
  • 如果当前状态是 "fulfilled",则立即执行 onFulfilled 回调,并传递成功的结果。
  • 如果当前状态是 "rejected",则立即执行 onRejected 回调,并传递失败的原因。
  • 如果当前状态是 "pending",则将 onFulfilledonRejected 回调分别加入到相应的回调队列中,等到状态改变后再执行。

🎯 总结一下这段代码做了什么:

  • 创建了一个对象 MyPromise,用来保存状态和值。
  • 提供了 resolvereject 方法改变状态。
  • then 中根据状态决定是否立即执行回调,或者先存起来等状态变化后再执行。

六、Promise 链式调用的原理(重要!)

Promise 最强大的功能之一就是链式调用

new Promise((resolve) => {
  setTimeout(() => resolve(1), 1000);
})
  .then(res => {
    console.log(res); // 1
    return res * 2;
  })
  .then(res => {
    console.log(res); // 2
    return res * 2;
  })
  .then(res => {
    console.log(res); // 4
  });

🔍 原理说明:

  • 每个 .then() 返回一个新的 Promise。
  • 如果你在 .then() 中返回一个普通值,它会被自动包装成一个 resolved 的 Promise。
  • 如果抛出异常,就会触发 .catch()
  • 如果返回的是另一个 Promise,它会等待这个 Promise 完成后再继续。

七、async/await 是 Promise 的语法糖

ES2017 引入了 async/await,让异步代码看起来像同步一样。

async function demo() {
  const result1 = await new Promise(resolve => setTimeout(resolve, 1000, 'foo'));
  console.log(result1); // foo

  const result2 = await new Promise(resolve => setTimeout(resolve, 500, 'bar'));
  console.log(result2); // bar

  return 'done';
}

demo().then(console.log); // done

✅ async 函数的特点:

  • 返回值一定是 Promise:无论你是否显式返回 Promise,async 函数总是返回一个 Promise。
  • 可以使用 await 关键字:只能在 async 函数中使用,用于等待一个 Promise 的结果,暂停函数的执行,直到 Promise 被解决
  • 错误处理可以用 try/catch:更接近同步写法,提高可读性和可维护性。

🔍 async/await 的本质:生成器 + Promise 自动执行器

虽然我们写的代码是 async/await,但它背后其实是基于 Generator(生成器)函数Promise 的组合,再由 JS 引擎自动帮你执行的。

类比伪代码(帮助理解):

function* genDemo() {
  const result1 = yield new Promise(resolve => setTimeout(resolve, 1000, 'foo'));
  const result2 = yield new Promise(resolve => setTimeout(resolve, 500, 'bar'));
  return 'done';
}

// 自动执行器
function run(gen) {
  const iterator = gen();
  function next(data) {
    const result = iterator.next(data);
    if (!result.done) {
      result.value.then(next);
    }
  }
  next();
}

run(genDemo).then(console.log); // done

可以看到,async/await 的本质其实就是这样一个自动生成并运行的 Generator 函数。


💡 await 的工作机制详解

const result = await someAsyncFunction(); // 假设返回一个 Promise

这行代码背后的逻辑是:

  1. someAsyncFunction() 返回一个 Promise。
  2. await 会让整个 async 函数暂停执行,把控制权交还给事件循环
  3. 浏览器继续执行其他任务(如渲染、事件监听等)。
  4. 当这个 Promise 变为 fulfilled 状态后,JS 引擎会恢复执行这个 async 函数,继续往下执行。
  5. result 变量将获得 Promise 的返回值。

⚠️ 注意:await 只能用在 async 函数中,否则会报错。


✅ 错误处理:使用 try/catch

async function demo() {
  try {
    const result = await fetchSomeData(); // 假设可能出错
    console.log('获取数据成功:', result);
  } catch (err) {
    console.error('出错了:', err);
  }
}
  • try/catch 可以捕获 await 表达式中的错误。
  • 不再需要 .catch() 来处理错误,使代码更加简洁直观。

✅ async 函数的返回值

无论你是否显式 returnasync 函数都会返回一个 Promise:

async function test() {
  return 'Hello'; // 实际上等于 Promise.resolve('Hello')
}

test().then(console.log); // Hello

如果你抛出异常:

async function test() {
  throw new Error('出错了');
}

test().catch(console.error); // Error: 出错了

八、完整示例 + 详细解释

我们来写一个完整的流程,模拟登录 → 获取用户信息 → 获取订单数据。

✅ 步骤一:定义异步函数

function login(username, password) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (username === "admin" && password === "123456") {
        resolve({ token: "abc123", userId: 123 });
      } else {
        reject("用户名或密码错误");
      }
    }, 1000);
  });
}

function fetchUserInfo(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id: userId, name: "张三", age: 28 });
    }, 1000);
  });
}

function fetchOrders(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { orderId: 1, product: "iPhone", price: 999 },
        { orderId: 2, product: "MacBook", price: 1999 }
      ]);
    }, 1000);
  });
}

✅ 方式一:使用 Promise 链式调用(解决回调地狱)

相比前面提到的回调地狱,我们可以利用 Promise 来简化代码结构:

login("admin", "123456")
  .then(tokenData => {
    console.log("登录成功:", tokenData);
    return fetchUserInfo(tokenData.userId);
  })
  .then(userInfo => {
    console.log("用户信息:", userInfo);
    return fetchOrders(userInfo.id);
  })
  .then(orders => {
    console.log("订单列表:", orders);
  })
  .catch(err => {
    console.error("错误:", err);
  });

优势对比:

  • 可读性增强:不再有层层嵌套,代码更加直观。
  • 错误集中处理:只需一个 .catch() 即可捕获所有错误,无需每层都处理。
  • 易于扩展:可以轻松添加更多的异步操作,而不会使代码变得更加混乱。

✅ 方式二:使用 async/await(推荐)

async function run() {
  try {
    const tokenData = await login("admin", "123456");
    console.log("登录成功:", tokenData);

    const userInfo = await fetchUserInfo(tokenData.userId);
    console.log("用户信息:", userInfo);

    const orders = await fetchOrders(userInfo.id);
    console.log("订单列表:", orders);
  } catch (err) {
    console.error("错误:", err);
  }
}

run();

九、总结:Promise 的核心思想

特性说明
状态管理pending → fulfilled/rejected,不可逆
回调缓存在 pending 时缓存回调,等状态变化后执行
链式调用每个 then 返回新 Promise,形成链条
错误传播一个 reject 会沿着链找 catch 处理
微任务机制Promise.then 是微任务,优先于宏任务执行
async/awaitPromise 的语法糖,让异步更像同步

📌 记住一句话:Promise 不只是语法,更是思维方式。

通过本文的学习,希望能够帮助你摆脱回调地狱带来的困扰,拥抱更加清晰和高效的异步编程方式。