【前端面试小册】JS-第8节:Promise 相关题目与进阶应用
一、Promise 的取消机制
1.1 Promise 无法直接取消
核心问题:Promise 一旦进入 pending 状态,就无法直接取消。
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('完成');
  }, 5000);
});
// ❌ Promise 没有 cancel() 方法
// promise.cancel();  // 不存在此方法
// Promise 只能等待被解决(fulfilled)或被拒绝(rejected)
promise.then(result => {
  console.log(result);  // 5 秒后会输出:完成
});
原因分析:
graph TD
    A[创建 Promise] --> B[进入 pending 状态]
    B --> C{等待异步操作}
    C --> D[fulfilled: 成功]
    C --> E[rejected: 失败]
    D --> F[Promise 生命周期结束]
    E --> F
    G[无法中途取消] -.->|不支持| B
关键理解:
- Promise 的设计哲学是"一旦开始,就要完成"
 - 一旦进入 
pending状态,只能等待resolve或reject - 这是 Promise 与可取消操作(如 HTTP 请求)的根本区别
 
1.2 为什么需要取消 Promise?
在实际开发中,取消异步操作的需求很常见:
// 场景:用户快速切换标签,之前的请求应该取消
async function loadUserData(userId) {
  const data = await fetch(`/api/users/${userId}`);
  return data.json();
}
// 用户点击标签1
loadUserData(1);  // 开始加载
// 用户快速切换到标签2
loadUserData(2);  // 新请求
// 但标签1的请求仍在进行,浪费资源
常见场景:
- HTTP 请求取消(用户切换页面、组件卸载)
 - 定时器清理(组件销毁时取消定时任务)
 - 搜索防抖(新的搜索请求应该取消旧的)
 - 文件上传/下载取消
 
二、实现 Promise 取消的方案
方案一:使用可取消的 Promise 库
一些第三方库提供了可取消的 Promise 功能:
// 示例:使用 makeCancelable 工具函数
function makeCancelable(promise) {
  let hasCanceled = false;
  
  const wrappedPromise = new Promise((resolve, reject) => {
    promise.then(
      value => hasCanceled ? reject({ isCanceled: true }) : resolve(value),
      error => hasCanceled ? reject({ isCanceled: true }) : reject(error)
    );
  });
  
  return {
    promise: wrappedPromise,
    cancel() {
      hasCanceled = true;
    }
  };
}
// 使用示例
const somePromise = new Promise(resolve => {
  setTimeout(() => resolve('完成'), 5000);
});
const cancelable = makeCancelable(somePromise);
cancelable.promise
  .then(result => console.log(result))
  .catch(error => {
    if (error.isCanceled) {
      console.log('Promise 已取消');
    }
  });
// 2 秒后取消
setTimeout(() => {
  cancelable.cancel();
}, 2000);
原理说明:
- 使用标志位 
hasCanceled标记是否已取消 - Promise 完成时检查标志位
 - 如果已取消,返回特殊的错误对象
 
方案二:使用 AbortController(推荐)
AbortController 是现代浏览器提供的标准 API,用于取消异步操作。
// ✅ 使用 AbortController 取消 fetch 请求
const controller = new AbortController();
const signal = controller.signal;
// 发起 fetch 请求,传入 signal
const url = 'https://api.example.com/data';
const request = fetch(url, {
  method: 'GET',
  signal: signal  // 传入 AbortSignal
});
// 处理请求结果
request
  .then(response => {
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    return response.json();
  })
  .then(data => {
    console.log('数据:', data);
  })
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('请求已取消');
    } else {
      console.error('请求失败:', error.message);
    }
  });
// 500ms 后取消请求
setTimeout(() => {
  controller.abort();  // 取消请求
  console.log('请求已取消');
}, 500);
AbortController 执行流程:
graph TD
    A[创建 AbortController] --> B[获取 signal]
    B --> C[发起 fetch 请求]
    C --> D[传入 signal]
    D --> E{调用 abort}
    E -->|是| F[触发 AbortError]
    E -->|否| G[正常完成请求]
    F --> H[catch 捕获 AbortError]
    G --> I[then 处理结果]
关键点:
AbortController是 Web 标准 API,现代浏览器都支持signal可以传递给任何支持它的 API(fetch、ReadableStream 等)- 取消时会抛出 
AbortError,需要特殊处理 
方案三:封装可取消的 Promise 工具
// 创建一个可取消的 Promise 封装
function cancellablePromise(executor) {
  let cancel;
  const promise = new Promise((resolve, reject) => {
    cancel = (reason) => {
      reject(new Error(reason || 'Promise 已取消'));
    };
    executor(resolve, reject, () => {
      // 传入一个取消函数
      return cancel;
    });
  });
  
  return {
    promise,
    cancel: (reason) => cancel(reason)
  };
}
// 使用示例
const { promise, cancel } = cancellablePromise((resolve, reject, getCancel) => {
  const timer = setTimeout(() => {
    resolve('操作完成');
  }, 5000);
  
  // 保存取消函数,用于清理资源
  const cancelFn = getCancel();
  // 可以在这里做一些清理工作
  // 注意:这只是示例,实际使用需要更复杂的逻辑
});
promise
  .then(result => console.log(result))
  .catch(error => console.log('错误:', error.message));
// 2 秒后取消
setTimeout(() => {
  cancel('用户取消操作');
}, 2000);
三、axios 取消请求的实现原理
3.1 axios CancelToken(旧版 API)
axios 提供了 CancelToken 来取消请求(注意:axios 0.22.0+ 已废弃,推荐使用 AbortController):
import axios from 'axios';
// 创建 CancelToken 实例
const cancelToken1 = axios.CancelToken.source();
const cancelToken2 = axios.CancelToken.source();
// 发起多个请求,分别添加对应的 CancelToken
const request1 = axios.get('https://api.example.com/data1', {
  cancelToken: cancelToken1.token
});
const request2 = axios.get('https://api.example.com/data2', {
  cancelToken: cancelToken2.token
});
// 取消 request1 请求
cancelToken1.cancel('Request 1 canceled by user');
// 处理请求结果
Promise.all([request1, request2])
  .then(([response1, response2]) => {
    console.log('Response 1:', response1.data);
    console.log('Response 2:', response2.data);
  })
  .catch(error => {
    if (axios.isCancel(error)) {
      // 请求被取消
      console.log('请求已取消:', error.message);
    } else {
      // 其他错误
      console.error('请求失败:', error.message);
    }
  });
3.2 axios 取消请求原理
实现原理:
graph TD
    A[创建 CancelToken.source] --> B[返回 token 和 cancel 方法]
    B --> C[发起 axios 请求]
    C --> D[传入 cancelToken]
    D --> E[axios 内部监听 token]
    E --> F{调用 cancel 方法}
    F -->|是| G[token Promise rejected]
    F -->|否| H[正常请求流程]
    G --> I[中断 XMLHttpRequest]
    I --> J[请求被取消]
    H --> K[请求完成]
详细步骤:
- 
创建 CancelToken 实例:
const source = axios.CancelToken.source(); // 返回 { token: CancelToken实例, cancel: 取消函数 } - 
token 的内部实现:
// 简化版原理 class CancelToken { constructor(executor) { let cancel; this.promise = new Promise(resolve => { cancel = resolve; // 当调用 cancel 时,Promise 被 resolve }); executor(cancel); } } - 
axios 内部处理:
- axios 监听 
cancelToken.promise - 如果 Promise 被 resolve(即调用了 cancel),则中断请求
 - 使用 
XMLHttpRequest.abort()中断网络请求 
 - axios 监听 
 - 
中断请求:
// axios 内部伪代码 if (config.cancelToken) { config.cancelToken.promise.then(cancel => { xhr.abort(); // 中断 XMLHttpRequest reject(new Cancel('Request canceled')); }); } 
3.3 axios 新版本使用 AbortController(推荐)
axios 0.22.0+ 推荐使用 AbortController:
import axios from 'axios';
// 创建 AbortController
const controller = new AbortController();
// 发起请求
const request = axios.get('https://api.example.com/data', {
  signal: controller.signal  // 传入 signal
});
// 取消请求
controller.abort();
// 处理结果
request
  .then(response => console.log(response.data))
  .catch(error => {
    if (axios.isCancel(error)) {
      console.log('请求已取消');
    }
  });
四、实战应用场景
场景一:React 组件中的请求取消
import { useEffect, useRef } from 'react';
import axios from 'axios';
function UserProfile({ userId }) {
  const cancelTokenRef = useRef(null);
  
  useEffect(() => {
    // 创建取消令牌
    const source = axios.CancelToken.source();
    cancelTokenRef.current = source;
    
    // 发起请求
    axios.get(`/api/users/${userId}`, {
      cancelToken: source.token
    })
    .then(response => {
      console.log('用户数据:', response.data);
    })
    .catch(error => {
      if (axios.isCancel(error)) {
        console.log('请求已取消(组件卸载或 userId 变化)');
      } else {
        console.error('请求失败:', error);
      }
    });
    
    // 清理函数:组件卸载或 userId 变化时取消请求
    return () => {
      source.cancel('组件卸载或参数变化');
    };
  }, [userId]);
  
  return <div>用户信息加载中...</div>;
}
场景二:搜索防抖与请求取消
function createSearchWithCancel() {
  let currentController = null;
  
  return function search(keyword) {
    // 取消之前的请求
    if (currentController) {
      currentController.abort();
    }
    
    // 创建新的控制器
    currentController = new AbortController();
    
    // 发起新请求
    return fetch(`/api/search?q=${keyword}`, {
      signal: currentController.signal
    })
    .then(response => response.json())
    .catch(error => {
      if (error.name === 'AbortError') {
        console.log('搜索已取消(新的搜索请求)');
      } else {
        throw error;
      }
    });
  };
}
const search = createSearchWithCancel();
// 用户快速输入
search('a');    // 请求1
search('ab');   // 取消请求1,发起请求2
search('abc');  // 取消请求2,发起请求3
场景三:文件上传取消
async function uploadFile(file, onProgress, signal) {
  const formData = new FormData();
  formData.append('file', file);
  
  return fetch('/api/upload', {
    method: 'POST',
    body: formData,
    signal: signal,  // 支持取消
    // 使用 XMLHttpRequest 可以监听上传进度
  })
  .then(response => response.json())
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('文件上传已取消');
    } else {
      throw error;
    }
  });
}
// 使用示例
const controller = new AbortController();
uploadFile(file, onProgress, controller.signal);
// 用户点击取消按钮
document.getElementById('cancelBtn').addEventListener('click', () => {
  controller.abort();
});
五、最佳实践与注意事项
1. 始终检查取消错误
// ✅ 正确处理取消错误
fetch(url, { signal })
  .then(response => response.json())
  .catch(error => {
    if (error.name === 'AbortError') {
      // 请求被取消,这是正常情况,不需要特殊处理
      console.log('请求已取消');
      return;
    }
    // 其他错误需要处理
    console.error('请求失败:', error);
  });
2. 清理相关资源
// ✅ 取消请求时清理相关资源
const controller = new AbortController();
let timer = null;
fetch(url, { signal: controller.signal })
  .then(response => response.json())
  .then(data => {
    // 清理定时器
    if (timer) clearTimeout(timer);
    console.log(data);
  })
  .catch(error => {
    if (error.name === 'AbortError') {
      // 清理资源
      if (timer) clearTimeout(timer);
      console.log('请求已取消');
    }
  });
// 设置超时
timer = setTimeout(() => {
  controller.abort();
}, 5000);
3. React 组件中的使用
// ✅ 在 useEffect 中正确使用
useEffect(() => {
  const controller = new AbortController();
  
  fetchData(controller.signal)
    .then(data => setData(data))
    .catch(error => {
      if (error.name !== 'AbortError') {
        setError(error);
      }
    });
  
  // 清理函数
  return () => {
    controller.abort();
  };
}, [dependencies]);
4. 避免内存泄漏
// ❌ 不好的做法:没有清理
function loadData() {
  fetch('/api/data')
    .then(response => response.json())
    .then(data => {
      // 如果组件已卸载,这里仍然会执行,可能导致内存泄漏
      setData(data);
    });
}
// ✅ 好的做法:使用 AbortController 清理
function loadData() {
  const controller = new AbortController();
  
  fetch('/api/data', { signal: controller.signal })
    .then(response => response.json())
    .then(data => {
      setData(data);
    });
  
  return () => controller.abort();
}
六、面试要点总结
核心知识点
- Promise 无法直接取消:一旦进入 
pending状态,只能等待完成或被拒绝 - 取消方案:
- 使用 
AbortController(推荐,标准 API) - 使用第三方库(如 axios 的 CancelToken)
 - 自定义封装(使用标志位)
 
 - 使用 
 - axios 取消原理:基于 
CancelToken或AbortController,内部调用XMLHttpRequest.abort() 
常见面试题
Q1: Promise 可以取消吗?
答:原生 Promise 无法直接取消。一旦进入 pending 状态,只能等待 resolve 或 reject。但可以通过 AbortController、第三方库或自定义封装实现取消功能。
Q2: 如何实现 Promise 取消?
答:
- 使用 AbortController(推荐):现代浏览器的标准 API,支持 fetch、ReadableStream 等
 - 使用第三方库:如 axios 的 CancelToken(旧版)或 AbortController(新版)
 - 自定义封装:使用标志位 + Promise rejection 模拟取消
 
Q3: axios 取消请求的原理是什么?
答:
- 创建 
CancelToken实例(旧版)或使用AbortController(新版) - 将 token/signal 传入请求配置
 - axios 内部监听取消信号
 - 调用 
XMLHttpRequest.abort()中断网络请求 - Promise 被 reject,触发错误处理
 
实战建议
- ✅ 新项目使用 
AbortController(标准 API,兼容性好) - ✅ React 组件中在 
useEffect清理函数中取消请求 - ✅ 搜索、上传等场景要及时取消旧的请求
 - ✅ 正确处理 
AbortError,避免误报错误 - ✅ 取消请求时记得清理相关资源(定时器、监听器等)
 
查看7道真题和解析