【前端面试小册】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 状态,只能等待 resolvereject
  • 这是 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[请求完成]

详细步骤

  1. 创建 CancelToken 实例

    const source = axios.CancelToken.source();
    // 返回 { token: CancelToken实例, cancel: 取消函数 }
    
  2. token 的内部实现

    // 简化版原理
    class CancelToken {
      constructor(executor) {
        let cancel;
        this.promise = new Promise(resolve => {
          cancel = resolve;  // 当调用 cancel 时,Promise 被 resolve
        });
        executor(cancel);
      }
    }
    
  3. axios 内部处理

    • axios 监听 cancelToken.promise
    • 如果 Promise 被 resolve(即调用了 cancel),则中断请求
    • 使用 XMLHttpRequest.abort() 中断网络请求
  4. 中断请求

    // 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();
}

六、面试要点总结

核心知识点

  1. Promise 无法直接取消:一旦进入 pending 状态,只能等待完成或被拒绝
  2. 取消方案
    • 使用 AbortController(推荐,标准 API)
    • 使用第三方库(如 axios 的 CancelToken)
    • 自定义封装(使用标志位)
  3. axios 取消原理:基于 CancelTokenAbortController,内部调用 XMLHttpRequest.abort()

常见面试题

Q1: Promise 可以取消吗?

答:原生 Promise 无法直接取消。一旦进入 pending 状态,只能等待 resolvereject。但可以通过 AbortController、第三方库或自定义封装实现取消功能。

Q2: 如何实现 Promise 取消?

答:

  1. 使用 AbortController(推荐):现代浏览器的标准 API,支持 fetch、ReadableStream 等
  2. 使用第三方库:如 axios 的 CancelToken(旧版)或 AbortController(新版)
  3. 自定义封装:使用标志位 + Promise rejection 模拟取消

Q3: axios 取消请求的原理是什么?

答:

  1. 创建 CancelToken 实例(旧版)或使用 AbortController(新版)
  2. 将 token/signal 传入请求配置
  3. axios 内部监听取消信号
  4. 调用 XMLHttpRequest.abort() 中断网络请求
  5. Promise 被 reject,触发错误处理

实战建议

  • ✅ 新项目使用 AbortController(标准 API,兼容性好)
  • ✅ React 组件中在 useEffect 清理函数中取消请求
  • ✅ 搜索、上传等场景要及时取消旧的请求
  • ✅ 正确处理 AbortError,避免误报错误
  • ✅ 取消请求时记得清理相关资源(定时器、监听器等)
#前端面试#
全部评论

相关推荐

1-自我介绍2-为什么选前端3-对比同龄人,你有什么优势4-实验室规模5-你在实验室里面做什么任务6-项目是老师带的吗?7-你一天的学习时间怎么安排8-你作为ld,你怎么管理好一个团队9-前两段实习为什么离职10-未来规划,哪个方向11-对公司地点有什么要求吗?12-学校是二本吗?(学院本破防)我说要改大了,他说有什么用吗?13-问我高考分数14-为什么选计算机,你这分数能上一本吧15-你大学之前有了解过什么计算机吗?16-项目是导师分配的项目17-选一个项目来说吧,我说了首屏优化18-你优化中间这20几秒发生了什么?19-js,css,three,vue这些内存多大20-浏览器的渲染原理21-你上一次面试有遇到什么难题吗22-问我dpr像素,然后问我一个div设置宽高30px,图片是多大23-问我首屏图片比例24-说谁pbr和ssrpass吧,项目的25-说说pbr,ssrpass的参数属性有哪些26-你做过GIS,怎么按需加载的?27-应该高海拔到低海拔这些加载数据,怎么实现,说说看28-react常见钩子29-react的key30-说说虚拟dom吧31-说说Zustand32-说说drawcall,有限制多少个吗?33-做一个从左到右的动画,说说js,css,canvas哪种好34-你echarts用什么模式35-CPU和GPU区别,GPU多少核36-你觉得你做过最有成就感的一件事是什么37-实习多久38-未来几年你想做什么39-有没有面过腾讯,字节等,破防,就说了百度美的,说我百度过了没,我说挂了40-你想学习公司什么东西41-会不会经常请假,我说可能期末两周,其他一般不会42-还有在面试其他公司吗?反问最快什么时候出,下周还有三面吗?他说这边过了的话应该有第三次,破防
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务