本篇会藉由设计「取消重复请求机制」来解析 axios 的原始码,篇幅较长请耐心阅读。
其实要实践取消请求的功能并不会很难,官方也有一目了然的 教学,不过我自己在实作後一直对於 cancelToken
的原理耿耿於怀,就去研究了一下原始码,所以在实际撰写之前,想先分享一下我的理解。
接下来我们会直接看打包过的档案: axios/dist/axios.js
,所有 axios 的程序码都在这。你可以一边看 github 一边看文章。
cancelToken
可以为我们取消多余或不必要的 http请求
,虽然在一般情况下可能感觉不到有取消请求的必要,不过在一些特殊情况中没有好好处理的话,可能会导致一些问题发生。像是...
Pending
时间较久的 API
若短时间内重复请求,会有旧盖新的情况。post
请求,有可能导致多次的资料操作,例如表单发送两次。先从最主要的 Axios类别
看起,每一个 axios 应用都会创建一个 Axios类别
,而当中最核心的就是 request
方法,不过我们先暂时跳过。
後面两段则是在类别上又新增了好几个方法,让我们可以发起不同的http请求: axios.get()
、axios.post()
。
不过仔细一看会发现,最终我们呼叫的还是 request
,所以才会说 request
是 axios 的核心。
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
Axios.prototype.request = function request(config) {
// ...先跳过
};
// 帮不同的请求方法创建别名,最终都是呼叫request
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
Axios.prototype[method] = function(url, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url
}));
};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
Axios.prototype[method] = function(url, data, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url,
data: data
}));
};
});
在前面我们有看到,Axios类别
中有个 interceptors
属性,其值为物件,并且有 request
和 response
的属性。
这两个属性都是 InterceptorManager类别
,而这个类别是用来管理拦截器的,我之也写过 一篇 在介绍拦截器是什麽,不晓得的人可以去看一下。
而今天我们就是要用Axios的拦截器来达到取消重复请求的功能,所以来看看 InterceptorManager
吧。
function InterceptorManager() {
// 储存拦截器的方法,未来阵列里会放入物件,每个物件会有两个属性分别对应成功和失败後的函式
this.handlers = [];
}
// 在拦截器里新增一组函式,我们在上一篇有用过
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
return this.handlers.length - 1;
};
// 注销拦截器里的某一组函式
InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null;
}
};
// 原码的写法我觉得很容易看不懂,所以我改写了一下
// 简单来说就是拿handlers跑回圈,把里面的物件当作参数来给fn执行
InterceptorManager.prototype.forEach = function(fn) {
this.handlers.forEach(obj => {
fn(h);
});
};
基本上这个类别还蛮单纯的,主要就是三个操作 handlers
的方法,我们之前就是透过 axios.interceptors.request.use
和 axios.interceptors.response.use
来添加拦截器的。
但现在我们要再更深入了解Axios是怎麽在请求前後透过拦截器处理 request
和 response
的,这时候就要回去看 Axios.prototype.request
了。
可以发现,每当我们发送请求 Axios.prototype.request
会宣告一个阵列以及一个Promise物件。
并且利用 InterceptorManager.prototype.forEach
把我们拦截器中新增的函式一一放进 chain
中。
至於 dispatchRequest
就是Axios主要发送 XMLHttpRequest
的函式,我们等等会提到。
当所有函式都放进 chain
後再两两一组拿出来作为 promise.then()
的参数,而且利用Promise的链式呼叫来串接。
最後我们的请求就可以依照 request拦截器 -> dispatchRequest -> response拦截器
的顺序进行处理。
Axios.prototype.request = function request(config) {
//..省略
var chain = [dispatchRequest, undefined];
// 定义一个状态是resolve的Promise; config是发出请求时带的设定
var promise = Promise.resolve(config);
// InterceptorManager.prototype.forEach,把request拦截器的每一组函式「往前」加进chain里
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
// InterceptorManager.prototype.forEach,把response拦截器的每一组函式「往後」加进chain里
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
// 全部加进去後,chain会长的像是这样: [
// request.handlers[0].fulfilled, request.handlers[0].rejected, ...,
// dispatchRequest, undefined,
// response.handlers[0].fulfilled, response.handlers[0].rejected, ...,
// ]
// 只要chain里还有项目,就继续执行
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
};
最後把所有的函数串接起来後,promise
会像是下面这样,并且 Axios.prototype.request
会把这个 promise
返回出来,所以我们才可以在呼叫 axios.get()
之後直接用 then()
。
Promise.resolve(config)
.then(requestFulfilled, requestRejected)
.then(dispatchRequest, undefined)
.then(responseFulfilled, responseRejected)
Promise
已经是 resolve
状态,所以请求拦截器会拿到 config
来做前置处理。config
,所以 dispatchRequest
才能拿到 config
来发送请求。dispatchRequest
在完成 XMLHttpRequest
後会返回请求的 response
给回应拦截器。response
,所以你最後才可以拿到API资料。现在知道了拦截器是如何串接的了,那 dispatchRequest
是如何发送http请求的呢?
我们只看重点部分,当中 adapter
会根据发送请求的环境对应到不同的适配器(建立请求的函式),而 dispatchRequest
会再以 then()
串接,由http请求的成功或失败来决定要进入回应拦截器的 fulfilled
函式或 rejected
函式。
module.exports = function dispatchRequest(config) {
// 检查请求是否被取消的函式
throwIfCancellationRequested(config);
// axios会使用预设的http请求适配器,除非你有特别设定
// 以浏览器发送请求会使用xhrAdapter,node环境则使用httpAdapter
var adapter = config.adapter || defaults.adapter;
// 适配器会把http请求包装成Promise并返回,dispatchRequest再以then()串接
return adapter(config).then(
// 若请求成功dispatchRequest会返回response给回应拦截器的fulfilled函式
function onAdapterResolution(response) {
throwIfCancellationRequested(config);
return response;
},
// 反之则将错误抛给回应拦截器的rejected函式
function onAdapterRejection(reason) {
if (!isCancel(reason)) throwIfCancellationRequested(config);
return Promise.reject(reason);
}
);
}
另外可以看到 throwIfCancellationRequested
不断的出现,这个函式会检查请求是否已经被「要求」取消,等我们进入到 CancelToken 时会再提到它。
由於我们是以浏览器发送请求,所以这边以 xhrAdapter
适配器为主。(完整程序码)
xhrAdapter
整段很长,但如果只看重点,其实就是在发送 XMLHttpRequest
,并在过程中做一些判断来决定要 resolve
或 reject
这个 Promise
。
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
// 建立一个新的XMLHttpRequest
var request = new XMLHttpRequest();
// 监听readyState的变化
request.onreadystatechange = function handleLoad() {
// readyState === 4 代表请求完成
if (!request || request.readyState !== 4) return;
// 若请求完成,准备好回应的response
var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
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(response),反之reject(error)
settle(resolve, reject, response);
request = null;
};
// 发送XMLHttpRequest
request.send(requestData);
});
};
到目前为止我们已经知道 axios 处理请求的流程,接下来就进入本文的重点 - CancelToken。
我把整个架构图像化,希望对各位有帮助。
在看原始码前,我们先看看 CancelToken
是怎麽使用的。
这段程序做了什麽可以先不管,我们只要知道,如果要使用 CancelToken
就必须在 request
的 config
中新增一个 cancelToken
属性。
let cancel
axios.get('/user/12345', {
cancelToken: new axios.CancelToken(c => { cancel = c; })
});
cancel()
再来就该看看我们在 cancelToken
属性中建构的 CancelToken类别
是什麽。
CancelToken
都会建立一个 Promise
,并且将 resolve
主动权给拿了出来,定义给resolvePromise
。CancelToken
的时候必须传入一个 function
,它会直接被呼叫并且得到一个名为 cancel
的函式作为参数。当要取消请求就是呼叫 cancel
,而它做了两件事情: 1. 赋值给属性 reason
2. 将属性 promise
给 resolve
function CancelToken(executor) {
// 判断executor是否为function
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
// 建立一个新的Promise物件,并将其resolve函式赋予给变数resolvePromise
// 此时Promise会是pending状态,还未被resolve
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
// 执行executor,并以函式「cancel」作为参数带入
var token = this;
executor(function cancel(message) {
// 确认reason是否存在,若存在代表cancel已被执行过
if (token.reason) return;
// 将reason赋值为一个Cancel类别
token.reason = new Cancel(message);
// resolve Promise
resolvePromise(token.reason);
});
}
// 确认reason是否存在,若存在代表此CancelToken的cancel已被执行过,便抛出错误
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
if (this.reason) throw this.reason;
};
所以 axios 只要根据这两个属性,就能判断此次请求是否已经被取消,而 throwIfRequested
就是利用 reason
来判断是否要抛出错误。
还记得我们在 dispatchRequest
里有看到 throwIfCancellationRequested
不断的被呼叫吗?(请看章节 #Function-dispatchRequest)
它的作用就是判断 config
是否有被加上 cancelToken
属性,有的话就会呼叫 CancelToken.prototype.throwIfRequested
,以此来判断请求是否已被取消。
function throwIfCancellationRequested(config) {
if (config.cancelToken) config.cancelToken.throwIfRequested();
}
没错,又再次看到了 xhrAdapter
,因为在前面我暂时省略了 xhrAdapter
内部的一个判断。
当它发现 config.cancelToken
存在,便会为 CancelToken.promise
接上一个 then()
,意味着当 promise
被 resolve
的那一刻,请求就会被 abort
。
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
var request = new XMLHttpRequest();
// ...省略....
if (config.cancelToken) {
// cancelToken.promise要被resolve才会执行then
// onCanceled(cancel)中的cancel会是cancelToken.reason
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) return;
// 取消XMLHttpRequest
request.abort();
reject(cancel);
request = null;
});
}
request.send(requestData);
});
};
首先我们可以知道 CancelToken 的原理就是在 request config
中加上一个 CancelToken类别
,并且利用其类别属性来判断 cancel
函式是否被呼叫执行,若已执行代表该请求被「要求」取消。
另外可以发现 axios 在以下三个时机点都有检查请求的取消与否:
了解整个 axios 架构以及 CancelToken 後,终於可以来实践取消请求的功能了,先来厘清我们的需求。
每次发送请求要判断是否已经存在相同的请求,若存在就取消前一次请求,只保留最新的
根据这样的需求我们归纳出几个必要的关键,然後准备以下程序码
config.cancelToken
// 暂存:纪录执行中的请求
const pending = new Map();
const addPending = config => {
// 利用method和url来当作这次请求的key,一样的请求就会有相同的key
const key = [config.method, config.url].join("&");
// 为config添加cancelToken属性
config.cancelToken = new axios.CancelToken(cancel => {
// 确认暂存中没有相同的key後,把这次请求的cancel函式存起来
if (!pending.has(key)) pending.set(key, cancel);
});
};
const removePending = config => {
// 利用method和url来当作这次请求的key,一样的请求就会有相同的key
const key = [config.method, config.url].join("&");
// 如果暂存中有相同的key,把先前存起来的cancel函式拿出来执行,并且从暂存中移除
if (pending.has(key)) {
const cancel = pending.get(key);
cancel(key);
pending.delete(key);
}
};
准备就绪後,只要在请求拦截与回应拦截器中呼叫它们即可...
// request 拦截器
instance.interceptors.request.use(
config => {
// 先判断是否有重复的请求要取消
removePending(config);
// 把这次请求加入暂存
addPending(config);
return config;
},
error => {
return Promise.reject(error);
}
);
// response 拦截器
instance.interceptors.response.use(
response => {
// 请求被完成,从暂存中移除
removePending(response);
return response;
},
error => {
return Promise.reject(error);
}
);
从此我们不必再担心 API 在回应前被重复触发导致错误,因为我们永远只会保留最新一次的请求。
<<: 安全框架和成熟度模型(Security Frameworks and Maturity Models)
>>: HTTP Token 使用方式: Basic Token v.s Bearer Token
Vue.js是一个前端框架,前端是由HTML、CSS和JavaScript三大元素所组成,所以我们前...
tags: OC 30 day 自动释放池的原理 存入到自动释放池中的对象,在自动释放池被销毁的时候...
第18篇有提到过Fetch的用法(连结),这篇要实际使用Fetch来做简单的Api串接。 首先介绍一...
水果钢琴 ( 类比讯号 ) 教学原文参考:水果钢琴 ( 类比讯号 ) 这篇文章会介绍如何使用「引脚设...
今天想发ARM的文章时,居然一直遇到这个画面: 虽然不确定是不是被攻击了,但後来还好可以连上主页了,...