Axios 文件格式事例
先看下 Axios 文件格式给的范例 https://github.com/axios/axios#cancellation
透过 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.);
透过 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();
透过 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(); 这里的 request 是 XMLHttpRequest 的一个实例。
另外还有一个表达式 done,即请求成功或者失败之后会将上面注册的 onCanceled 展开中止注册。
至此整个中止请求的逻辑就跑通了。我简单画了个图(画了几个小时 ),希望能方便大家理解。
结合 Vue 实现离开页面中止未完成的请求
思路就是利用一个对象来管理所有的 CancelToken 实例。发起请求之前,把新创建的 cancelToken 保存到对象中,请求结束后(包括成功、失败)把对应的实例清除。
再结合 vue-router 的路由守卫,这样就可以在离开页面的时候中止所有未完成的请求。
有些全局的接口需要做特殊处理,比如请求用户信息之类的,这些全局通用的接口就不能再离开页面的时候内存模块。
具体代码这里就不展示了。我写了一个 demo,有需要的小伙伴可以自行查看。
https://github.com/AD-feiben/demos/tree/main/abort-req
最后再重申一点,学习源码是为了学习源码中优秀的设计,我们需要思考怎样将这个设计应用到我们的项目中,这才是最重要的一点。
希望文章的内容能为你提供一丝丝帮助,如果错误,还望指正。