​​中级前端进阶方向 第七步)深入 javascript 高级特性Async/Await,你了解多少,附案例源码

    🤙《中级前端进阶方向指南》

《中级前端进阶方向 第一步)JavaScript 微任务 / 宏任务机制》

《中级前端进阶方向 第二步)深入事件循环(Event Loop)》

《中级前端进阶方向 第三步)深入javascript原型链,附学习代码》

《中级前端进阶方向 第四步)深入javascript 闭包、this 、作用域提升,附学习案例源码》

《​​中级前端进阶方向 第五步)深入 javascript高级特性Proxy,附学习案例源码》

《​​中级前端进阶方向 第六步)深入 javascript 高级特性Generator生成器,附案例源码》


--🖍《延伸扩展》-----------------------------------------------------------------------------------------
《中级前端进阶方向 延伸扩展一)javascript 私有状态/工厂函数/回调中保持局部状态/防抖与节流/IIFE 捕获》

1) 概念速览

  • async / await 是基于 Promise 的语法糖,用来把基于回调/then 的异步代码写得像同步代码一样,增强可读性和可维护性。

  • async 标记一个函数为异步函数,该函数总是返回一个 Promise(即使返回值是普通值也会被 Promise.resolve 包裹)。

  • await 用在 async 函数内部,等待一个 Promise 解析(或等待 thenable),并把解析值返回。await 会“暂停”当前 async 函数的执行,但不会阻塞整个 JS 运行时 / 事件循环。


2) 基本语法:

async function foo() {
  return 42;            // 等同于 return Promise.resolve(42)
}

foo().then(v => console.log(v)); // 42

async function bar() {
  const a = await Promise.resolve(1);
  const b = await 2;  // await 一个非 Promise 值 -> 直接得到该值
  return a + b;       // 返回值同样被包成 Promise
}
bar().then(console.log); // 3

关键特性:一个 async 函数 永远返回一个 Promise

  • 如果函数内部返回一个非 Promise 的值(如字符串、数字、对象),async 函数会自动用 Promise.resolve() 将其包装成一个已解决的 Promise(fulfilled Promise)

  • 如果函数内部显式返回一个 Promise,那么就以这个 Promise 为准。

  • 如果函数内部抛出异常,它会返回一个被拒绝的 Promise(rejected Promise)


3) await 的微任务(task)细节

await 本质上是把后面的继续执行放到微任务队列(microtask)中。因此:

console.log('start');

(async () => {
  console.log('inside before await');
  await null;                   // 将后续部分排为微任务
  console.log('inside after await');
})();

console.log('end');

输出顺序:
start  ->  inside before await  ->  end  ->  inside after await

说明:调用 async 函数时,函数体会同步执行直到遇到第一个 await(或执行完)。await 后的代码被排入微任务队列,当前宏任务结束后执行微任务(任务队列可以看第二步)

await 的工作流程

  1. await 后面的表达式通常是一个 Promise,但也可以是一个任何值

  2. 如果表达式是非 Promise 的值await 会直接返回这个值

  3. 如果表达式是 Promiseawait 会“阻塞”后面的代码,等待 Promise 完成

    • 如果 Promise 成功解决(fulfilled)await 会返回 Promise 的解决值(resolution value)。

    • 如果 Promise 被拒绝(rejected)await 会抛出拒绝的原因(rejection reason),就像一个 throw 语句一样。

注意:await 关键字只能在 async 函数内部使用。它的作用是暂停异步函数的执行,等待一个 Promise 对象的解决(settle),然后恢复函数的执行并返回结果。


4) 串行与并行(非常常见的性能问题)

await 在循环中会导致串行执行(若你逐项 await):串行(慢):

for (const url of urls) {
  const resp = await fetch(url);
  const data = await resp.json();
  results.push(data);
}

并行(快)——推荐:先启动所有 Promise,再 await

const promises = urls.map(url => fetch(url).then(r => r.json()));
const results = await Promise.all(promises);

注意:

  • Promise.all 一旦有一个失败,会导致整体 reject(可用 allSettled 处理每项结果)。

  • 如果要“并发但限制并发数”,需要并发控制(见下)。


5) 常见坑与反模式

  • array.forEach(async item => { await doAsync(item); }) —— 这不会等待内部 async 完成;forEach 不能与 await 配合控制流。改用 for..ofPromise.all

  • 忘记 awaitconst result = fetch(url).then(...); vs const result = await fetch(url);

  • 在顶层用 await:只有在 ESM 模块或支持顶层 await 的环境中才可用(现代浏览器、Node 的 ESM 模式支持)。

  • await 并不创建新线程,不能用于替代 CPU 密集型任务的分片(CPU 密集应交由 worker / 子进程)。

示例错误用法:

[1,2,3].forEach(async n => {
  await doSomething(n); // forEach 不等待这些 async,外部无法 await 完成
});

正确做法:

for (const n of [1,2,3]) {
  await doSomething(n); // 串行
}

// 或并发
await Promise.all([1,2,3].map(n => doSomething(n)));

6) 错误处理(try / catch / finally)

推荐在 async 中用 try/catch,便于捕获 await 抛出的错误:

async function getData() {
  try {
    const resp = await fetch('/api/data');
    if (!resp.ok) throw new Error('bad status');
    return await resp.json();
  } catch (err) {
    console.error('请求失败:', err);
    throw err; // 可选:重新抛出使调用方知道
  } finally {
    // 清理工作(可选)
  }
}

对于并发多个任务而要逐项处理错误,Promise.allSettled 很有用:

const results = await Promise.allSettled(promises);
results.forEach(r => {
  if (r.status === 'fulfilled') { /* r.value */ }
  else { /* r.reason */ }
});

7) 取消与超时(AbortController 与超时模式)

浏览器 fetch 与现代 Node.js 支持 AbortController,可用于取消请求:

async function fetchWithTimeout(url, ms) {
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), ms);

  try {
    const resp = await fetch(url, { signal: controller.signal });
    return await resp.json();
  } finally {
    clearTimeout(id);
  }
}

另一种经典超时模式(不优雅,旧法):

const response = await Promise.race([
  fetch(url),
  new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000))
]);

推荐使用 AbortController,因为可以在超时时真正取消底层请求


8) 并发限制(实现一个简单的 p-limit)

当你需要对大量任务并发做限流时,可以用简单的队列实现:

function pLimit(concurrency) {
  const queue = [];
  let active = 0;

  const next = () => {
    if (active >= concurrency || queue.length === 0) return;
    active++;
    const { fn, resolve, reject } = queue.shift();
    (async () => {
      try {
        const val = await fn();
        resolve(val);
      } catch (err) {
        reject(err);
      } finally {
        active--;
        next();
      }
    })();
  };

  return (fn) => new Promise((resolve, reject) => {
    queue.push({ fn, resolve, reject });
    next();
  });
}

// 用法:
const limit = pLimit(3); // 最多 3 个并发
const tasks = urls.map(url => limit(() => fetch(url).then(r => r.json())));
const results = await Promise.all(tasks);

这个模式很实用


9) 设计模式与实用技巧

  • 重试(带指数退避)

async function retry(fn, attempts = 3) {
  let i = 0;
  while (i < attempts) {
    try {
      return await fn();
    } catch (err) {
      i++;
      if (i >= attempts) throw err;
      await new Promise(r => setTimeout(r, 2 ** i * 100)); // 指数退避
    }
  }
}
  • 后台(fire-and-forget)并安全记录错误

(async () => {
  try {
    await doSomething();
  } catch (err) {
    console.error('后台任务失败', err);
  }
})(); // 立即执行但错误被捕获
  • 在顶层用 await(仅在模块/支持环境)

// 在 ESM 模块中
const data = await fetch('/api').then(r => r.json());
  • 将同步/异步分开:不要在 sync-heavy 函数里混用大量 await,考虑把耗时 I/O 单独封装。


10) async 函数的同步部分与任务调度(关键理解点)

调用 async 函数会 立即 执行函数体直到第一个 await(或结束),这些同步代码不会被延迟。示例:

async function f() {
  console.log('start f');
  await 1;
  console.log('after await');
}
console.log('before call');
f();
console.log('after call');
// 输出: before call -> start f -> after call -> after await

这是理解控制流顺序与调试非常重要的点。


11)  错误处理:使用 try...catch 

 await 在遇到被拒绝的 Promise 时会抛出异常,我们可以使用经典的 try...catch 块来捕获错误,这使得错误处理变得同步化,非常直观。

没有 Async/Await (Promise 风格):

function fetchUser() {
  return fetch('/api/user')
    .then(response => {
      if (!response.ok) {
        throw new Error('Network response was not ok');
      }
      return response.json();
    })
    .then(user => console.log(user))
    .catch(error => console.error('Fetch failed:', error)); // 统一捕获错误
}

使用 Async/Await:

async function fetchUser() {
  try {
    const response = await fetch('/api/user');
    if (!response.ok) {
      // throw 的错误会被后面的 catch 捕获
      throw new Error('Network response was not ok');
    }
    const user = await response.json(); // 等待第二个异步操作
    console.log(user);
  } catch (error) {
    // 捕获所有 try 块内 await 抛出的错误,以及显式 throw 的错误
    console.error('Fetch failed:', error);
  }
}

你也可以在调用 async 函数后使用 .catch(),因为它在任何情况下都返回 Promise。

fetchUser().catch(error => console.error('Fetch failed externally:', error));

12)  与 Promise 链的对比

Promise 链式调用:

function getuserAndPosts(userId) {
  return fetch(`/api/users/${userId}`)
    .then(userResponse => userResponse.json())
    .then(user => {
      console.log('User:', user);
      return fetch(`/api/posts?userId=${user.id}`);
    })
    .then(postsResponse => postsResponse.json())
    .then(posts => {
      console.log('Posts:', posts);
      return posts; // 最终返回值
    })
    .catch(error => {
      console.error('Error:', error);
    });
}

Async/Await:

async function getuserAndPosts(userId) {
  try {
    const userResponse = await fetch(`/api/users/${userId}`);
    const user = await userResponse.json();
    console.log('User:', user);

    const postsResponse = await fetch(`/api/posts?userId=${user.id}`);
    const posts = await postsResponse.json();
    console.log('Posts:', posts);

    return posts; // 最终返回值
  } catch (error) {
    console.error('Error:', error);
  }
}

优势一目了然

  • 代码结构:Async/Await 的代码是从上到下线性执行的,非常符合人类的阅读习惯。它消除了 .then() 的嵌套,代码更扁平。

  • 变量作用域:在 try 块中,所有中间变量(如 userResponseuser)都在同一个作用域内,无需像 Promise 链那样通过传递参数或在外部定义变量。

  • 条件逻辑和循环:处理 if/else 和循环(如 forwhile)中的异步操作变得异常简单,而这在纯 Promise 链中是非常棘手的。


13)   进阶用法和注意事项

并行执行:使用 Promise.all

上面的例子是串行执行,第二个请求必须等第一个请求完成才能开始。如果多个异步操作之间没有依赖关系,并行执行可以大幅节省时间。

async function fetchIndependentData() {
  // 错误:这样写是串行的,会等待第一个 await 完成再执行第二个
  const a = await fetch('/api/a');
  const b = await fetch('/api/b');

  // 正确:使用 Promise.all 实现并行
  const [resultA, resultB] = await Promise.all([
    fetch('/api/a').then(r => r.json()),
    fetch('/api/b').then(r => r.json())
  ]);

  console.log(resultA, resultB);
  return { resultA, resultB };
}

Promise.all 接受一个 Promise 数组,并返回一个新的 Promise。这个新的 Promise 会在所有输入的 Promise 都成功解决后才解决,其解决值是一个结果数组,顺序与输入数组一致。如果其中任何一个被拒绝,整个 Promise.all 会立即被拒绝。

await 在循环中的使用

串行循环(一个接一个执行):

async function processArray(array) {
  for (const item of array) {
    await processItem(item); // 等待上一个处理完再处理下一个
  }
}

并行循环(同时执行):

async function processArray(array) {
  // 使用 map 创建所有 Promise,然后用 Promise.all 并行等待
  const promises = array.map(item => processItem(item));
  const results = await Promise.all(promises);
  console.log(results);
}
顶层 Await (Top-Level Await)

在 ES2022 之前,await 不能直接在模块的顶层作用域使用,必须包裹在 async 函数中。现在,在 ES 模块(<script type="module"> 中,可以直接使用顶层 await

// 在 ES Module 中
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
// 模块会等待顶层 await 解决后才执行导入此模块的代码

14) 常用组合一览(备忘小抄)

  • 串行处理:

for (const item of items) {
  await do(item);
}
  • 并行处理(全部成功才继续):

await Promise.all(items.map(item => do(item)));
  • 并行但收集个别错误:

const results = await Promise.allSettled(promises);
  • 竞速(先完成者):

const r = await Promise.race([p1, p2]);
  • 超时:

const r = await Promise.race([fetch(url), timeoutPromise(3000)]);

或使用 AbortController 更优雅地取消请求。

  • 限制并发:使用上文 pLimit 或成熟库 p-limit


15) 总结:

与旧式回调 / generator 的关系

  • async/await 是基于 Promise 的更友好语法,generator + co 也曾被用来写同步风格异步(co 库),现在基本被 async/await 取代。

  • await 更直观、更易读,且已被广泛原生支持。


TypeScript 相关注意

  • 在 TypeScript 中,async function foo(): Promise<T> {} 给出明确返回类型。

  • await 的结果会被类型系统推断:const x = await fetchJson()x 的类型是 Promise 解包后的类型。

  • 须注意 unknown / any 的捕获处理与类型收窄在 try/catch 中的差异(catch 的错误默认为 unknown,建议先类型判断)。


调试与性能提示

  • async/await 会产生额外的 promise/microtask,但通常代价较小。避免在高频短函数里无谓地包一层 async(例如非常短的循环内每次都创建很多微任务会有一点 overhead)。

  • 若要观察未捕获的 Promise rejection,Node 环境中使用 process.on('unhandledRejection', ...)(或在浏览器里监听 unhandledrejection 事件)。

  • 若需要让界面“让出”控制权以便渲染,await Promise.resolve() 只会将后续放到微任务,可能不足以触发渲染;可使用 await new Promise(r => setTimeout(r, 0)) 或平台特定 API。

特性说明
async声明一个异步函数,该函数总是返回一个 Promise。
await暂停 async 函数的执行,等待一个 Promise 的解决,并返回其结果。只能在 async 函数内使用。
返回值async 函数return的值会成为其返回的 Promise 的解决值。
错误处理使用 try...catch 来捕获 await 表达式可能抛出的错误,与同步代码错误处理方式一致。
核心优势代码更清晰、更易读,像写同步代码一样写异步代码。轻松处理条件分支和循环中的异步操作。
底层原理它是基于 Promise 的语法糖,并没有取代 Promise,而是与之协同工作。
并行优化对于无依赖的异步操作,应结合 Promise.all 使用以实现并行,提升性能。
  1. 尽量用 for..ofPromise.all,避免 forEach + await 的误用。

  2. 并发量大时加并发限制,防止资源耗尽(文件描述符、socket、内存)。

  3. 关键 I/O (请求/DB)应支持取消(AbortController)与超时。

  4. 使用 try/catch 明确处理错误;对于并行任务用 allSettled 以便逐项处理失败。

  5. 避免把大量 CPU 密集型逻辑放在 async 中期望 await 帮忙“转移”——它只对 I/O 有帮助。

  6. 在库/公共 API 中显式返回 Promise 类型(TypeScript)以便调用者明确知道可 await

总而言之,Async/Await 是现代 JavaScript 中处理异步操作的首选方式。它极大地改善了代码的可读性和可维护性,是每个 JS 开发者必须掌握的核心技能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星空下的DeppBing

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值