JavaScript 异步编程核心:Promise 与 Async/Await 底层实现与错误处理最佳实践

7 阅读7分钟

一、Promise 的底层实现机制

1.1 Promise 规范与状态管理

Promise 是基于 Promises/A+ 规范实现的,本质上是一个状态机,具有三种状态:

  • pending(等待态)
  • fulfilled(完成态)
  • rejected(拒绝态)

一旦状态变更为 fulfilled 或 rejected,就不再改变。这种状态封装是 Promise 实现可靠性的核心。

// Promise 简易实现的核心部分
function MyPromise(executor) {
  this.state = 'pending';      // 初始状态
  this.value = undefined;      // 成功值
  this.reason = undefined;     // 失败原因
  this.onFulfilledCallbacks = [];  // 成功回调队列
  this.onRejectedCallbacks = [];   // 失败回调队列

  const resolve = (value) => {
    if (this.state === 'pending') {
      this.state = 'fulfilled';
      this.value = value;
      this.onFulfilledCallbacks.forEach(fn => fn(value));
    }
  };

  const reject = (reason) => {
    if (this.state === 'pending') {
      this.state = 'rejected';
      this.reason = reason;
      this.onRejectedCallbacks.forEach(fn => fn(reason));
    }
  };

  try {
    executor(resolve, reject);
  } catch (e) {
    reject(e);
  }
}

1.2 微任务队列与事件循环

Promise 的处理函数(.then().catch().finally())会被放入微任务队列中执行,而不是直接执行或者放入宏任务队列:

// Promise 处理为微任务的示例
console.log('同步开始');

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

setTimeout(() => {
  console.log('setTimeout宏任务');
}, 0);

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

// 输出顺序:
// 同步开始
// 同步结束
// Promise微任务
// setTimeout宏任务

底层实现上,V8 引擎会维护一个 Promise Jobs 队列,它在事件循环中的执行优先级高于宏任务。

1.3 Promise 链式调用实现

then 方法的实现是 Promise 链式调用的核心,它总是返回一个新的 Promise:

MyPromise.prototype.then = function(onFulfilled, onRejected) {
  // 确保参数为函数
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
  onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };
  
  // 创建新的 Promise 实例实现链式调用
  const promise2 = new MyPromise((resolve, reject) => {
    if (this.state === 'fulfilled') {
      queueMicrotask(() => {
        try {
          const x = onFulfilled(this.value);
          // 处理返回值(可能是普通值或Promise)
          resolvePromise(promise2, x, resolve, reject);
        } catch (e) {
          reject(e);
        }
      });
    }
    
    if (this.state === 'rejected') {
      queueMicrotask(() => {
        try {
          const x = onRejected(this.reason);
          resolvePromise(promise2, x, resolve, reject);
        } catch (e) {
          reject(e);
        }
      });
    }
    
    // 处理 pending 状态,将回调存入队列
    if (this.state === 'pending') {
      this.onFulfilledCallbacks.push(() => {
        queueMicrotask(() => {
          try {
            const x = onFulfilled(this.value);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      });
      
      this.onRejectedCallbacks.push(() => {
        queueMicrotask(() => {
          try {
            const x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      });
    }
  });
  
  return promise2;
};

二、Async/Await 的底层实现机制

2.1 生成器与协程

Async/Await 实际上是 Generator 和 Promise 的语法糖,底层实现依赖于 JavaScript 引擎的协程(Coroutine)机制:

// Generator 实现原理类似
function* generatorExample() {
  const result1 = yield Promise.resolve(1);
  console.log(result1);  // 1
  const result2 = yield Promise.resolve(2);
  console.log(result2);  // 2
  return 'done';
}

// 手动执行生成器
function runGenerator(gen) {
  const g = gen();
  
  function handle(result) {
    if (result.done) return Promise.resolve(result.value);
    
    return Promise.resolve(result.value).then(
      res => handle(g.next(res)),
      err => handle(g.throw(err))
    );
  }
  
  return handle(g.next());
}

// 模拟 async/await
runGenerator(generatorExample).then(res => console.log(res));

2.2 从编译到执行的转换过程

Async/Await 代码在编译阶段会被转换为状态机,类似于以下过程:

// 原始 async 函数
async function fetchData() {
  const user = await getUserData();
  const orders = await getOrdersData(user.id);
  return { user, orders };
}

// 被转换为(简化版)
function fetchData() {
  return new Promise((resolve, reject) => {
    let state = 0;  // 状态机状态
    let user, orders;
    
    function next(value) {
      switch (state) {
        case 0:
          state = 1;
          return Promise.resolve(getUserData()).then(nextStep, handleError);
        case 1:
          user = value;
          state = 2;
          return Promise.resolve(getOrdersData(user.id)).then(nextStep, handleError);
        case 2:
          orders = value;
          resolve({ user, orders });
          return;
      }
    }
    
    function handleError(error) {
      reject(error);
    }
    
    function nextStep(value) {
      try {
        return next(value);
      } catch (e) {
        handleError(e);
      }
    }
    
    next();
  });
}

2.3 暂停与恢复执行

Async/Await 的一个关键特性是能够暂停函数执行,等待 Promise 解决后再恢复执行。这个过程在引擎内部是通过保存和恢复执行上下文实现的:

async function processTasks() {
  console.log("开始处理");
  
  // 暂停执行,将控制权交还给事件循环
  const result1 = await task1();
  console.log("任务1完成:", result1);
  
  // 再次暂停执行
  const result2 = await task2(result1);
  console.log("任务2完成:", result2);
  
  return "所有任务完成";
}

JavaScript 引擎会保存函数的局部变量、执行位置等信息,当 Promise 解决后,重新载入这些信息并从暂停点继续执行。

三、错误处理最佳实践

3.1 Promise 错误冒泡与捕获

Promise 具有错误冒泡机制,错误会沿着 Promise 链向下传递,直到被捕获:

// 不推荐的方式:每个 then 单独处理错误
fetchData()
  .then(data => {
    try {
      return processData(data);
    } catch (e) {
      console.error("处理数据错误", e);
      throw e;  // 重新抛出以继续传递
    }
  })
  .then(result => {
    try {
      return formatResult(result);
    } catch (e) {
      console.error("格式化结果错误", e);
      throw e;
    }
  });

// 推荐的方式:利用错误冒泡
fetchData()
  .then(data => processData(data))
  .then(result => formatResult(result))
  .catch(error => {
    // 集中处理所有错误
    console.error("操作链中出现错误:", error);
    // 根据错误类型进行不同处理
    if (error instanceof NetworkError) {
      // 处理网络错误
    } else if (error instanceof ValidationError) {
      // 处理验证错误
    }
  });

3.2 Async/Await 错误处理模式

Async/Await 可以使用传统的 try/catch 结构进行错误处理,使代码更加清晰:

// 基础 try/catch 模式
async function fetchUserData(userId) {
  try {
    const user = await database.users.findById(userId);
    const permissions = await database.permissions.findByUserId(userId);
    return { user, permissions };
  } catch (error) {
    console.error("获取用户数据失败:", error);
    throw new UserDataError(error.message, { userId, originalError: error });
  }
}

// 更高级:错误边界模式
async function withErrorBoundary(asyncFn, errorHandler) {
  try {
    return await asyncFn();
  } catch (error) {
    return errorHandler(error);
  }
}

// 使用错误边界
const userData = await withErrorBoundary(
  () => fetchUserData(123),
  (error) => {
    reportErrorToMonitoring(error);
    return { user: null, permissions: [], error };
  }
);

3.3 自定义错误类与错误分类

定义明确的错误类有助于更精确地处理不同类型的错误:

// 自定义错误基类
class AppError extends Error {
  constructor(message, options = {}) {
    super(message);
    this.name = this.constructor.name;
    this.timestamp = new Date();
    this.code = options.code || 'ERR_UNKNOWN';
    this.data = options.data || {};
    
    // 记录错误发生的堆栈信息
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, this.constructor);
    }
  }
  
  toJSON() {
    return {
      name: this.name,
      message: this.message,
      code: this.code,
      timestamp: this.timestamp,
      data: this.data
    };
  }
}

// 特定错误类型
class NetworkError extends AppError {
  constructor(message, options = {}) {
    super(message, { code: 'ERR_NETWORK', ...options });
  }
}

class ValidationError extends AppError {
  constructor(message, options = {}) {
    super(message, { code: 'ERR_VALIDATION', ...options });
    this.validationErrors = options.validationErrors || [];
  }
  
  toJSON() {
    return {
      ...super.toJSON(),
      validationErrors: this.validationErrors
    };
  }
}

// 使用自定义错误
async function validateAndSubmitForm(formData) {
  try {
    const errors = validateForm(formData);
    if (errors.length > 0) {
      throw new ValidationError("表单验证失败", { 
        validationErrors: errors,
        data: { formId: formData.id }
      });
    }
    
    return await submitForm(formData);
  } catch (error) {
    if (error instanceof ValidationError) {
      // 处理验证错误
      showValidationErrors(error.validationErrors);
    } else if (error instanceof NetworkError) {
      // 处理网络错误
      retryWithExponentialBackoff(submitForm, formData);
    } else {
      // 未知错误
      reportToErrorTracking(error);
      showGenericErrorMessage();
    }
    throw error; // 或者可以返回一个默认值
  }
}

3.4 异步操作的终止与取消

Promise 原生不支持取消,但可以通过 AbortController 接口实现可取消的异步操作:

function fetchWithTimeout(url, options = {}) {
  const { timeout = 5000, ...fetchOptions } = options;
  const controller = new AbortController();
  const { signal } = controller;
  
  // 创建超时计时器
  const timeoutId = setTimeout(() => controller.abort(), timeout);
  
  // 发起请求
  const promise = fetch(url, { signal, ...fetchOptions })
    .then(response => {
      clearTimeout(timeoutId);
      if (!response.ok) {
        throw new NetworkError(`HTTP error ${response.status}`, { 
          status: response.status,
          data: { url }
        });
      }
      return response.json();
    })
    .catch(error => {
      clearTimeout(timeoutId);
      if (error.name === 'AbortError') {
        throw new NetworkError(`请求超时(${timeout}ms)`, {
          code: 'ERR_TIMEOUT',
          data: { url, timeout }
        });
      }
      throw error;
    });
    
  // 增强promise,添加取消能力
  promise.abort = () => controller.abort();
  return promise;
}

// 使用示例
async function loadUserProfile(userId) {
  const request = fetchWithTimeout(`/api/users/${userId}`, { timeout: 3000 });
  
  try {
    const userData = await request;
    updateUserInterface(userData);
  } catch (error) {
    if (error.code === 'ERR_TIMEOUT') {
      showTimeoutMessage();
    } else {
      showErrorMessage(error.message);
    }
  }
  
  // 如果需要,可以主动取消
  // someEvent.on('cancelRequest', () => request.abort());
}

四、高级异步模式与实战案例

4.1 并发控制模式

在实际应用中,常常需要控制异步操作的并发数量,避免资源过载:

// 并发限制器
class ConcurrencyManager {
  constructor(limit = 5) {
    this.limit = limit;
    this.running = 0;
    this.queue = [];
  }
  
  async add(fn) {
    // 返回一个Promise,表示任务的最终结果
    return new Promise((resolve, reject) => {
      // 将任务及其解决/拒绝函数加入队列
      this.queue.push({ fn, resolve, reject });
      this.run();
    });
  }
  
  async run() {
    // 如果有等待任务且未达到并发限制
    if (this.queue.length > 0 && this.running < this.limit) {
      this.running++;
      const { fn, resolve, reject } = this.queue.shift();
      
      try {
        // 执行任务
        const result = await fn();
        resolve(result);
      } catch (error) {
        reject(error);
      } finally {
        this.running--;
        // 继续处理队列中的下一个任务
        this.run();
      }
    }
  }
}

// 使用示例:批量处理图片
async function processImages(imageUrls) {
  const concurrency = new ConcurrencyManager(3);  // 最多同时处理3张图片
  const results = [];
  
  for (const url of imageUrls) {
    // 添加到并发管理器
    results.push(
      concurrency.add(async () => {
        const image = await downloadImage(url);
        const processed = await applyFilters(image);
        return await uploadProcessedImage(processed);
      })
    );
  }
  
  // 等待所有图片处理完成
  return Promise.all(results);
}

4.2 实战案例:可恢复的文件上传

这个案例融合了前面讨论的多种技术,实现了一个带有重试、进度报告和错误处理的文件上传系统:

class ResumableUploader {
  constructor(file, url, options = {}) {
    this.file = file;
    this.url = url;
    this.chunkSize = options.chunkSize || 1024 * 1024;  // 默认1MB
    this.retries = options.retries || 3;
    this.retryDelay = options.retryDelay || 1000;  // 默认1秒
    this.onProgress = options.onProgress || (() => {});
    this.onError = options.onError || (() => {});
    this.onSuccess = options.onSuccess || (() => {});
    
    this.uploadId = null;
    this.chunks = this._createChunks();
    this.controller = new AbortController();
    this.isAborted = false;
  }
  
  _createChunks() {
    const chunks = [];
    let start = 0;
    
    while(start < this.file.size) {
      const end = Math.min(start + this.chunkSize, this.file.size);
      chunks.push({
        blob: this.file.slice(start, end),
        start,
        end,
        progress: 0,
        status: 'pending', // pending, uploading, failed, success
        retries: 0
      });
      start = end;
    }
    
    return chunks;
  }
  
  async start() {
    try {
      // 初始化上传
      this.uploadId = await this._initializeUpload();
      
      // 上传所有分块
      await this._uploadChunks();
      
      if (this.isAborted) return null;
      
      // 完成上传
      const result = await this._finalizeUpload();
      this.onSuccess(result);
      return result;
    } catch (error) {
      if (!this.isAborted) {
        this.onError(error);
      }
      throw error;
    }
  }
  
  abort() {
    this.isAborted = true;
    this.controller.abort();
  }
  
  async _initializeUpload() {
    return await this._retryOperation(async () => {
      const response = await fetch(`${this.url}/initialize`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          filename: this.file.name,
          contentType: this.file.type,
          size: this.file.size,
          totalChunks: this.chunks.length
        }),
        signal: this.controller.signal
      });
      
      if (!response.ok) {
        throw new NetworkError(`初始化上传失败: ${response.status}`, { 
          status: response.status 
        });
      }
      
      const data = await response.json();
      return data.uploadId;
    });
  }
  
  async _uploadChunks() {
    // 创建并发控制器
    const concurrency = new ConcurrencyManager(3);
    
    // 为每个块创建上传任务
    const uploadTasks = this.chunks.map((chunk, index) => {
      return concurrency.add(async () => {
        if (this.isAborted) return;
        return await this._uploadChunk(chunk, index);
      });
    });
    
    // 等待所有块上传完成
    await Promise.all(uploadTasks);
  }
  
  async _uploadChunk(chunk, index) {
    chunk.status = 'uploading';
    
    return this._retryOperation(async () => {
      if (this.isAborted) return;
      
      try {
        const formData = new FormData();
        formData.append('uploadId', this.uploadId);
        formData.append('chunkIndex', index);
        formData.append('chunk', chunk.blob);
        
        const xhr = new XMLHttpRequest();
        
        // 监听上传进度
        xhr.upload.addEventListener('progress', (event) => {
          if (event.lengthComputable) {
            const chunkProgress = event.loaded / event.total;
            chunk.progress = chunkProgress;
            this._updateOverallProgress();
          }
        });
        
        // 包装XHR为Promise
        const uploadPromise = new Promise((resolve, reject) => {
          xhr.open('POST', `${this.url}/chunk`);
          
          xhr.onload = () => {
            if (xhr.status >= 200 && xhr.status < 300) {
              chunk.status = 'success';
              chunk.progress = 1;
              this._updateOverallProgress();
              resolve(JSON.parse(xhr.responseText));
            } else {
              chunk.status = 'failed';
              reject(new NetworkError(`上传分块失败: ${xhr.status}`, { status: xhr.status }));
            }
          };
          
          xhr.onerror = () => {
            chunk.status = 'failed';
            reject(new NetworkError('网络错误'));
          };
          
          xhr.onabort = () => {
            reject(new Error('上传已取消'));
          };
        });
        
        // 连接到abort控制器
        this.controller.signal.addEventListener('abort', () => xhr.abort());
        
        // 发送请求
        xhr.send(formData);
        return await uploadPromise;
        
      } catch (error) {
        chunk.status = 'failed';
        throw error;
      }
    }, chunk);
  }
  
  async _finalizeUpload() {
    return await this._retryOperation(async () => {
      const response = await fetch(`${this.url}/finalize`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          uploadId: this.uploadId
        }),
        signal: this.controller.signal
      });
      
      if (!response.ok) {
        throw new NetworkError(`完成上传失败: ${response.status}`, { status: response.status });
      }
      
      return await response.json();
    });
  }
  
  async _retryOperation(operation, chunk = null) {
    let lastError;
    let retries = chunk ? chunk.retries : 0;
    const maxRetries = chunk ? this.retries : this.retries * 2;  // 非分块操作允许更多重试
    
    while (retries < maxRetries) {
      try {
        return await operation();
      } catch (error) {
        if (this.isAborted) throw new Error('上传已取消');
        
        lastError = error;
        retries++;
        
        if (chunk) {
          chunk.retries = retries;
        }
        
        // 如果达到最大重试次数,抛出错误
        if (retries >= maxRetries) {
          throw new Error(`操作失败,已重试${retries}次: ${lastError.message}`);
        }
        
        // 指数退避策略
        const delay = this.retryDelay * Math.pow(2, retries - 1);
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
    
    throw lastError;
  }
  
  _updateOverallProgress() {
    if (this.isAborted) return;
    
    // 计算总体进度
    const totalProgress = this.chunks.reduce((sum, chunk) => sum + chunk.progress, 0) / this.chunks.length;
    
    // 报告进度
    this.onProgress({
      totalProgress,
      uploadedBytes: this.chunks.reduce((sum, chunk) => sum + (chunk.blob.size * chunk.progress), 0),
      totalBytes: this.file.size
    });
  }
}

// 使用示例
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  if (!file) return;
  
  const uploader = new ResumableUploader(file, 'https://api.example.com/upload', {
    onProgress: (progress) => {
      document.getElementById('progressBar').value = progress.totalProgress * 100;
      document.getElementById('progressText').textContent = 
        `${Math.round(progress.totalProgress * 100)}% (${formatBytes(progress.uploadedBytes)}/${formatBytes(progress.totalBytes)})`;
    },
    onError: (error) => {
      console.error('上传错误:', error);
      document.getElementById('statusText').textContent = `上传失败: ${error.message}`;
    },
    onSuccess: (result) => {
      document.getElementById('statusText').textContent = '上传成功!';
      document.getElementById('fileLink').href = result.fileUrl;
      document.getElementById('fileLink').textContent = result.fileName;
    }
  });
  
  document.getElementById('cancelButton').addEventListener('click', () => {
    uploader.abort();
    document.getElementById('statusText').textContent = '上传已取消';
  });
  
  document.getElementById('statusText').textContent = '开始上传...';
  try {
    await uploader.start();
  } catch (e) {
    console.error('上传过程中发生错误:', e);
  }
});

// 工具函数:格式化字节大小
function formatBytes(bytes, decimals = 2) {
  if (bytes === 0) return '0 Bytes';
  
  const k = 1024;
  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  
  return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
}

五、总结与最佳实践

5.1 Promise/Async 最佳实践汇总

  1. 总是返回 Promise

    • 异步函数应始终返回 Promise,保持接口一致性
    • 避免混合使用回调和 Promise
  2. 使用 async/await 代替链式 then

    • 提高代码可读性和维护性
    • 使错误处理更加直观
  3. 合理运用并行与串行

    • 使用 Promise.all 并行处理独立操作
    • 使用 for...of 循环和 await 实现串行操作
  4. 集中处理错误

    • 使用自定义错误类进行分类
    • 在适当的层级处理错误,避免在每个 Promise 中都处理
  5. 冷静处理拒绝状态

    • 不要忽略 Promise 的拒绝状态
    • 使用 Promise.allSettled 处理多个可能失败的 Promise
  6. 取消长时间运行的操作

    • 使用 AbortController 实现可取消的操作
    • 在组件卸载或状态变化时及时取消
  7. 避免回调地狱的等价物

    • 避免嵌套 async/await 函数
    • 提取有意义的函数名,而不是内联匿名函数

5.2 未来展望

随着 JavaScript 的发展,异步编程模型将继续演进:

  1. 可观察对象(Observable):处理多值异步流,已在 RxJS 等库中实现,可能成为语言标准

  2. 顶层 await:在模块顶层使用 await,无需包裹在 async 函数中

  3. 错误处理增强:更好的错误传播和处理机制

  4. 原生取消支持:语言层面对异步操作取消的官方支持

Promise 和 Async/Await 已经显著改善了 JavaScript 的异步编程体验,但掌握其底层机制和最佳实践对于编写高效、可维护的代码至关重要。通过理解其工作原理和采用合适的错误处理策略,我们可以充分发挥 JavaScript 异步编程模型的优势。