『译』前端大佬的 5 个 Async/Await 小技巧

1,818 阅读9分钟

小声BB

本文在翻译过程中确保意思传达准确的前提下,会加入很多本人的个人解释和一些前置知识补充(使用引用块标注)

🎊如果觉得文章内容有用,点个赞再走,交个朋友~🎊

如果你(我自己)在反面教材里面看到了自己代码的影子,那么这篇文章值得你花点时间好好看看。只要学到一个小点就是赚到,比如单例设计模式下的状态做初始化的时候,设置成 promise,比设置成其他类型要好得多(踩过坑),可以很好地解决竞态问题(数据先取再存)。

正文

原文5 Async/Await Secrets That Professional JavaScript Developers Use (But Never Document

作者Blueprintblog

区分新手教程和专业代码的进阶玩法

你已经掌握了 async/await 的基础用法。你能写出简单的异步函数,也能处理基本的 Promise 链。但从教程里的示例代码,到真正能在生产环境稳定运行的代码,中间有着巨大的鸿沟。

大多数文档只会教你语法糖的写法。真正的前端开发者在日常工作中,需要掌握那些能处理 边界情况、避免 竞态条件(race conditions) 、并且能优雅处理 请求失败 的高级技巧。

这些技巧不是纸上谈兵,而是在大厂前端项目中反复打磨过的最佳实践。

问题是这些高级技巧很少有文档记录。它们通常通过开发日常活动,代码 review,团队写作,生产事故复盘(手动狗头🐶)等场景,在程序员们中代代相传。

思维模型才是重中之重

大多数教程教 async/await 的方式是颠倒的:它们先讲语法,再给示例。而资深开发者的思考方式则完全不同。

❌ 我以前认为:

// "async/await 是 Promises" 的语法糖
async function getData() {
  const result = await fetch('/api/data');
  return result.json();
}

✅ 大佬的思路:

// “async/await 是用于管理执行上下文的状态机”
async function getData() {
  try {
    // 执行在此暂停,控制权返回给调用方
    const response = await fetch('/api/data');
​
    // 当 Promise 解决后,执行在此恢复
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
​
    // 另一个暂停点
    const data = await response.json();
    return data;
  } catch (error) {
    // 捕获网络错误和 JSON 解析错误
    throw new Error(`数据获取失败: ${error.message}`);
  }
}

区别在于:专业开发者理解每个 await 都是一个暂停点,在此期间 JavaScript 可以执行其他任务。他们会围绕这些暂停点设计函数,而不仅仅考虑理想流程(happy path)。

1.Error Boundary(优雅的失败恢复)

秘密: 大佬在异步函数中使用策略性的错误边界,构建具有弹性的应用,使得部分 Error 不会导致整个程序崩溃,影响用户体验。

初学者做法:不做Error Boundary

async function processUserData(userId) {
  const user = await fetchUser(userId);
  const preferences = await fetchPreferences(userId);
  const recommendations = await generateRecommendations(user, preferences);
​
  return { user, preferences, recommendations };
  // 如果任何一步失败,整个函数都会失败
}

✅ 大佬做法:优雅降级

async function processUserData(userId) {
  const results = await Promise.allSettled([
    fetchUser(userId),
    fetchPreferences(userId).catch(() => getDefaultPreferences()),
  ]);
​
  const [userResult, preferencesResult] = results;
​
  if (userResult.status === 'rejected') {
    /* ================================================
     * 🎯 小技巧:仅关键失败会阻止执行
     * 用户数据是必需的,因此立即抛出错误
     * ================================================ */
    throw new Error('用户数据必需,但不可用');
  }
​
  const user = userResult.value;
  const preferences = preferencesResult.value;
​
  /* ================================================
   * 🎯 小技巧:可选功能优雅失败
   * 推荐功能提升用户体验,但非关键
   * ================================================ */
  const recommendations = await generateRecommendations(user, preferences)
    .catch(error => {
      console.warn('推荐数据不可用:', error.message);
      return []; // 返回空数组而非崩溃
    });
​
  return { user, preferences, recommendations };
}
​

区别对待不同的错误,关键的错误需要直接抛异常,一些则需要提示即可。

妙在哪里: Promise.allSettled 允许部分操作失败而其他操作成功。关键数据(请求失败)会抛出错误,可选数据则优雅降级。这样即便外部服务出问题,应用仍可运行。

还有高手:

class DataService {
  async processUserWithFallbacks(userId) {
    const strategies = [
      /* ================================================
       * 🎯 高级技巧:多重回退策略
       * 主 API → 缓存 → 默认值
       * ================================================ */
      () => this.fetchFromPrimaryAPI(userId),
      () => this.fetchFromCache(userId),
      () => this.generateDefaultUser(userId)
    ];
​
    for (const strategy of strategies) {
      try {
        const result = await strategy();
        if (this.isValidUserData(result)) {
          return result;
        }
      } catch (error) {
        console.warn('策略失败,尝试下一个:', error.message);
      }
    }
​
    throw new Error('所有用户数据策略已耗尽');
  }
}

真实应用场景:

  • 数据面板加载时显示部分数据(而不是某个请求失败,整个面板崩了)
  • 电商结算可有可无的功能崩了就崩了,别影响主流程
  • 社交媒体动态内容优雅加载(小破站悬浮的广告崩了,别影响视频播放)
  • 银行应用中带有交易回退机制(有 planB)

小技巧: 使用有意义的错误信息帮助开发者调试,例如:“用户数据不可用”这样的提示语比“请求失败”更好。

2.异步队列模式(受控并发)

小技巧: 大佬不会让异步操作失控。他们使用并发控制来防止服务器过载的同时,保持最佳性能。

❌ 业余做法:并行混乱

async function processFiles(files) {
  // This might overwhelm the server with 100 concurrent requests
  const results = await Promise.all(
    files.map(file => uploadFile(file))
  );
  return results;
}

✅ 大佬玩法:受控批处理

async function processFiles(files, concurrency = 3) {
  const results = [];
  const queue = [...files];
​
  while (queue.length > 0) {
    /* ================================================
     * 🎯 技巧:按受控批次处理
     * 防止服务器过载和速率限制
     * ================================================ */
    const batch = queue.splice(0, concurrency);
​
    const batchResults = await Promise.all(
      batch.map(async (file, index) => {
        try {
          /* ================================================
           * 🎯 技巧:每个文件单独处理错误
           * 单个上传失败不会中断整个批次
           * ================================================ */
          const result = await uploadFile(file);
          return { success: true, file: file.name, data: result };
        } catch (error) {
          return { 
            success: false, 
            file: file.name, 
            error: error.message 
          };
        }
      })
    );
​
    results.push(...batchResults);
​
    /* ================================================
     * 🎯 技巧:进度报告优化用户体验
     * 用户看到增量进度,而不仅是加载
     * ================================================ */
    const completed = results.length;
    const total = files.length;
    console.log(`Progress: ${completed}/${total} files processed`);
  }
​
  return results;
}

妙在哪里: 受控并发**考虑到服务器性能的同时提供并行处理能力。

还有高手:

class AsyncQueue {
  constructor(concurrency = 3) {
    this.concurrency = concurrency;
    this.running = 0;
    this.queue = [];
  }
​
  async add(asyncFunction) {
    return new Promise((resolve, reject) => {
      /* ================================================
       * 🎯 高级技巧:动态队列管理
       * 自动管理执行而不阻塞
       * ================================================ */
      this.queue.push({
        fn: asyncFunction,
        resolve,
        reject
      });
​
      this.process();
    });
  }
​
  async process() {
    if (this.running >= this.concurrency || this.queue.length === 0) {
      return;
    }
​
    this.running++;
    const { fn, resolve, reject } = this.queue.shift();
​
    try {
      const result = await fn();
      resolve(result);
    } catch (error) {
      reject(error);
    } finally {
      this.running--;
      this.process(); // 处理下一个任务
    }
  }
}
​
// 使用示例
const queue = new AsyncQueue(3);
const uploadPromises = files.map(file => 
  queue.add(() => uploadFile(file))
);
const results = await Promise.all(uploadPromises);

在此基础上,给每个任务增加优先级的概念,结合排序算法,比如最小堆排序。就能有一个按照优先级处理的异步队列。

真实应用场景:

  • 图像处理与上传
  • API 数据同步
  • 批量邮件发送
  • 大数据集处理

高级小技巧: 根据系统性能动态调整并发数:从 3 开始,如果响应快可以增加,如果速度慢则降低。

超时逃生策略(永不无限挂起)

小技巧: 专业开发者从不完全信任外部 API。他们总是实现超时策略,以防应用程序无限挂起。

❌ 业余做法:无限等待

async function fetchCriticalData() {
  // 如果这个 API 崩了,你的应用会无限挂起
  const data = await fetch('/api/critical-data');
  return data.json();
}

✅ 大佬:带回退的超时策略

function withTimeout(promise, ms, fallback = null) {
  return new Promise((resolve, reject) => {
    /* ================================================
     * 🎯 小技巧:Promise race
     * 谁先完成就算赢,防止挂起
     * ================================================ */
    const timeout = setTimeout(() => {
      if (fallback !== null) {
        resolve(fallback);
      } else {
        reject(new Error(`操作超时,超过 ${ms} 毫秒`));
      }
    }, ms);
​
    promise
      .then(resolve)
      .catch(reject)
      .finally(() => clearTimeout(timeout));
  });
}
​
async function fetchCriticalData() {
  try {
    /* ================================================
     * 🎯 小技巧:多重超时策略
     * UI 使用快速超时,保证回退机制
     * ================================================ */
    const response = await withTimeout(
      fetch('/api/critical-data'),
      5000 // 5 秒超时
    );
​
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
​
    /* ================================================
     * 🎯 小技巧:JSON 解析也要设置超时
     * 网络慢或解析可能挂起
     * ================================================ */
    const data = await withTimeout(
      response.json(),
      2000 // JSON 解析 2 秒超时
    );
​
    return data;
  } catch (error) {
    if (error.message.includes('timed out')) {
      // 回退到缓存数据或默认值
      return getCachedData() || getDefaultData();
    }
    throw error;
  }
}

妙在哪里: JavaScript 的 Promise.race() 行为允许超时与请求同时竞争。先完成的那个“赢”,防止无限挂起,同时提供回退方案。

还有高手:

class ResilientFetcher {
  constructor(options = {}) {
    this.defaultTimeout = options.timeout || 5000;
    this.retryAttempts = options.retries || 3;
    this.retryDelay = options.retryDelay || 1000;
  }
​
  async fetchWithRetry(url, options = {}) {
    const timeout = options.timeout || this.defaultTimeout;
​
    for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {
      try {
        /* ================================================
         * 🎯 高级技巧:阶梯性地判断超时
         * 每次重试等待时间增加,尊重服务器恢复
         * ================================================ */
        const response = await withTimeout(
          fetch(url, options),
          timeout
        );
​
        if (response.ok) {
          return response;
        }
​
        if (response.status >= 500 && attempt < this.retryAttempts) {
          // 服务器错误,阶梯性地重试
          await this.delay(this.retryDelay * Math.pow(2, attempt - 1));
          continue;
        }
​
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      } catch (error) {
        if (attempt === this.retryAttempts) {
          throw error;
        }
​
        console.warn(`第 ${attempt} 次尝试失败:`, error.message);
        await this.delay(this.retryDelay);
      }
    }
  }
​
  delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

实际应用场景:

  • 支付(处理超时)
  • 实时聊天
  • 不可靠服务的 API 集成
  • 网络条件差的 app

小技巧: 针对不同操作使用不同的超时值,例如 UI 交互 2 秒,文件上传 10 秒,数据处理 30 秒。

异步初始化(竞态条件预防)

意思是在并发或异步操作中,确保多个操作不会因为执行顺序不确定而产生错误结果或冲突

小技巧:专业开发者使用初始化来消除竞态条件,确保异步资源在需要时始终可用。

❌ 新手做法:竞态条件泛滥

class DataService {
  constructor() {
    this.data = null;
    this.loadData(); // 直接触发异步请求,危险!
  }
​
  async loadData() {
    this.data = await fetch('/api/data').then(r => r.json());
  }
​
  getData() {
    return this.data; // 可能为 null,如果调用过早
  }
}
​

✅ 专业做法:基于 Promise 的初始化

class DataService {
  constructor() {
    /* ================================================
     * 🎯 小技巧:存储 Promise 而非结果
     * 允许多个消费者等待同一个异步操作
     * ================================================ */
    this.dataPromise = this.initializeData();
    this.cache = new Map();
  }
​
  async initializeData() {
    try {
      /* ================================================
       * 🎯 小技巧:单次初始化 + 回退策略
       * 启动时优雅处理网络错误
       * ================================================ */
      const response = await fetch('/api/data');
      if (!response.ok) {
        throw new Error(`加载数据失败: ${response.status}`);
      }
​
      const data = await response.json();
​
      /* ================================================
       * 🎯 小技巧:加载时验证数据结构
       * 防止因返回数据格式错误导致运行时异常
       * ================================================ */
      if (!this.isValidData(data)) {
        console.warn('数据结构无效,使用默认值');
        return this.getDefaultData();
      }
​
      return data;
    } catch (error) {
      console.error('数据初始化失败:', error);
      return this.getDefaultData();
    }
  }
​
  async getData() {
    /* ================================================
     * 🎯 小技巧:始终返回 Promise
     * 完全消除时序问题
     * ================================================ */
    return await this.dataPromise;
  }
​
  async getItem(id) {
    if (this.cache.has(id)) {
      return this.cache.get(id);
    }
​
    /* ================================================
     * 🎯 小技巧:在操作前等待初始化完成
     * 确保数据已加载后再查找条目
     * ================================================ */
    await this.dataPromise;
    const item = await this.fetchItem(id);
    this.cache.set(id, item);
    return item;
  }
}

妙在哪里:将 Promise 存储起来而不是存储最终结果意味着所有使用者都会等待同一次初始化。无论方法何时被调用,它们要么立即获得数据,要么等待数据加载完成后再返回。

还有高手:

// 高级实现class AdvancedDataService {
  constructor() {
    this.state = 'initializing';
    this.initPromise = this.initialize();
    this.eventEmitter = new EventTarget();
  }
​
  async initialize() {
    try {
      this.state = 'loading';
      this.emit('stateChange', { state: 'loading' });
​
      /* ================================================
       * 🎯 高级技巧:渐进式初始化
       * 先加载关键数据,再加载额外数据
       * ================================================ */
      const critical = await this.loadCriticalData();
      this.state = 'partial';
      this.emit('stateChange', { state: 'partial', data: critical });
​
      const optional = await this.loadOptionalData();
      this.state = 'complete';
      this.emit('stateChange', { state: 'complete', data: { ...critical, ...optional } });
​
      return { ...critical, ...optional };
    } catch (error) {
      this.state = 'error';
      this.emit('stateChange', { state: 'error', error });
      throw error;
    }
  }
​
  async waitForState(targetState) {
    if (this.state === targetState) return;
​
    return new Promise(resolve => {
      const handler = (event) => {
        if (event.detail.state === targetState) {
          this.eventEmitter.removeEventListener('stateChange', handler);
          resolve(event.detail);
        }
      };
      this.eventEmitter.addEventListener('stateChange', handler);
    });
  }
​
  emit(event, data) {
    this.eventEmitter.dispatchEvent(
      new CustomEvent(event, { detail: data })
    );
  }
}
​
// 使用示例
const service = new AdvancedDataService();
await service.waitForState('partial'); // 可开始使用基础功能
// 后续...
await service.waitForState('complete'); // 全部功能可用

玩游戏的时候这汇总分部加载的方案体现的很直观,很多游戏主流程包下载完毕就可以体验,然后再后台下载一些附加玩法的资源。

真实场景应用

  • 数据库连接池
  • 身份认证 token 管理
  • 配置加载系统
  • 缓存预热策略
  1. 数据库连接池(Database connection pools)

    • 概念:连接池是一种管理数据库连接的技术。它会提前创建一定数量的数据库连接,并在应用程序需要访问数据库时复用这些连接,而不是每次都重新建立连接。
    • 目的:提升性能、减少连接建立和销毁的开销,同时控制数据库的并发访问量。
    • 举例:在 Node.js 中使用 pg-pool 管理 PostgreSQL 连接,或者在 Java 后端使用 HikariCP 连接池。
  2. 认证令牌管理(Authentication token management)

    • 概念:在用户认证过程中,会生成令牌(Token,例如 JWT),用于标识用户身份并控制访问权限。令牌管理指的是生成、存储、验证和刷新这些令牌的整个过程。
    • 目的:保证安全访问,防止未授权操作,同时在分布式系统中支持无状态认证。
    • 举例:前端获取登录返回的 JWT 存储在 localStorage 或 httpOnly cookie 中,后端通过中间件验证令牌有效性。
  3. 配置加载系统(Configuration loading systems)

    • 概念:应用启动或运行时需要从不同源加载配置(如环境变量、配置文件、远程配置服务)。配置加载系统负责统一管理这些配置。
    • 目的:保证应用启动时能正确读取配置、支持热更新配置、减少硬编码。
    • 举例:使用 dotenv 加载 .env 文件,或者使用 Spring Cloud Config、Apollo 来管理微服务配置。
  4. 缓存预热策略(Cache warming strategies)

    • 概念:在系统启动或更新后,将经常访问的数据提前加载到缓存中,以减少首次访问延迟。
    • 目的:提高系统性能,避免冷启动时用户请求被迫等待数据库或远程服务响应。
    • 举例:Redis 缓存中提前加载热门商品数据,或者前端 SSR 页面渲染时预先加载关键 API 数据。

小技巧:在初始化期间派发一些事件(比如当前加载资源的百分比),这样 UI 组件可以显示渐进式加载状态,而不是空白界面。

5. 异步状态机模式(Async State Machine,复杂流程管理)

小技巧:大佬把复杂的异步操作看作状态机,而不是简单的线性流程。这种方法能让调试、测试和错误恢复更加高效。

业余做法:线性流程混乱

async function syncUserData(userId) {
  const user = await fetchUser(userId);
  const updated = await updateUser(user);
  const synced = await syncToThirdParty(updated);
  return synced;
  // 无法清楚地知道错误发生在哪一步
}

专业做法:状态机方法

class UserSyncStateMachine {
  constructor(userId) {
    this.userId = userId;
    this.state = 'idle';  // 当前状态
    this.data = null;
    this.error = null;
    this.events = [];  // 记录状态变化事件
  }
​
  async execute() {
    try {
      await this.transition('fetching');
      // 🎯 小技巧:每个状态转换都是显式的
      this.data = await fetchUser(this.userId);
​
      await this.transition('validating');
      if (!this.isValidUser(this.data)) {
        throw new Error('用户数据结构无效');
      }
​
      await this.transition('updating');
      // 🎯 小技巧:状态在异步操作中保持
      this.data = await updateUser(this.data);
​
      await this.transition('syncing');
      this.data = await syncToThirdParty(this.data);
​
      await this.transition('completed');
      return this.data;
    } catch (error) {
      await this.transition('error', error);
      throw error;
    }
  }
​
  async transition(newState, data = null) {
    const previousState = this.state;
    this.state = newState;
​
    // 🎯 小技巧:事件日志用于调试
    const event = {
      timestamp: Date.now(),
      from: previousState,
      to: newState,
      data: data || this.data,
      error: newState === 'error' ? data : null
    };
​
    this.events.push(event);
​
    if (newState === 'error') this.error = data;
​
    // 🎯 小技巧:开发环境可模拟延迟
    if (process.env.NODE_ENV === 'development') {
      await new Promise(resolve => setTimeout(resolve, 100));
    }
​
    // 可用于外部监控
    this.emit('stateChange', event);
  }
​
  emit(event, data) {
    console.log(`UserSync[${this.userId}] ${event}:`, data);
  }
​
  getExecutionSummary() {
    return {
      userId: this.userId,
      finalState: this.state,
      totalTime: this.events.length > 0 ? 
        this.events[this.events.length - 1].timestamp - this.events[0].timestamp : 0,
      events: this.events,
      error: this.error
    };
  }
}

妙在哪里:状态机模式提供了对异步流程的完整可视化。可以在任意状态暂停、注入错误进行测试,并精确追踪错误位置,使复杂异步操作可调试、可测试。

还有高手:

class AsyncStateMachine {
  constructor(states, initialState = 'idle') {
    this.states = states;
    this.currentState = initialState;
    this.context = {};  // 存储状态上下文
    this.history = [];
  }
​
  async execute(input) {
    while (this.currentState !== 'completed' && this.currentState !== 'error') {
      // 🎯 高级:动态状态转换逻辑
      const stateConfig = this.states[this.currentState];
      if (!stateConfig) throw new Error(`未知状态: ${this.currentState}`);
​
      try {
        const result = await stateConfig.action(this.context, input);
        const nextState = stateConfig.next(result, this.context);
        await this.transition(nextState, result);
      } catch (error) {
        const errorState = stateConfig.onError || 'error';
        await this.transition(errorState, error);
      }
    }
​
    return this.context;
  }
​
  async transition(newState, data) {
    const event = {
      timestamp: Date.now(),
      from: this.currentState,
      to: newState,
      data
    };
​
    this.history.push(event);
    this.currentState = newState;
​
    // 🎯 秘诀:在转换过程中更新上下文
    if (data && typeof data === 'object') Object.assign(this.context, data);
  }
}
​
// 用法示例
const userSyncStates = {
  idle: { action: async () => ({ ready: true }), next: () => 'fetching' },
  fetching: {
    action: async (context, input) => {
      const user = await fetchUser(input.userId);
      return { user };
    },
    next: (result) => result.user ? 'updating' : 'error',
    onError: 'error'
  },
  updating: {
    action: async (context) => {
      const updated = await updateUser(context.user);
      return { user: updated };
    },
    next: () => 'completed',
    onError: 'error'
  }
};
​
const machine = new AsyncStateMachine(userSyncStates);
const result = await machine.execute({ userId: 123 });

实际应用场景

  • 支付流程管理(Payment processing workflows)
  • 文件上传与校验步骤(File upload with validation steps)
  • 多步骤表单提交(Multi-step form submissions)
  • 数据迁移流程(Data migration processes)

专业提示:任何包含超过 3 步或有复杂错误恢复需求的异步流程,都建议使用状态机。即便只是调试上的收益,也非常值得。

实践策略

  1. 先在现有代码中加入 错误边界(Error Boundaries) ,影响大且风险低。
  2. 对外部 API 调用添加 超时策略(Timeouts) ,能避免应用“卡死”。
  3. 队列模式(Async Queue) 在应用扩展时非常关键,应提前实现。
  4. 初始化模式(Async Initialization)状态机模式(Async State Machine) 用于最关键的异步流程——那些一旦出错就会让你彻夜难眠的逻辑。

大佬教我的一句话我永远不会忘:“异步代码的目的不是让事情更快,而是让事情更稳健。”