一、为什么异步编程在JS中如此重要

在 JavaScript 的世界里,异步编程几乎是呼吸般的存在。 这门语言最初诞生于浏览器,为的是让网页在用户操作的同时还能去加载数据、响应事件、渲染动画——而这一切都运行在单线程之上。

单线程意味着同一时间只能做一件事,如果某个任务耗时过长(比如网络请求、文件读取、复杂计算),整个页面就会“卡死”,按钮点不动、动画停滞、用户体验瞬间崩塌。

异步编程正是为了解决这个问题而生:

  • 让 JavaScript 可以先挂起耗时任务,继续处理其他工作,等结果准备好再回来执行。

  • 让我们能同时监听用户输入、加载数据、播放动画,而不会互相阻塞。

  • 是现代 Web 应用、Node.js 服务、跨平台应用流畅运行的基石。

JS异步的演变:回调函数->Promise->async/await

*JS异步运行机制--事件循环:

JavaScript 的异步执行依赖于事件循环(Event Loop),它协调:

  • 同步任务:立即执行

  • 微任务:如Promise.then,在当前任务结束后立即执行

  • 宏任务:如setTimeout,在下一轮事件循环中执行

这种机制确保异步任务不会阻塞主线程,同时也保证执行顺序的可预测性。

在一次事件循环中,先执行所有同步代码 → 清空微任务队列 → 最后处理宏任务

实例1:

console.log("同步开始");

setTimeout(() => {
  console.log("setTimeout 回调");
}, 0);

Promise.resolve()
  .then(() => {
    console.log("Promise 微任务 1");
  })
  .then(() => {
    console.log("Promise 微任务 2");
  });

console.log("同步结束");

输出顺序:

同步开始
同步结束
Promise 微任务 1
Promise 微任务 2
setTimeout 回调

解释:

  1. 同步代码优先执行:console.log("同步开始") 和 console.log("同步结束") 是同步任务,立即执行。
  2. 微任务队列:
    Promise.resolve().then(...) 会将 .then() 的回调加入微任务队列。
    所有同步任务执行完后,事件循环会清空微任务队列。
  3. 宏任务队列:
    setTimeout(..., 0) 是宏任务,它会在下一轮事件循环中执行。所以它最后才输出。

实例2:

async function async1() {
  console.log('async1 start');
  await new Promise(resolve => {
    console.log('promise1');
  });
  console.log('async1 success');
  return 'async1 end';
}

console.log('script start');

async1().then(res => console.log(res));

console.log('script end');

输出顺序:

script start
async1 start
promise1
script end

解释:

  1. console.log('script start') → 同步任务,立即执行
  2. 调用 async1():
    输出 'async1 start'
    执行 new Promise(...),输出 'promise1'
    遇到 await,暂停后续执行,等待 Promise 解决(但它永远不会)
    await new Promise(resolve => {
      console.log('promise1');
    });
    //这段代码创建了一个 Promise,但没有调用 resolve(),所以这个 Promise 永远处于 pending 状态。
    
    //由于 await 会暂停后续代码执行直到 Promise 被解决(fulfilled 或 rejected),而这个 Promise 永远不会解决,所以:
    
    //console.log('async1 success') 永远不会执行
    
    //return 'async1 end' 永远不会发生
    
    //.then(res => console.log(res)) 也永远不会触发

  3. console.log('script end') → 同步任务,立即执行

二、Callback回调函数

回调函数定义:被作为参数传递的函数就成为回调函数。

回调函数名字虽然抽象,但我们可以将其理解为“回头再调用的函数”,代表着我交给你一个函数,在任务完成后可以回头调用。实现了简单的异步编程。

优点:

  • 回调函数简单直接,易于实现
  • 让指定函数在恰当时机,以恰当触发条件被触发
  • 让函数更灵活,可以按照实际需要调整函数
  • 提高程序效率,如我需要在指定时间触发某函数,在不使用回调函数的情况,我需要在函数内不断查询当前时间,不仅效率低下还会使线程堵塞。而使用回调函数就可以查询当前时间后,计算还剩多少时间,使用setTimeOut()函数触发,在触发指定函数前将线程让给其他程序。

简单来说,触发程序就像餐馆上餐,这份餐就是程序运行的结果。而以往我们需要不停询问厨房是否做好,使用回调函数就像厨房给了我们一份呼号机,出餐后通知我们并上餐。

缺点:

  • 如果我们需要使用多个回调函数,需要在每一层2函数中层层嵌套,使得代码难以阅读和维护,运行结果处理分散,形成回调地狱。
// 回调地狱:层层嵌套的回调(Callback Hell)
function callbackHell(userId, done) {
  readConfig((err, config) => {
    if (err) return done(err);

    connectDb(config.db, (err, conn) => {
      if (err) return done(err);

      findUser(conn, userId, (err, user) => {
        if (err) return done(err);

        fetchProfile(user.token, (err, profile) => {
          if (err) return done(err);

          transformData(profile, (err, report) => {
            if (err) return done(err);

            saveReport(config.output, report, (err) => {
              if (err) return done(err);

              done(null, "完成:报告已生成");
            });
          });
        });
      });
    });
  });
}

// ——— 模拟的异步函数们 ———
function readConfig(cb) {
  setTimeout(() => {
    console.log("[readConfig]");
    // 模拟成功
    cb(null, { db: "db://example", output: "report.txt" });
  }, 100);
}

function connectDb(uri, cb) {
  setTimeout(() => {
    console.log("[connectDb]", uri);
    cb(null, { uri, connected: true });
  }, 120);
}

function findUser(conn, userId, cb) {
  setTimeout(() => {
    console.log("[findUser]", userId);
    // 模拟可能失败
    if (userId == null) return cb(new Error("缺少 userId"));
    cb(null, { id: userId, token: "token-abc" });
  }, 80);
}

function fetchProfile(token, cb) {
  setTimeout(() => {
    console.log("[fetchProfile]", token);
    cb(null, { name: "Alice", age: 28 });
  }, 150);
}

function transformData(profile, cb) {
  setTimeout(() => {
    console.log("[transformData]");
    try {
      const report = `User: ${profile.name}, Age: ${profile.age}`;
      cb(null, report);
    } catch (e) {
      cb(e);
    }
  }, 60);
}

function saveReport(path, content, cb) {
  setTimeout(() => {
    console.log("[saveReport]", path, "->", content);
    cb(null);
  }, 70);
}

// 运行示例
callbackHell(42, (err, msg) => {
  if (err) {
    console.error("失败:", err.message);
  } else {
    console.log("成功:", msg);
  }
});

可以看到,如果运行出错,我们难以在层层嵌套中debug。

为了解决回调地狱,我们在ES6中引入了Promise。

三、Promise

Promise人如其名,代表承诺,即:我承诺无论我的内部程序是否正常运行,会在未来某个时候给你一个结果。

设计动机:解决回调地狱,统一错误处理。

核心概念:状态(pending-处理中/fullfilled-已解决/rejected-已拒绝)

基本用法:new Promise(创建promise对象)、resolve(传递并包装成功结果)、rejected(传递错误结果)

//在函数内部:
resolve('这里内容会被作为成功结果传出')

reject('这里结果会被作为错误结果传出')

链式调用:

  • .then(res=>{}):
    接受上一级的成功结果,res为接受的参数,同时若有需要再向下调起下一级函数。
  • .catch(err=>{}):
    接受所有位置的错误结果,err为接收到的错误,同时自定义发生错误后程序怎样执行。
  • .finally(()=>{}):
    不接收任何参数,无论程序运行成功与否都会运行,适合做收尾工作而不是处理结果:如关闭连接,隐藏元素等。

使用Promise重写回调地狱(promiseFlow即为callBackHell的重置):

function promiseFlow(userId) {
  return readConfig()
    .then(config => {
      return connectDb(config.db)
        .then(conn => ({ config, conn }));
    })
    .then(({ config, conn }) => {
      return findUser(conn, userId)
        .then(user => ({ config, user }));
    })
    .then(({ config, user }) => {
      return fetchProfile(user.token)
        .then(profile => ({ config, profile }));
    })
    .then(({ config, profile }) => {
      return transformData(profile)
        .then(report => ({ config, report }));
    })
    .then(({ config, report }) => {
      return saveReport(config.output, report);
    });
}

function readConfig() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("[readConfig]");
      resolve({ db: "db://example", output: "report.txt" });
    }, 100);
  });
}

function connectDb(uri) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("[connectDb]", uri);
      resolve({ uri, connected: true });
    }, 120);
  });
}

function findUser(conn, userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("[findUser]", userId);
      if (userId == null) return reject(new Error("缺少 userId"));
      resolve({ id: userId, token: "token-abc" });
    }, 80);
  });
}

function fetchProfile(token) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("[fetchProfile]", token);
      resolve({ name: "Alice", age: 28 });
    }, 150);
  });
}

function transformData(profile) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("[transformData]");
      try {
        const report = `User: ${profile.name}, Age: ${profile.age}`;
        resolve(report);
      } catch (e) {
        reject(e);
      }
    }, 60);
  });
}

function saveReport(path, content) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("[saveReport]", path, "->", content);
      resolve();
    }, 70);
  });
}

// 运行
promiseFlow(42)
  .then(() => {
    console.log("成功:报告已生成");
  })
  .catch(err => {
    console.error("失败:", err.message);
  });

四、async/await语法糖

本质是Promise函数的语法糖,使Promise函数更具有可读性,结构更接近同步代码。

用法:async返回Promise函数,await等待Promise结果(await只能写在async内部)。

错误处理:try/catch,try中如果出现了错误,catch就会捕捉。


// async/await 版本的流程
async function asyncFlow(userId) {
  try {
    const config = await readConfig();
    const conn = await connectDb(config.db);
    const user = await findUser(conn, userId);
    const profile = await fetchProfile(user.token);
    const report = await transformData(profile);
    await saveReport(config.output, report);

    console.log("成功:报告已生成");
  } catch (err) {
    console.error("失败:", err.message);
  }
}

//函数定义与Promise写法相同

// 运行
asyncFlow(42);

其中,await意思是:需要等待来得到结果,得到结果后就会告诉函数体。

改进:

  • 结构线性,看起来像同步代码,顺序一目了然。
  • 错误集中处理,包裹整个流程。
  • 变量作用域更加清晰。

五、对比

演化路线

  • 回调 → 最原始的异步方式,但容易混乱。

  • Promise → 解决回调地狱,提供链式调用和状态管理。

  • async/await → 在 Promise 基础上进一步简化,让异步代码像同步一样易读。

特性 回调函数 Callback Promise async/await
可读性 差(嵌套多) 中等
错误处理 分散在回调中 .catch 统一 try...catch 统一
状态管理 有状态(pending/fulfilled/rejected) 基于 Promise 状态
语法复杂度 中等 低(最直观)
适用场景 简单异步任务 中等复杂度任务 复杂异步流程
Logo

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

更多推荐