• JS axios cancelToken 是如何实现取消请求?稍有啰嗦但超有耐心的 axios 源码分析


    壹 ❀ 引

    axios,一个基于promise且对ajax进行了二次封装的http库,在提供了与promise类似的API便捷写法同时,它还有一大特点,便是支持取消http请求。当然取消请求并不是axios独有特性,它也只是对于XMLHttpRequest.abort()进行了内部封装。

    我在如何做好一个基础的搜索功能?记一个因客户大数据量而导致的后发先至Bug一文中,对于文章问题给出的解决方案就有涉及到取消请求。虽然原生做法也能达到目的,但实际开发中我们不会用那么麻烦的写法,自然会借助三方http库,比如axios,而在这个库中,我们若需要取消请求,就得使用它的cancelToken,那么这个cancelToken到底是怎么做的呢?为什么一个简单的source.cancel调用请求就不会发起了?出于好奇,我简单阅读了axios对于cancelToken实现的相关源码,那么在这篇文章做个简单记录。

    贰 ❀ 一个取消请求的例子

    了解事情原貌最简单的做法就是写一个请求取消的例子,然后断点到axios中去阅读源码,因此我在本地项目准备了一个这样的例子,而本地项目请求接口容易跨域,为了解决这个问题,才有了React axios 使用 http-proxy-middleware 解决跨域问题小记这篇文章。所以这里我们就不多赘述,直接结合axios实现请求取消:

    // 引用CancelToken
    const CancelToken = axios.CancelToken;
    // 调用CancelToken.source得到一个source实例,此实例包含token和cancel两个属性
    const source = CancelToken.source();
    // 请求接口时附带cancelToken:source.token,get与post有所区别,具体查看官方文档
    axios.get('api/request', { cancelToken: source.token })
      .catch(function (thrown) {
        if (axios.isCancel(thrown)) {
          alert(`Request canceled.${thrown.message}`);
        }
      });
    // 通过source.cancel取消请求
    source.cancel('Operation canceled by the user.');
    

    上述代码中我们实现了一个简单接口取消请求,运行项目并打开控制台,发现并没发起request请求,且alert正常弹出。

    而实现取消接口请求也比较简单,通过CancelToken.source()创建一个resoure实例:

    // 单纯创建一个实例,不用于请求,让我们查看它
    const source = CancelToken.source();
    console.dir(source)
    

    如图,此实例包含一个名为cancel的方法,接受一个message字段,也就你要取消请求时需要传递的理由,当然也可以不传。另外是一个token对象,它包含一个状态为pendingPromise对象(这个东西作用超级大,下文会解释这个Promise从哪来有什么用)。

    取消请求的目的虽然达到了,可这几个方法像个黑盒,它里面到底发生了什么?没关系,让我们顺着上面请求取消的例子来一探究竟!

    叁 ❀ cancelToken源码浅析

    取消接口第一步创建resouce实例,它的源码在node_modules/axios/lib/cancel/CancelToken.js中可查看,代码如下:

    /**
     * Returns an object that contains a new `CancelToken` and a function that, when called,
     * cancels the `CancelToken`.
     */
    CancelToken.source = function source() {
      var cancel;
      var token = new CancelToken(function executor(c) {
        cancel = c;
      });
      return {
        token: token,
        cancel: cancel
      };
    };
    

    当我们执行CancelToken.source()触发的就是上述代码,首先,我们可以看到此方法确实返回一个包含canceltoken两个属性的对象,让我们来看看执行过程。

    首先,此方法创建了一个calcel变量,紧接着,又创建了一个token,而token的赋值结果是调用构造函数CancelToken得到,注意,此时我们在调用构造函数时,传递了一个function如下:

    function executor(c) {
        cancel = c;
    }
    

    你可以先不用思考它有什么用,就把这个函数理解成调用构造函数传递了一个实参,紧接着返回了一个包含上述两个属性的对象,对于source方法,它做的事情就这么简单,让我们紧接着看看构造函数CancelToken,代码如下:

    /**
     * A `CancelToken` is an object that can be used to request cancellation of an operation.
     *
     * @class
     * @param {Function} executor The executor function.
     */
    function CancelToken(executor) {
      if (typeof executor !== 'function') {
        throw new TypeError('executor must be a function.');
      }
    	// 其实就外层保存resolve方法
      var resolvePromise;
      this.promise = new Promise(function promiseExecutor(resolve) {
        resolvePromise = resolve;
      });
    
      var token = this;
      executor(function cancel(message) {
        if (token.reason) {
          // Cancellation has already been requested
          return;
        }
    
        token.reason = new Cancel(message);
        // 在这里调用resolve方法,用于改变Promise状态
        resolvePromise(token.reason);
      });
    }
    

    OK,对于构造函数CancelToken,它接受一个参数executor,而这个参数其实就是上面提到的executor方法。接着,函数内声明了一个名为resolvePromise的变量。然后就是构造器属性this.promise,既然是构造器属性,那么这里可以预先知道,source方法中得到的token上一定有这个promise属性(上面截图已经展示了token中有个Promise),而这个属性又由一个Promise构造器创建。

    new Promise方法做的事情很简单,在内部将resolve赋值给外部变量resolvePromise,我们都知道Promise接受一个callback,而callback内部可以使用resolvereject两个方法,而上述所做的事仅仅是将Promise内部的resolve方法暴露出去,以达到在外层改变Promise状态的目的,比如:

    // 外层定义一个变量用于保存promise内部的方法
    let resolvePromise;
    const p = new Promise((resolve, reject) => {
      resolvePromise = resolve;
    })
    p.then((res) => {
      console.log(res);// 我在外层改变promise状态
    });
    setTimeout(() => {
      // 外层调用promise的resolve方法以达到改变promise状态目的
      resolvePromise('我在外层改变promise状态')
    }, 3000)
    

    所以简单来说,通过这种做法我们将promise内部方法暴露出来,想在哪用就在哪用,大概如此。

    让我们继续回到CancelToken方法,接着我们将this赋予给变量token,不要诧异,我们知道当new一个构造函数时,其实就是在隐式的给this赋值,然后返回这个this,而为了让这种绑定与返回更为可视化,常常有如下的写法:

    function Fn(name){
      const that = this;
      that.name = name;
      return that;
    }
    

    所以上面算是一种可视化做法,毕竟new CancelToken本身就是返回一个token并赋值给token,也相当于更好理解。

    接着,我们执行了executor方法,它的参数又是一个函数,如下:

    function cancel(message) {
      if (token.reason) {
        // Cancellation has already been requested
        return;
      }
      token.reason = new Cancel(message);
      // 在这里调用resolve方法,用于改变Promise状态
      resolvePromise(token.reason);
    }
    

    同学们,还记得这个executor从哪来吗?它不就是new CancelToken时传递的函数吗:

    function executor(c) {
        cancel = c;
    }
    

    所以这个c其实就是上面传递给executorcancel方法,而cancel = c这一句,目的跟上面resolve赋值给外层一样,也是将定义在CancelToken内部的cancel暴露出去。

    你可能有点混乱了,其实这里一共做了三次方法暴露,第一次我们将resolve暴露出去,目的是让cancel中可以通过resolvePromise来改变Promise状态;而这个cancel也不是在定义的地方使用,这里又做了第二次暴露,将cancel抛出去赋予给了source方法,紧接着source做了第三次方法暴露,当我们开发者调用source将得到canceltoken两个属性。大致流程如下图:

    那么到这里我们解释了source方法是如何产生的这两个属性,以及这两个属性分别有什么用。

    所以当我们调用接口时,传递了一个source.token其实就是给这次请求绑定一个状态是pendingPromise,它更像是一个开发,一旦我们调用source.cancel就会启动这个开发,告知axios要取消这个请求,怎么取消的呢?

    让我们找到node_modules/axios/lib/adapters/xhr.js这个文件,文章开头就说了axios是对于ajax的封装,而ajax又是对于XMLHttpRequest那一套的封装,所以回归本质,在xhr.js中我们可以看到发起请求所有的准备代码都在这里,这里大致贴一点代码并补了注释,我删掉了部分对于本身意义不大的代码:

    return new Promise(function dispatchXhrRequest(resolve, reject) {
      // 准备请求附带数据
      var requestData = config.data;
      // 准备请求头
      var requestHeaders = config.headers;
      // 创建xhr对象
      var request = new XMLHttpRequest();
    
      // 授权相关
      if (config.auth) {
        var username = config.auth.username || '';
        var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
        requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
      }
      // 获取请求地址
      var fullPath = buildFullPath(config.baseURL, config.url);
      // 调用open,发起请求
      request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
    
      // 设置超时时间,你期望多久没反应就提示超时那就设置多少
      request.timeout = config.timeout;
    
      // OK,监听state状态,
      request.onreadystatechange = function handleLoad() {
        if (!request || request.readyState !== 4) {
          return;
        }
        // 准备response数据体
        var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;
        var response = {
          data: responseData,
          status: request.status,
          statusText: request.statusText,
          headers: responseHeaders,
          config: config,
          request: request
        };
    
        settle(resolve, reject, response);
        // 一旦请求结束,清空请求
        request = null;
      };
    
      // 监听abort的处理
      request.onabort = function handleAbort() {
      };
    
      // 监听error的处理
      request.onerror = function handleError() {
      };
    
      // 监听超时后的处理
      request.ontimeout = function handleTimeout() {
      };
    
      // 这里就是看我们有没有给请求附带cancelToken,如果带了就会走这里
      if (config.cancelToken) {
        // 还记得这里的promise知道是哪创建的吗?
        config.cancelToken.promise.then(function onCanceled(cancel) {
          // 两种情况,要么此时没请求,要么请求结束被清空为null,这里就不做处理,直接返回
          if (!request) {
            return;
          }
          // 这里调用了XMLHttpRequest.abort()
          request.abort();
          // 这里修改的是请求体自身promise的状态,而不是我们上文提到的那个promise
          reject(cancel);
          // 清空request,请求对象都不要了
          request = null;
        });
      }
    });
    

    大家可以大致看看上述代码,这里我们抽离出最重要的一段:

    // 这里就是看我们有没有给请求附带cancelToken,如果带了就会走这里
    if (config.cancelToken) {
      // 还记得这里的promise知道是哪创建的吗?
      config.cancelToken.promise.then(function onCanceled(cancel) {
        // 两种情况,要么此时没请求,要么请求结束被清空为null,这里就不做处理,直接返回
        if (!request) {
          return;
        }
        // 这里调用了XMLHttpRequest.abort()
        request.abort();
        // 这里修改的是请求体自身promise的状态,而不是我们上文提到的那个promise
        reject(cancel);
        // 清空request,请求对象都不要了
        request = null;
      });
    }
    

    config不用多说,其实就是我们使用axios发起请求时的配置,上文的例子也展示了,你要想取消请求,你就得给请求配置中加一个{ cancelToken: source.token },而这个token中其实就是一个状态为pendingPromise,目的是什么我们也很清楚了。这个Promise就为了是跟当前请求进行绑定,它就像一颗遥控炸弹,而炸弹的开关就是 source.cancel,当我们调用cancel时,就会执行下面这个方法:

    function cancel(message) {
      if (token.reason) {
        // Cancellation has already been requested
        return;
      }
      token.reason = new Cancel(message);
      // 启动炸弹
      resolvePromise(token.reason);
    }
    

    方法里就干一件事,将跟请求绑定在一起的Promise的状态给改成resolve,一旦修改完成,那是不是就得跑下面这段代码:

    config.cancelToken.promise.then(function onCanceled(cancel) {
      // 两种情况,要么此时没请求,要么请求结束被清空为null,这里就不做处理,直接返回
      if (!request) {
        return;
      }
      // 这里调用了XMLHttpRequest.abort()
      request.abort();
      // 这里修改的是请求体自身promise的状态,而不是我们上文提到的那个promise
      reject(cancel);
      // 清空request,请求对象都不要了
      request = null;
    });
    

    毕竟你Promise状态变了,那我then就得执行,执行了干什么?request.abort()也就是XMLHttpRequest.abort()取消请求,成功把这个请求给引爆炸掉了!!!

    abort干了什么呢?很遗憾,这个就彻彻底底是个黑盒了,是浏览器在帮我们处理,断点跟不进去,但通过MDN我们可以的值,当调用此方法时,如果请求已发出,那么该方法将中止请求。当一个请求被中止时,它 readyState被更改为 XMLHttpRequest.UNSENT(0) 并且请求的 status代码被设置为 0。

    肆 ❀ 总

    那么到这里,我们完整跟着请求代码把axios执行过程给讲了一遍,你会发现axios在内部其实就是创建一个名为token 的 Promise与一个改变Promise状态的方法cancel,当你需要取消一个请求时,那就把这个Promise与请求绑在一起,然后便可通过cancel改变请求上的Promise状态,从而达到取消请求的目的,不得不说这个三方库的作者那是真的牛妈妈给牛宝宝开门,牛到家了!!

    另外,abort原生做法我测试发现若请求发出,是会取消请求,所以可以看到chrome控制台有红色的被取消的请求,而axios直接没请求,猜测应该是axios内部做了特殊处理,或者是我请求被取消的太快,导致根本没发出,至于这点我就没再去研究了。

    var xhr = new XMLHttpRequest(),
        method = "GET",
        url = "https://developer.mozilla.org/";
    xhr.open(method, url, true);
    
    xhr.send();
    
    if (true) {
      xhr.abort();
    }
    

    结合前面文章,我们就可以在简历中写通过axios.cancelToken解决XXX问题,若面试问你这个取消是怎么实现的。那真是不好意思,你算是撞到咱们的枪口上了,又可以愉快的跟面试官battle一番。OK,那么到这里,本文正式结束!!2点了!!睡觉!!

  • 相关阅读:
    Linux(CentOS 7)iptables防火墙
    Linux(CentOS 7)DNS服务器搭建
    Linux(CentOS 7)LNMP环境搭建
    Linux(CentOS 7)nginx网站服务器
    Linux(CentOS 7)LAMP环境的搭建
    JavaScript 中repalce的使用
    DOM&BOM
    CSS颜色
    CSS中的单位
    Grid Layout 注释
  • 原文地址:https://www.cnblogs.com/echolun/p/15204279.html
Copyright © 2020-2023  润新知