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

更多阅读

相关推荐
zeijiershuai1 小时前
Vue框架
前端·javascript·vue.js
写完这行代码打球去1 小时前
没有与此调用匹配的重载
前端·javascript·vue.js
IT、木易1 小时前
大白话Vue Router 中路由守卫(全局守卫、路由独享守卫、组件内守卫)的种类及应用场景
前端·javascript·vue.js
程序员小续1 小时前
React 组件库:跨版本兼容的解决方案!
前端·react.js·面试
oil欧哟1 小时前
🥳 做了三个月的学习卡盒小程序,开源了!
前端·vue.js·微信小程序
奶球不是球1 小时前
el-table(elementui)表格合计行使用以及滚动条默认样式修改
前端·vue.js·elementui
liuyang___2 小时前
vue管理布局左侧菜单栏NavMenu
前端·javascript·vue.js
destinying3 小时前
Vue 项目“瘦身”神器:自动清理未引用代码的终极方案
前端·javascript·vue.js
bbb1693 小时前
react源码分析 setStatae究竟是同步任务还是异步任务
前端·javascript·react.js