JavaScript异步编程:FE-Interview中的Promise/async考点

JavaScript异步编程:FE-Interview中的Promise/async考点

【免费下载链接】FE-Interview 🔥🔥🔥 前端面试,独有前端面试题详解,前端面试刷题必备,1000+前端面试真题,Html、Css、JavaScript、Vue、React、Node、TypeScript、Webpack、算法、网络与安全、浏览器 【免费下载链接】FE-Interview 项目地址: https://gitcode.com/gh_mirrors/fei/FE-Interview

你是否还在为异步编程中的回调地狱烦恼?是否在面试中被Promise状态转换问题难住?本文将通过FE-Interview项目中的经典题目,带你一文掌握Promise/async/await的核心考点,解决90%的异步编程面试问题。

Promise核心特性解析

Promise(承诺)是JavaScript中处理异步操作的标准方案,它解决了传统回调函数嵌套过深的问题。在FE-Interview/summarry/javascript.md中,编号427题详细介绍了Promise的实现原理。

三种状态与状态转换

Promise有三种不可逆转的状态:

  • Pending(进行中):初始状态
  • Fulfilled(已成功):操作完成
  • Rejected(已失败):操作出错

状态转换路径只有两种:

  • Pending → Fulfilled
  • Pending → Rejected

mermaid

核心方法使用示例

// 创建Promise实例
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    if (Math.random() > 0.5) {
      resolve('操作成功'); // 状态变为Fulfilled
    } else {
      reject(new Error('操作失败')); // 状态变为Rejected
    }
  }, 1000);
});

// 处理结果
promise
  .then(result => console.log(result))  // 成功回调
  .catch(error => console.error(error)) // 失败回调
  .finally(() => console.log('操作完成')); // 无论成功失败都会执行

手写Promise实现

面试中常考手写Promise实现,FE-Interview/summarry/javascript.md的427题要求实现一个完整的Promise。核心要点包括:

  1. 状态管理(pending/fulfilled/rejected)
  2. 回调函数存储与执行
  3. then方法链式调用
  4. 错误捕获机制
class MyPromise {
  constructor(executor) {
    this.state = 'pending';
    this.value = null;
    this.reason = null;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

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

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

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

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      if (this.state === 'fulfilled') {
        setTimeout(() => {
          try {
            const result = onFulfilled(this.value);
            resolve(result);
          } catch (error) {
            reject(error);
          }
        }, 0);
      }

      if (this.state === 'rejected') {
        setTimeout(() => {
          try {
            const result = onRejected(this.reason);
            resolve(result);
          } catch (error) {
            reject(error);
          }
        }, 0);
      }

      if (this.state === 'pending') {
        this.onFulfilledCallbacks.push(() => {
          setTimeout(() => {
            try {
              const result = onFulfilled(this.value);
              resolve(result);
            } catch (error) {
              reject(error);
            }
          }, 0);
        });

        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const result = onRejected(this.reason);
              resolve(result);
            } catch (error) {
              reject(error);
            }
          }, 0);
        });
      }
    });
  }

  catch(onRejected) {
    return this.then(null, onRejected);
  }
}

Promise并发控制方法

在实际开发中,我们经常需要控制并发请求数量。FE-Interview/summarry/javascript.md的730题要求实现一个带并发限制的请求函数。

Promise.all与Promise.race

Promise提供了两个常用的并发控制方法:

  • Promise.all:等待所有Promise完成,返回结果数组
  • Promise.race:只要有一个Promise完成,就返回其结果
// 实现Promise.all
Promise.all = function(promises) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      return reject(new TypeError('参数必须是数组'));
    }

    const results = [];
    let completed = 0;
    const length = promises.length;

    if (length === 0) {
      return resolve(results);
    }

    promises.forEach((promise, index) => {
      Promise.resolve(promise).then(result => {
        results[index] = result;
        completed++;
        
        if (completed === length) {
          resolve(results);
        }
      }, reason => {
        reject(reason);
      });
    });
  });
};

实现带并发限制的请求函数

// 控制最大并发数的请求函数
function requestWithLimit(urls, max) {
  return new Promise((resolve) => {
    if (urls.length === 0) {
      resolve([]);
      return;
    }
    
    const results = [];
    let index = 0; // 当前请求索引
    let count = 0; // 已完成请求数量

    // 发送请求
    async function request() {
      if (index >= urls.length) return;
      
      const i = index;
      const url = urls[index];
      index++;
      
      try {
        const response = await fetch(url);
        results[i] = await response.json();
      } catch (error) {
        results[i] = error;
      } finally {
        count++;
        
        // 所有请求完成
        if (count === urls.length) {
          resolve(results);
        }
        
        // 继续发送请求
        request();
      }
    }

    // 启动max个并发请求
    const tasks = Math.min(max, urls.length);
    for (let i = 0; i < tasks; i++) {
      request();
    }
  });
}

async/await语法糖详解

async/await是ES2017引入的语法糖,基于Promise实现,让异步代码看起来像同步代码。FE-Interview/summarry/javascript.md的109题对比了Promise和async/await的使用场景。

基本用法与错误处理

// 使用async/await重构Promise代码
async function fetchData() {
  try {
    // 等待Promise完成
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    
    // 同步写法,异步执行
    console.log(data);
    return data;
  } catch (error) {
    // 集中错误处理
    console.error('请求失败:', error);
    throw error; // 可以继续向上抛出错误
  }
}

// 调用async函数
fetchData()
  .then(data => console.log('处理结果:', data))
  .catch(error => console.error('捕获错误:', error));

并发请求优化

使用async/await时,可以通过Promise.all实现并发请求,提高性能:

async function fetchMultipleData() {
  try {
    // 同时发起多个请求,而不是顺序执行
    const [userData, productData, orderData] = await Promise.all([
      fetch('/api/user').then(res => res.json()),
      fetch('/api/products').then(res => res.json()),
      fetch('/api/orders').then(res => res.json())
    ]);
    
    return { userData, productData, orderData };
  } catch (error) {
    console.error('获取数据失败:', error);
  }
}

常见面试题解析

题目1:Promise状态转换与链式调用

问题:Promise有几种状态?通过catch捕获到reject之后,在catch后面还能继续执行then方法吗?

解答:Promise有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。状态一旦改变就不会再变。catch之后可以继续执行then,因为catch返回的仍然是一个Promise对象。

Promise.reject(new Error('错误'))
  .catch(error => {
    console.error('捕获错误:', error);
    return '从错误中恢复'; // 返回正常值,使后续then执行成功回调
  })
  .then(result => {
    console.log('继续执行:', result); // 会执行,输出"继续执行: 从错误中恢复"
    return Promise.reject(new Error('新错误'));
  })
  .then(
    () => console.log('不会执行'),
    error => console.error('捕获新错误:', error) // 会执行
  );

题目2:async/await错误处理最佳实践

问题:如何在async/await中捕获多个异步操作的错误?

解答:可以使用try/catch捕获所有错误,或为每个await表达式单独添加错误处理:

async function multipleOperations() {
  // 方法1: 统一捕获所有错误
  try {
    const result1 = await asyncOperation1();
    const result2 = await asyncOperation2();
  } catch (error) {
    console.error('捕获错误:', error);
  }
  
  // 方法2: 单独处理每个操作的错误
  const result3 = await asyncOperation3().catch(error => {
    console.error('操作3失败:', error);
    return null; // 返回默认值
  });
  
  const result4 = await asyncOperation4().catch(error => {
    console.error('操作4失败:', error);
    return null;
  });
  
  return { result3, result4 };
}

题目3:Promise执行顺序

问题:分析以下代码的输出顺序:

console.log('start');

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

new Promise((resolve) => {
  console.log('promise executor');
  resolve();
}).then(() => {
  console.log('promise then');
});

console.log('end');

解答:输出顺序为:start → promise executor → end → promise then → setTimeout。

原因是:

  1. 同步代码优先执行
  2. Promise构造函数是同步执行的
  3. then回调属于微任务(Microtask),在同步代码执行完后立即执行
  4. setTimeout回调属于宏任务(Macrotask),在微任务之后执行

异步编程最佳实践

1. 避免嵌套结构

即使使用Promise,也应避免不必要的嵌套:

// 不好的写法
getUser(userId)
  .then(user => {
    getPosts(user.id)
      .then(posts => {
        getComments(posts[0].id)
          .then(comments => {
            console.log(comments);
          });
      });
  });

// 好的写法
getUser(userId)
  .then(user => getPosts(user.id))
  .then(posts => getComments(posts[0].id))
  .then(comments => console.log(comments))
  .catch(error => console.error(error));

// 最好的写法 (async/await)
async function getCommentsForFirstPost(userId) {
  try {
    const user = await getUser(userId);
    const posts = await getPosts(user.id);
    const comments = await getComments(posts[0].id);
    return comments;
  } catch (error) {
    console.error(error);
    throw error;
  }
}

2. 错误处理策略

为所有异步操作添加错误处理:

// 全局未捕获错误处理
window.addEventListener('unhandledrejection', event => {
  console.error('未处理的Promise错误:', event.reason);
  // 可以设置event.preventDefault()来阻止浏览器默认处理
});

// 局部错误处理
async function criticalOperation() {
  try {
    // 关键操作
    await riskyOperation();
  } catch (error) {
    // 记录错误日志
    logErrorToService(error);
    
    // 展示友好提示
    showUserMessage('操作失败,请稍后重试');
    
    // 可以选择重新尝试或回滚操作
    if (shouldRetry(error)) {
      return retryOperation();
    }
  }
}

总结

异步编程是JavaScript面试的必考点,掌握Promise和async/await不仅能应对面试,更能写出优雅高效的异步代码。通过FE-Interview项目中的JavaScript题目汇总,可以系统学习更多异步编程相关题目。

关键知识点回顾:

  • Promise的三种状态及状态转换规则
  • then方法的链式调用和值传递
  • async/await语法糖的使用和错误处理
  • 微任务与宏任务的执行顺序
  • 并发控制方法(Promise.all, Promise.race等)

掌握这些知识点,你就能从容应对各类异步编程面试题,写出更健壮的异步代码。

【免费下载链接】FE-Interview 🔥🔥🔥 前端面试,独有前端面试题详解,前端面试刷题必备,1000+前端面试真题,Html、Css、JavaScript、Vue、React、Node、TypeScript、Webpack、算法、网络与安全、浏览器 【免费下载链接】FE-Interview 项目地址: https://gitcode.com/gh_mirrors/fei/FE-Interview

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

抵扣说明:

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

余额充值