• home > webfront > SGML > html5 >

    中断操作:AbortController学习笔记

    Author:zhoulujun Date:

    AbortController是一个DOM API。MDN上对它的介绍是 AbortController接口表示一个控制器对象。AbortController可以用在fetch和addEventListener,分别用来废弃请求和废弃监听器。这是一个

    前端面试一般喜欢问:

    在看来《使用 AbortController 终止 fetch 请求》,觉得写的非常详细,于是提炼下笔记:


    AbortController背景介绍

    在现在的浏览器中,有两种主要的方法发送请求:XMLHttpRequest 和 fetch。XMLHttpRequest 这个接口在浏览器中存在很长一段时间了,fetch 则是 ES2015 引入的特性。

    XMLHttpRequest 可以在请求中途终止(abortable)。举个例子

    let xhr = new XMLHttpRequest();
    xhr.method = 'GET';
    xhr.url = 'https://slowmo.glitch.me/5000';
    xhr.open(method, url, true);
    xhr.send();
    
    // Abort the request at a later stage
    abortButton.addEventListener('click', function() {
      xhr.abort();
    });

    fetch 刚开始引入时并不支持终止请求。Github 上最早 在 2015 年就有终止 fetch 请求的提案 issue 出现。在 fetch 规范之外也有许多解决这个问题的方案,像 cancelable-promises 和其他 hacks

    终于,通用的 AbortController 和 AbortSignal API 出来了。该 API 在 DOM 标准 中定义,而不是在语言规范中定义的。


    什么是 AbortController

    AbortController是一个DOM API。MDN上对它的介绍是 AbortController接口表示一个控制器对象,允许根据需要终止一个或多个Web请求。

    AbortController可以用在fetch和addEventListener,分别用来废弃请求和废弃监听器。

    具体看官网:

    https://developer.mozilla.org/zh-CN/docs/Web/API/AbortController/AbortController

    https://caniuse.com/?search=AbortController


    DOM 文档 中有这么一段话:

    虽然 Promise 没有提供内置的终止算法(aborting mechanism),但是许多使用它们的 API 需要终止语义。AbortController 提供一个 abort() 方法来支持这些需求,这个方法用来切换相应 AbortSignal 对象的状态。希望支持终止功能的 API 可以接受 AbortSignal 对象,并基于其状态来确定执行流程。


    AbortController由两部分构成:abort-signal和abort-controller,架构图如下:

    红色部分是我们重点需要关注的部分,因为它们将直接体现在实际应用中

    AbortSignal源码解读


    AbortSignal源码解析

    ...
    export default class AbortSignal extends EventTarget<Events, EventAttributes> {
        /**
         * 从abortedFlags中获取当前AbortSignal实例aborted状态
         */
        public get aborted(): boolean {
            const aborted = abortedFlags.get(this)
            if (typeof aborted !== "boolean") {
                throw new TypeError(
                    `Expected 'this' to be an 'AbortSignal' object, but got ${
                        this === null ? "null" : typeof this
                    }`,
                )
            }
            return aborted
        }
    }
    // 设置abort自定义事件
    defineEventAttribute(AbortSignal.prototype, "abort")
    
    ...
    
    /**
     * 创建一个AbortSinal实例,并设置aborted状态为false,存入abortedFlags中,同时绑定abort事件属性
     */
    export function createAbortSignal(): AbortSignal {
        const signal = Object.create(AbortSignal.prototype)
        EventTarget.call(signal)
        abortedFlags.set(signal, false)
        return signal
    }
    
    /**
     * 设置AbortSinal实例aborted状态为true,同时触发abort监听事件回调
     */
    export function abortSignal(signal: AbortSignal): void {
        if (abortedFlags.get(signal) !== false) {
            return
        }
    
        abortedFlags.set(signal, true)
        signal.dispatchEvent<"abort">({ type: "abort" })
    }
    ...
    1. AbortSignal继承EventTarget(第三方依赖包,作用是赋予实例监听自定义事件,它会帮你解决兼容性问题addEventListener/onXX/dispatchEvent),AbortSignal自定义了abort监听事件

    2. AbortSignal.aborted():获取当前实例是否已经启动了abort监听事件。

    3. abortedFlags:map类型,用于存储每个实例的是否已经启动了abort监听事件,默认为false(createAbortSignal创建实例的时候设置),调用abortSignal函数的时候会设置为true

    4. createAbortSignal():构建函数,初始化实例对象为false,绑定abort监听事件(需要用户自己设置abort监听回调事件)

    5. abortSignal(instance):设置当前实例状态为ture,同时触发abort监听回调事件


    AbortController源码解析

    ...
    export default class AbortController {
        /**
         * 构造函数,创建一个AbortSignal实例并存入signals中
         */
        public constructor() {
            signals.set(this, createAbortSignal())
        }
    
        /**
         * 从signals中获取当前AbortSignal实例
         */
        public get signal(): AbortSignal {
            return getSignal(this)
        }
    
        /**
         * 先从signals中获取当前AbortSignal实例,然后设置实例aborted状态为true,触发abort监听回调事件
         */
        public abort(): void {
            abortSignal(getSignal(this))
        }
    }
    ...
    1. AbortController构造函数中会调用createAbortSignal创建AbortSignal实例并存入一个Map类型的signals中。

    2. abort()方法会调用abortSignal函数,传入的参数就是从signals中取出来的AbortSignal实例

    3. signal()方法作用是从signals中取出AbortSignal实例

    参考源码:

    1. EventTarget源码传送门

    2. AbortController源码传送门

    3. fetch源码传送门


    AbortController addEventListener

    众所周知,如果需要 removeEventListenr(type, callback), 它的callback必须和addEventListener是同一个函数引用,而在某些业务场景下,我们并不想多写函数可以改成用signal来控制。

    例如,当在按钮鼠标时设置一个监听器,在监听器中再监听鼠标移动,鼠标松开关闭监听器:

      document.addEventListener('mousedown', callback);
      document.addEventListener('mouseup', callback2);
      function callback (e) {
          document.addEventListener('mousemove',  callback3);
       }
    
      function callback2 (e) {
         document.removeEventListener('mousemove', callback3);
      }
      
      function callback3(event) {}

    如果改写成用AbortController怎么写呢?

        const controller = new AbortController();
        function callback (e) {
          document.addEventListener('mousemove',  (e) => {
              
          },{
               signal: controller.signal  
          });
       }
        document.addEventListener('mousedown', callback);
        document.addEventListener('mouseup', controller.abort);


    高级进阶

    每次请求,都会重新创建一个AbortSignal实例吗?

    答:是的

    signals和abortedFlags都是Map类型,每一个请求都会创建一个实例,随着时间的推移和请求的增多,如何防止缓存雪崩问题?

    答:signals和abortedFlags准确的说是WeakMap类型,而WeakMap跟Map会有所区别,WeakMap的键只能是对象的引用,当垃圾回收机制执行时,会检测WeakMap的键是否被引用,若没有被引用,该键对会被删除,并自动回收,从而防止缓存雪崩的问题。

    AbortSignal是如何具备监听事件能力的?

    答:它本身并不具备事件处理能力,它继承了一个EventTarget类使其具备监听处理事件能力


      参考文章:

      一个可中断请求fetch的原理分析和应用 https://zhuanlan.zhihu.com/p/416572062

       [译] 使用 AbortController 终止 fetch 请求 https://juejin.cn/post/6844904072051425293

      一个可中断请求fetch的原理分析和应用(之前的笔记) https://github.com/ctq123/blogs/issues/9

      AbortController使用场景探索 https://www.jianshu.com/p/2f23c33e1922




      转载本站文章《中断操作:AbortController学习笔记》,
      请注明出处:https://www.zhoulujun.cn/html/webfront/SGML/html5/2022_0530_8824.html