🚫 AbortController 的 5 个注意点

1. AbortController 可以复用吗?

答:不能。一旦一个 signal 被 abort 他的状态就永远是 aborted=true 也就不会再触发 abort 事件。

有两种解决办法:

"全局"变量

tsx 复制代码
// 初始化一次
let abortController = new AbortController()

export default ({ question }) => {
  const handleCancel = () => {
    abortController.abort()
    
    // 每次 abort 后再初始化一次
    abortController = new AbortController()
  };
  
  const startChat = () => {
    request(
      { message: question, signal: abortController.signal },
      {
        onUpdate: (partialAnswer) => {
          setAnswer(partialAnswer)
        },
        onSuccess: () => {
          setThinkingStatus('Completed')
          setThinkingStopTime(Date.now())
        },
      },
    )
  }
}

ref

来保存引用,否则每次 rerender 都会实例化一个新的 AbortController 导致无法中断。

tsx 复制代码
export default ({ question }) => {
  // 初始化一次
  const abortControllerRef = React.useRef<AbortController>()
  
  const handleCancel = () => {
    abortController.abort()
    
    // 每次 abort 后再初始化一次
    abortControllerRef = new AbortController()
  }
  
  startChat() {
    request(
      { message: question, signal: abortControllerRef.signal },
      {
        onUpdate: (partialAnswer) => {
          setAnswer(partialAnswer)
        },
        onSuccess: () => {
          setThinkingStatus('Completed')
          setThinkingStopTime(Date.now())
        },
      },
    )
  }
  }
}

2. AbortController 能一次取消多个 fetch 请求吗?

答:能。注意是一次取消不是连续取消,和 1 并不冲突。因为背后的原理是 fetch 内部会监听来自同一个 abortController 信号的 abort 事件,也就是 abort 事件是"广播"可以被多个消费者感知。

ts 复制代码
let urls = [...]; // a list of urls to fetch in parallel

let controller = new AbortController();

// an array of fetch promises
let fetchJobs = urls.map(url => fetch(url, {
  signal: controller.signal
}));

let results = await Promise.all(fetchJobs);

// if controller.abort() is called from anywhere,
// it aborts all fetches

我们验证下:

从截图可以还可以看出 abort 事件回调是同步的。

3. 一个请求能被多个 AbortController 取消吗?

fetch 虽然只能接受一个 signal,但是我们可以利用 AbortSignal.any 这个静态方法传入 signal 数组。 只要其中之一 abort 则结果为 aborted。

ts 复制代码
const cancelDownloadButton = document.getElementById("cancelDownloadButton");

const userCancelController = new AbortController();

cancelDownloadButton.addEventListener("click", () => {
  userCancelController.abort();
});

// 创建 Timeout controller 实例的快捷方式,下文会介绍
const timeoutSignal = AbortSignal.timeout(1_000 * 60 * 5);

// 取决于用户取消和超时二者哪个快
const combinedSignal = AbortSignal.any([
  userCancelController.signal,
  timeoutSignal,
]);

try {
  const res = await fetch(someUrlToDownload, {
    signal: combinedSignal,
  });
} catch (e) {
  if (e.name === "AbortError") {
    // 用户取消
  } else if (e.name === "TimeoutError") {
    // 超时自动取消
  } else {
    // ...
  }
}

4. AbortController 除了能被 fetch 当做参数,还可以被哪些原生 API 当做参数?

在 fetch 中使用略过

ts 复制代码
const controller = new AbortController();
const signal = controller.signal;

// Start fetch
fetch('https://api.example.com/data', { signal })
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(err => {
    // On abort, the promise is rejected with an AbortError
    if (err.name === 'AbortError') {
      console.log('Fetch aborted');
    } else {
      console.error('Another error', err);
    }
  });

// Abort fetch after 2 seconds
setTimeout(() => controller.abort(), 2000);

addEventListener

ts 复制代码
// Create a new AbortController
const controller = new AbortController();

// Get the AbortSignal from the controller
const signal = controller.signal;

// Listen for click events on the document
document.addEventListener('click', () => {
  console.log('Document was clicked');
}, { signal });

// Call abort on the controller after 5 seconds
setTimeout(() => {
  controller.abort();
  console.log('No longer listening for clicks');
}, 5e3);

上述代码的效果是,五秒后 document 点击不生效,自动注销 click 回调,即被 abort 后,document 的 click event listener 数组长度将减一。相当于可控制的 once。

其他:Node.js 的流中。此处不表。

5. 创建超时自动 abort 的快捷静态方法 AbortSignal.timeout

AbortSignal.timeout(time) 将返回一个超时自动 abort 的实例。

使用 AbortSignal.timeout 简化版:

ts 复制代码
const signal = AbortSignal.timeout(5e3);

document.addEventListener('click', () => {
  console.log('Document was clicked');
}, { signal });

相比 setTimeout 手动 abort,该方法有个缺点无法在超时时做一些业务逻辑,因为无法监听 timeout 事件。

更多阅读

相关推荐
Ares-Wang8 分钟前
Vue》》@ 用法
前端·javascript·vue.js
Lazy_zheng2 小时前
React架构深度解析:从 Stack 到 Fiber,解决 CPU 和 I/O 瓶颈问题
前端·react.js·前端框架
张志鹏PHP全栈2 小时前
Vue3第五天,ref 和 reactive的介绍和区别
前端·vue.js
江城开朗的豌豆3 小时前
Vue/React凭什么吊打传统前端?6年老司机带你揭秘它们的性能杀招!
前端·javascript·vue.js
江城开朗的豌豆3 小时前
虚拟DOM:为什么React/Vue比直接操作DOM快10倍?
前端·javascript·vue.js
光影少年3 小时前
vue3.6更细哪些东西
前端·vue.js·掘金·金石计划
WildBlue3 小时前
React 遇上原子 CSS:前端开发的超级进化 🚀
前端·react.js
练习前端两年半3 小时前
Vue 3 Render函数深度解析:Text、Comment、Fragment节点的渲染机制
前端·vue.js
namehu3 小时前
“c is not a function” - 一次由 useEffect 异步函数引发的 React 底层崩溃分析
前端·javascript·react.js
iaku3 小时前
🔥React工程化实践:构建企业级可维护应用架构
前端·react.js·前端框架