Axios源码阅读(三):取消请求

一、功能介绍

官方文档指出有2种方法可以取消请求,分别是cancelTokenabortController,下面是示例代码:

// method 1
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.post('/user/12345', {
  name: 'new name'
}, {
  cancelToken: source.token
});

source.cancel('Operation canceled by the user.');

// method 2
const CancelToken = axios.CancelToken;
let cancel;
axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    cancel = c;
  })
});

cancel();

// method 3
const controller = new AbortController();
axios.get('/foo/bar', {
   signal: controller.signal
}).then(function(response) {
   //...
});

controller.abort();

通过文档描述和示例代码总结出以下功能点:

  1. 支持cancelToken取消请求,cancelToken可以通过工厂函数产生,也可以通过构造函数生成;
  2. 支持Fetch APIAbortController取消请求;
  3. 一个token/signal可以取消多个请求,一个请求也可同时使用token/signal
  4. 如果在开始axios request之前执行了取消请求,则并不会发出真实的请求(见Cancellation最后一个Note);

二、源码阅读

通过阅读源码逐个了解上述功能是如何实现的。

2.1 cancelToken 取消请求

通过搜索找到axios.CancelToken = require('./cancel/CancelToken'),于是打开该文件,找到CancelToken构造函数后,发现这个函数非常的绕,这里把代码结构稍微整理下:

传送门:./lib/cancel/CancelToken.js

var Cancel = require('./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 token = this; // 把 this 保存在局部变量中

  // snippet 1
  // 给实例添加“promise”属性
  
  // snippet 2 
  // this.promise resolved 后,call所有的观察者,并且清空观察者队列
  
  // snippet 3
  // 重写 this.promise.then 方法,在 then 里面添加了订阅的代码
  
  // snippet 4
  // 执行构造函数传入的 executor,参数为一个改变 this.promise 状态的回调函数
}

上面的代码是CancelToken构造函数,这个函数实现的非常巧妙,我们逐一分析。

snippet 1

var resolvePromise;
// 给实例添加 promise 属性,并且把 resolve 保存在局部变量中
this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
});

这里把resolve保存在局部变量中为一个关键步骤,相当于保存了一把“钥匙”,以后可以随时用这把钥匙改变this.promise的状态继而引发观察者的action

snippet 2

this.promise.then(function(cancel) {
    // 通过上下文分析,这里传入的 cancel 为取消请求的原因对象,例如:{message: 'reason'}
    if (!token._listeners) return;

    var i;
    var l = token._listeners.length;
    // 将观察者队列清空
    for (i = 0; i < l; i++) {
      token._listeners[i](cancel);
    }
    token._listeners = null;
});

这里代码的功能是在请求被取消之后,执行所有之前所有订阅“取消”状态的方法,这里的代码也很关键,因为在xhr.js中需要根据该状态去执行xhr.abort,代码如下:

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;
    };
    // 这里 onCanceled 订阅了取消事件
    config.cancelToken && config.cancelToken.subscribe(onCanceled);
    if (config.signal) {
        config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
    }
}

上面的代码在./lib/adapters/xhr.js中,cancelToken.subscribe方法会把onCanceled放到cancelToken实例的_listeners中。这里使用了观察者模式,使用户可以很方便的添加取消事件的回调。

snippet 3

this.promise.then = function(onfulfilled) {
    var _resolve;
    // eslint-disable-next-line func-names
    var promise = new Promise(function(resolve) {
        token.subscribe(resolve);
        _resolve = resolve;
    }).then(onfulfilled);

    promise.cancel = function reject() {
        token.unsubscribe(_resolve);
    };

    return promise;
};

这里的代码重写了token.promise.then方法,当调用者后续调用then方法时,添加的方法可以直接添加到实例的_listeners数组中,这个和直接调用subscribe的区别是,这样订阅的方法是异步执行的。

snippet 4

// 执行构造函数传入的 function,并且传入一个 cancel 方法
executor(function cancel(message) {
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }

    token.reason = new Cancel(message);
    // 改变 token.promise 的状态
    resolvePromise(token.reason);
});

在构造函数CancelToken执行时就执行传入的function,我认为这样做主要是为了在构造函数内对外暴露一个接口,可以通过这个方法访问构造函数内部变量。也是因为这些代码,所以支持config.cancelToken = new CancelToken

CancelToken.source

示例代码中提到使用source.cancel取消请求,这里看下这个功能是如何实现的:

/**
 * 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返回一个对象,cancel就是在CancelToken构造函数内executor执行时传入的参数。

2.2 AbortController 取消请求

在这之前,我对AbortController没什么了解,所以先看下MDN-AbortController。看完后,我将其简单理解为abort事件控制器,因为其只支持abort事件,我们通过观察实例上的signal状态便能知道请求是否被取消。我们找到相关代码如下:

传送门:./lib/adapters/xhr.js

onCanceled = function(cancel) {
    if (!request) {
        return;
    }
    reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel);
    request.abort();
    request = null;
 };
if (config.signal) {
    config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
}

xhr.send前监听了signal的状态,我们调用controller.abort后,执行onCanceled取消xhr请求。

2.3 一个请求订阅多个事件

因为CancelToken是基于已撤销的提案,所以Axiosv0.22.2使用AbortController,为了兼容以前的代码,现在库中仍然保留了相关代码,在原来使用CancelToken的地方使用了||确保两者都起作用。

2.4 多个请求观察一个对象

能实现这个功能是因为代码采取了良好的设计。

基于 CancelToken

每个token实例都有一个_listeners数组,当每个请求的adapter执行的时候都会往_listeners压入一个观察回调,当token.promise凝固后就会执行所有的观察回调。

基于 AbortController

使用AbortController就更简单了,这个相当于是原生实现观察者模式。

三、总结

个人感觉cancellation这部分代码是最绕的,又因为各种异步代码,所以有些地方光凭看很难知道到底是怎么执行的,所以我们不妨运行Axios提供的测试代码,然后再浏览器中本地调试,通过打断点查看执行请情况。

通过这次源码阅读,我了解了:

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

推荐阅读更多精彩内容