🚫 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 事件。

更多阅读

相关推荐
雪碧聊技术1 小时前
深入解析Vue中v-model的双向绑定实现原理
前端·javascript·vue.js·v-model
Sun_light2 小时前
6个你必须掌握的「React Hooks」实用技巧✨
前端·javascript·react.js
百锦再2 小时前
重新学习Vue中的按键监听和鼠标监听
javascript·vue.js·vue·计算机外设·click·up·down
快起来别睡了2 小时前
Vue 3 中的组件通信与组件思想详解
vue.js
不讲道理的柯里昂2 小时前
Vue MathJax Beautiful,基于Mathjax的数学公式编辑插件
vue.js·开源
啷咯哩咯啷2 小时前
Vue3构建低代码表单设计器
前端·javascript·vue.js
用户26124583401612 小时前
vue学习路线(10.监视属性-watch)
前端·vue.js
小山不高2 小时前
react中封装组件对三方库或者已有ui组件进行自动调整大小嵌入显示区域
react.js
Spider_Man2 小时前
React-Router 全面解析与实战指南
前端·react.js
慧一居士3 小时前
Vite 完整功能详解与 Vue 项目实战指南
前端·vue.js