这次终于弄懂Axios是如何中断请求了

Axios 文档案例

先看下 Axios 文档给的例子 https://github.com/axios/axios#cancellation

  1. 通过 CancelToken.source 工厂函数进行取消
const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function (thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // handle error
  }
});

axios.post('/user/12345', {
  name: 'new name'
}, {
  cancelToken: source.token
})

// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');

  1. 通过 CancelToken 构造函数进行取消
const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // An executor function receives a cancel function as a parameter
    cancel = c;
  })
});

// cancel the request
cancel();

  1. 通过 AbortController 中断请求,这是 fetch 的 api,本文就不再详细介绍了,具体使用可以参考 https://developer.mozilla.org/zh-CN/docs/Web/API/AbortController/AbortController
const controller = new AbortController();

axios.get('/foo/bar', {
   signal: controller.signal
}).then(function(response) {
   //...
});
// cancel the request
controller.abort()

源码分析

首先需要从 GitHub 下载 Axios 源码。如果不想下载,也可以打开 https://github1s.com/axios/axios/ 进行查看。

工厂函数 CancelToken.source

通过前面两个例子,可以知道取消请求和 CancelToken 这个类息息相关,CancelToken.source()工厂函数只不过是在我们看不见的内部帮助我们去实例化一个 CancelToken 的实例出来。

那我们先来看下工厂函数的实现。

// 文件路径 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
  };
};

module.exports = CancelToken;

可以看到 CancelToken.source 这个工厂函数就是帮助我们实例化了一个 CancelToken 的实例,然后返回给我们需要使用的 实例(token)取消请求的函数(cancel)

接下来我们继续深入 CancelToken 内部,看看为什么执行了 cancel 函数后,请求就中断了。

CancelToken 类

// 文件路径 Axios/lib/cancel/CancelToken.js

// ...

/**
 * 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;

  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;

  // eslint-disable-next-line func-names
  this.promise.then(function(cancel) {
    if (!token._listeners) return;

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

    for (i = 0; i < l; i++) {
      token._listeners[i](cancel);
    }
    token._listeners = null;
  });

  // eslint-disable-next-line func-names
  this.promise.then = function(onfulfilled) { // ...
  };

  executor(function cancel(message) {
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }

    token.reason = new Cancel(message);
    resolvePromise(token.reason);
  });
}

// ...

通过文档中的例子可以看出,在发起请求的时候传入了 cancelToken,也就是 CancelToken 的一个实例。

实例化的过程中会调用我们传入的 executor函数,将 cancel 函数传递给我们外部。

另外这个实例上有一个 promise 的属性,当我们调用 cancel 函数,promise 则会从 pending 的状态变成 fulfilled。从而触发 promise.then,执行所有的 token._listeners

token._listeners 又从何而来?

答案还是在当前的文件中

// 文件路径 Axios/lib/cancel/CancelToken.js

// ...

/**
 * Subscribe to the cancel signal
 */

CancelToken.prototype.subscribe = function subscribe(listener) {
  // reason 值不为 undefined 说明该请求已取消,可直接调用 listener
  if (this.reason) {
    listener(this.reason);
    return;
  }

  if (this._listeners) {
    this._listeners.push(listener);
  } else {
    this._listeners = [listener];
  }
};

// ...

CancelToken 的原型对象上添加了 subscribe 方法,用于订阅取消请求的事件。如果该请求已被取消,则会立即调用 listener,否则会将 listener 保存在 _listeners 数组中。

当我们调用 cancel 也就是取消请求的时候,_listeners 中保存的 listener 则会被调用(见上文)。

这时候并没有看到中断请求的操作,具体的逻辑是在 listener 内部,这样写的原因就是可以进行解耦,提高代码的复用性。

另外还有一个 unsubscribe 取消订阅就不再展开了。

这就是典型的订阅发布模式。

取消请求

最快速的方法就是搜索 config.cancelToken.subscribe,这样就可以快速定位到取消请求的具体实现。

只搜索 lib 文件夹即可,可以看到有两处地方,一个是 lib/adapters/http.js,另一个是 lib/adapters/xhr.js

因为 Axios 是一个支持 node.js 和浏览器的 http 客户端。这里应用了适配器的模式来兼容这两个平台。本文研究的是取消请求,就不去深究这部分了,我们看其中之一就好了。

// Axios/lib/adapters/xhr.js

// ...
    if (config.cancelToken || config.signal) {
      // Handle cancellation
      // eslint-disable-next-line func-names
      onCanceled = function(cancel) {
        if (!request) {
          return;
        }
        reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel);
        request.abort();
        request = null;
      };

      config.cancelToken && config.cancelToken.subscribe(onCanceled);
      if (config.signal) {
        config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
      }
    }
    
// ...

从 187 行这里开始,我们可以看到 config.cancelToken.subscribe(onCanceled)cancelToken 注册了一个中断请求的回调。request.abort(); 这里的 requestXMLHttpRequest 的一个实例。

另外还有一个函数 done,即请求成功或者失败之后会将上面注册的 onCanceled 进行取消注册。

至此整个取消请求的逻辑就跑通了。我简单画了个图(画了几个小时),希望能方便大家理解。

image

结合 Vue 实现离开页面取消未完成的请求

思路就是利用一个对象来管理所有的 CancelToken 实例。发起请求之前,把新创建的 cancelToken 保存到对象中,请求结束后(包括成功、失败)把对应的实例清除。

再结合 vue-router 的路由守卫,这样就可以在离开页面的时候取消所有未完成的请求。

有些全局的接口需要做特殊处理,比如请求用户信息之类的,这些全局通用的接口就不能再离开页面的时候中断请求。

具体代码这里就不展示了。我写了一个 demo,有需要的小伙伴可以自行查看。

https://github.com/AD-feiben/demos/tree/main/abort-req

最后再重申一点,学习源码是为了学习源码中优秀的设计,我们需要思考如何将这个设计应用到我们的项目中,这才是最重要的一点。

希望文章的内容能为你提供一丝丝帮助,如果错误,还望指正。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容