cheungseol / cheungseol.github.io

2 stars 0 forks source link

AbortController 使用总结 #22

Open cheungseol opened 5 years ago

cheungseol commented 5 years ago

AbortController 是一个DOM API,提供了终止 Promise 网络请求继续执行的能力

业务中可能遇到的场景

  1. 用户连续的输入,频繁的调用请求接口😫(传统的解决方案有很多:Rxjs Observable 、节流等等)
  2. 查看地图时需要加载比较多资源和文件,当用户频繁的调整地图的缩放比例时,需要展示最新尺寸的图形,但加载历史尺寸的网络请求还在缓慢地执行和响应中,占用网络带宽,是我们不期望看到的结果🤦🏼‍♀️🤦🏼‍♂️🤦🏻‍♀️🤦🏻‍♂️🤦🏽‍♀️🤦🏽‍♂️🤦‍♀️🤦‍♂️🤦🏿‍♀️ 🤦🏿‍♂️

Demo

演示一个根据用户输入内容调用接口请求数据的🌰例子

image

image

使用方式

调用 AbortController 构造函数,返回 controller 实例对象 和 signal 对象,将 signal 作为参数传给 fetch ,执行 controller.abort() 方法终止 fetch 请求

// 调用 AbortController 构造函数,新建一个实例对象
let abortController = new AbortController();
let { signal } = abortController;
// 将实例对象返回的 signal 作为参数传给 fetch 请求
async function getGists(){
    const response = await fetch('https://api.github.com/gists', { signal });
    const gists = await response.json();
    console.info(gists);
};
// 可以通过绑定 EventListener 监听 signal 变化
signal.addEventListener('abort', () => {
    console.error('[SIGNAL ABORT EVENT TRIGGERED]')
})
signal.onabort = () => {
    console.error('[SIGNAL ONABORT EVENT TRIGGERED]')
}
getGists()
// 触发实例对象的 abort 方法,signal 触发一个 AbortDomError 事件
abortController.abort()
// signal 的状态变为 aborted,未执行完的 fetch 被 abort 终止,同时触发 EventListener 队列中的回调

请求超时

const racefetch = async function(timeout = 3000){
    let fetchPromise = fetch('https://api.github.com/gists')
        .then(r=> r.json())
        .then(p => {
            console.log('[RESP]', p)
            return p
        })
    let timeoutPromise = new Promise(function(resolve, reject){
        setTimeout(()=>{
             reject(new Error("fetch timeout"))
        }, timeout)
    });
    return Promise.race([fetchPromise, timeoutPromise])
}

可以看到👇下面的测试结果,虽然能实现超时 throw error 的效果,但并没有真正阻止 fetch 请求的传输 🤷‍♂️

image

const controllerFetch = async function (timeout = 3000) {
    async function  doRequest() {
        const controller = new AbortController();
        const signal = controller.signal;
        const timer = setTimeout(() => controller.abort(), timeout);
        try {
            const resp = await fetch('https://api.github.com/gists', {signal}) 
            const respJson = await resp.json()
            return respJson
        } catch (e) {
            return Promise.reject(e);
        } finally {
            clearTimeout(timer);
        } 
    } 
    return doRequest();
}

下图是使用使用 AbortController 的方式,可以在设定的 timeout 时真正意义上的关闭网络连接 🌟,0 字节传输

image

使用中需要注意的地方

看源码的实现就可以知道为什么,AbortSignal 在 abort 之后,状态就变成了 aborted,那么下次调用 fetch 请求的时候,当读到 AbortSignal 的状态是 aborted 的时候,就直接 reject 返回了。

// Return early if already aborted, thus avoiding making an HTTP request
if (signal.aborted) {
    return Promise.reject(abortError);
}

所以如果有请求重试的需求的话,需要每次调用 fetch 前都重新 new 一个 AbortController,返回一个新的 signal

// 第一个 AbortController
let firstController = new AbortController();
let { signal: firstSignal } = firstController;
// 第二个 AbortController
let anotherController = new AbortController();
let { signal: anotherSignal } = anotherController;

async function getGists(signal){
    const response = await fetch('https://api.github.com/gists', { signal });
    const gists = await response.json();
    console.log(gists);
};
// 终止第一个 AbortSignal
firstController.abort()
// 被 firstSignal 标记的请求已经终止
getGists(firstSignal)
// anotherSignal 标记的 fetch 请求不受影响仍然可以执行
getGists(anotherSignal)
const controller = new AbortController()
const { signal } = controller
async function fetchAll({ signal }={}) {  
  const fetchSingle = [1,2,3].map(async () => {
    console.info("[SIGANL STATUS]", signal)
    const response = await fetch('https://api.github.com/gists', { signal });
    return response.json();
  });
  return Promise.all(fetchSingle);
}
// 调用
const controller = new AbortController();
const signal = controller.signal;

fetchAll({ signal }) 

👇 下图可以看到,当 signal 被 abort 后, 所有被 signal 标记的 fetch 请求都终止了:

image

兼容性 🌚

🥇Edge 浏览器率先在 2017 年支持 AbortController API ,FireFox 紧随其后 🏃‍♂️,我们常用的 Chrome 是在 2018 年的 66 版本开始支持的 😅

image

AbortController 的实现

需要浏览器的支持去关闭 TCP socket 连接才能真正意义上结束一次网络请求。这里介绍 fetch polyfill 对 AbortController 的实现。

fetch polyfill 对 AbortController 的实现

虽然是一个 DOM API,但是借助 polyfill 可以实现在 service-worker、nodejs 等环境中使用AbortController(相关 MR

abortcontroller-polyfill 模块

fetch polyfill 在 service-worker 中的实现其实是引用了abortcontroller-polyfill 模块,模块主要完成两件事情:

  1. 对 AbortController 和 AbortSignal 的 polyfill
  2. 修改全局的 fetch 函数,使 fetch 具备接收和响应 AbortSignal 事件的能力

通过源码分析,很容易发现其实这个 polyfill 的目的是让用户可以在不支持 AbortController 的 js 执行环境中仍然能够使用这个属性,并且得到相似的运行结果。虽然结果看上去相似,但效果并不相同 🙅‍♂️(它并无法真正的去实现关闭 TCP 连接,只是在 abort 事件发生时不再对 fetch请求结果作处理)

参考