[momo的源码之路]自顶而下解析axios源码(下篇)

1,241 阅读5分钟

前言:由于我觉得我水平太烂了,博主建议我每天读一些源代码。恰巧作为某校园项目负责人,在参考BuildAdmin二次封装Axios后,看到众多掘金作者对无效二次封装方案的深恶痛疾,我赶紧进行code review。为了对Axios有更深入的理解,于是乎我选择了Axios作为最近阅读的源码...

axios最精彩的地方之一(我认为的)是request请求链的相关操作,在上一部分中已经进行了相关解读。但是axios还有其他功能值得我们进行分析,本章将会对取消请求、并发请求进行分析。
在上面这一段话写完后我后悔了,取消请求的细节也是十分值得注意的!

取消请求

  其实在日常的使用中,我曾经遇到过使用取消请求的场景,那就是减少用户重试过多造成网络请求浪费。从axios源码我了解了他的原理,这里通过对官网的学习,我对其进行了测试。

image.png
  当没有使用AbortController进行请求时,请求正常进行,但是当使用abort方法后,返回请求被取消的信息。

image.png
  当使用另外一种取消请求的机制cancalToken时,能进行取消请求的同时,对取消时候信息也能自定义:

image.png image.png


  第一种取消请求的方式AbortController是为了终止promise提供的一个对象,对比new AbortController.abort()触发前后,其中的aborted发出了改变,让函数起到取消请求作用。具体的AbortController可以查看[Mdn文档]。(AbortController.AbortController())
在abort()执行前: image.png
执行后: image.png

  第二种方式是axios内部提供的一系列操作,且他在一开始的axios的产生和暴露的时候已经被挂载,实例没有相关的方法。还是老规矩,从自顶而下从使用开始看,有这么几个步骤:

  1. 使用工厂方法CancelToken创建每一次的实例source,如图所示,每一次创建的都是新的对象
  2. config传输cancelToken的属性,其中值为source.token
  3. 在对应的source使用cancel方法,下图也会展示执行前后的source的内部结构的不同

两个对象是不同的:
image.png
  可以看到执行前的sorce的相关属性,其中重要的属性cancel是函数,传入终止信息,而token含有promise属性,值为pending状态的promise对象,如下所示:

image.png


  重中之重!!!在前面提到取消请求的时候,有几个节点是判定用户是否取消请求的,而判定请求则是使用下面的这一段代码,借用思唯大哥的一幅图帮大家明确其中的几点,不管是第一种还是第二种取消请求方式,他都适用。

image.png
(上述代码出现的节点都是下面流程图中需要注意的节点) image.png

  第一种取消请求的方式源代码在实际的请求中出现,可以看到适配器封装底层请求的/lib/adapters/xhr.js中有:

image.png

  来看看第二种的源码吧!在lib/cancel/CancelToken.js中,找到其定义:

image.png
跟随代码的步骤,俺们看看创建CancelToken究竟做了啥,对于Cancel功能的代码,并没有外接其他函数:

/**
 * 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.');
  }

  // 调用的时候传入错误信息
  var resolvePromise;

  // 创建当前promise对象,并将改变状态为Fulfiled的方法暴露
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  // 拷贝一下this
  var token = this;

  // 当对象的状态改变的时候,检查当前this是否含有_listeners
  // 此时cancel是
  // eslint-disable-next-line func-names
  this.promise.then(function(cancel) {
    if (!token._listeners) return;

    var i;
    var l = token._listeners.length;

    // 如果含有_listeners属性,不打断执行其中的函数并传入此时的cancel信息
    for (i = 0; i < l; i++) {
      token._listeners[i](cancel);
    }
    // 重置this的这个属性
    token._listeners = null;
  });

  
  // 相当于重写一个then方法,返回
  // eslint-disable-next-line func-names
  this.promise.then = function(onfulfilled) {
    var _resolve;
    // eslint-disable-next-line func-names
    var promise = new Promise(function(resolve) {
      // 注册上文提到的_listeners
      token.subscribe(resolve);
      _resolve = resolve;
    }).then(onfulfilled);

    promise.cancel = function reject() {
      // 取消对这个的监听
      token.unsubscribe(_resolve);
    };

    // 重写then返回的promise对象
    return promise;
  };

  executor(function cancel(message) {
    // 如果已经被取消,则返回
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }

    // 设置取消原因,Cancel只是格式化取消信息
    token.reason = new Cancel(message);
    // 把本函数属性promise编程fulfilled
    resolvePromise(token.reason);
  });
}

/**
 * 如果传递了原因,那么抛出原因
 * Throws a `Cancel` if cancellation has been requested.
 */
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    throw this.reason;
  }
};

/**
 * 
 * Subscribe to the cancel signal
 */

CancelToken.prototype.subscribe = function subscribe(listener) {
  // 判定如果被取消
  if (this.reason) {
    // listener其实是then返回的promised resolve函数
    listener(this.reason);
    return;
  }

  // 构建相关数组
  if (this._listeners) {
    this._listeners.push(listener);
  } else {
    this._listeners = [listener];
  }
};

/**
 * Unsubscribe from the cancel signal
 */

CancelToken.prototype.unsubscribe = function unsubscribe(listener) {
  if (!this._listeners) {
    return;
  }
  var index = this._listeners.indexOf(listener);
  if (index !== -1) {
    this._listeners.splice(index, 1);
  }
};


  可能你看到这么长的代码会头晕,没事,我一下子也头晕!下面是一些图,希望能帮助大家进行理解。首先source工厂不是单例每次生产的都不是同一个对象,而对象含有cancel方法和tokenCancelToken对象。

image.png
  单单这样子不能明白其中的运行机制,我们再看!如果concel代码执行了会怎样呢? 我将这一文件的代码提出来进行执行,发现他会进入此处的标记然后就终止了,那么对下面的listeners数组的操作又是怎么回事?
  和第一次请求一样,也会对其进行监听和记录: image.png 同时也有释放函数done对其进行清空回收:

image.png

并发请求

  axios提供了allspread方法供给并发请求的执行,也许你们没有对其进行过使用,没问题,可以看下面的案例:

image.png
  从源代码中看看他们的实现:

image.png image.png

  终述:axios果然是基于promsise的,他对于proise的应用玩的十分生动,不仅如此,多处体现的硬绑定也让我的基础得到了加强。我会在掘金等平台一直分享我的见解,希望能在评论区多交流学习!
  我是momo,我们下期见!

image.png 开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 6 天,点击查看活动详情