目录
CancelToken存在内存泄露隐患【切片上传长时间没有中断】
本质:观察订阅+
xhr.abort()
就绪状态readyState:0
- 取值范围是 0 到 4,分别表示不同的状态:
- 0: 请求未初始化(
open
方法还未调用)。- 1: 服务器连接已建立(
open
方法已经调用)。- 2: 请求已接收(
send
方法已经调用,并且头部和状态已经可获得)。- 3: 请求处理中(响应体正在接收)。
- 4: 请求已完成,且响应已就绪。
状态码status:0
// 创建AbortController实例且存放到controller上
// 注意这里每次请求都会创建一个新的AbortController实例,
//是因为AbortController实例调用abort后
//AbortController实例的状态signal就为aborted不能更改
controller.current = new AbortController();
const xhr = new XMLHttpRequest();
xhr.open("get", "https://mdn.github.io/dom-examples/abort-api/sintel.mp4");
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
setMessage("下载成功");
setLoading(false);
}
};
// 监听AbortController实例的abort事件,当AbortController实例调用abort方法时,就会触发该事件执行回调
controller.current.signal.addEventListener("abort", () => {
setMessage("下载中止");
setLoading(false);
xhr.abort();
});
xhr.send();
// 调用AbortController实例的abort方法,从而触发上面注册在abort事件的回调的执行
const abortDownload = () => {
controller.current.abort();
};
cancelToken
源码
CancelToken
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;
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;
});
// source.cancel指向此处的cancel函数
//在取消时调用 resolvePromise 函数
executor(function cancel(message) {
// token.reason有值代表cancel已被执行,CancelToken是一个一次性的Subject,notify一次后即失效
if (token.reason) {
return;
}
token.reason = new CanceledError(message);
// resolvePromise执行时,会执行上面this.promise.then中传入的回调函数。从而把listeners全执行
resolvePromise(token.reason);
});
}
(un)subscribe
- 在请求前,
subscribe
注册onCancel
函数 - 在请求后,
unsubscribe
注销onCancel
函数
CancelToken.prototype.subscribe = function subscribe(listener) {
// 如果CancelToken实例已经执行cancel,直接执行该回调函数
if (this.reason) {
listener(this.reason);
return;
}
// 如果CancelToken实例还没执行cancel,则把回调函数放进_listeners里
if (this._listeners) {
this._listeners.push(listener);
} else {
this._listeners = [listener];
}
};
// 把回调函数从_listeners中移除
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
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel,
};
};
示例
A.axios.canceltoken
//初始化
let cancel = null;
//请求
if(cancel != null) cancel() //如果上一次的请求还在继续,则取消
axios({
method:"get",
url:"http://localhost:3000/test.php",
cancelToken:new axios.CancelToken(function(c){
cancel = c
})
}).then(response=>{
//处理响应数据
cancel = null
}).catch(reason=>{
//错误处理
})
}
B.source对象创建canceltoken
let source = axios.CancelToken.source();
// 判断上一次的请求是否还在继续,如果还在继续,则取消上一次的请求
if(source.token._listeners!=undefined )
{
source.cancel("取消请求")
source = axios.CancelToken.source()
}
axios.get('http://localhost:3000/front-end/axios/response.php',{
cancelToken:source.token
}).then(response=>{
// 处理响应
}).catch(reason=>{
if(axios.isCancel(reason)){
console.log("取消请求",reason)
}else{
//错误处理
}
})
}
AbortController接口:(控制器对象)
2021 年 10 月推出的AxiosV0.22.0
版本中把CancelToken
打上 👎deprecated 的标记,意味废弃。与此同时,推荐 AbortController
来取而代之
源码
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
var onCanceled;
// done函数用于在请求结束后注销回调函数,以免发生内存泄漏
function done() {
// 如果使用CancelToken实例,则会在下面发出请求逻辑之前通过subscribe注册onCanceled函数
if (config.cancelToken) {
config.cancelToken.unsubscribe(onCanceled);
}
// 如果使用AbortController实例,则会在下面发出请求逻辑之前通过signal.addEventListener监听abort事件且注册onCancel作为回调函数
if (config.signal) {
config.signal.removeEventListener("abort", onCanceled);
}
}
var request = new XMLHttpRequest();
var fullPath = buildFullPath(config.baseURL, config.url);
request.open(
config.method.toUpperCase(),
buildURL(fullPath, config.params, config.paramsSerializer),
true
);
function onloadend() {
// 生成response对象
var response = { data, status, statusText, headers, config, request };
// settle函数内部根据response.status或config.validateStatus去调用_resolve或_reject
settle(
function _resolve(value) {
resolve(value);
done();
},
function _reject(err) {
reject(err);
done();
},
response
);
}
if ("onloadend" in request) {
// Use onloadend if available
request.onloadend = onloadend;
} else {
// Listen for ready state to emulate onloadend
request.onreadystatechange = function handleLoad() {
if (!request || request.readyState !== 4) {
return;
}
if (
request.status === 0 &&
!(request.responseURL && request.responseURL.indexOf("file:") === 0)
) {
return;
}
// onreadystatechange事件会先于onerror或ontimeout事件触发
// 因此onloaded需要在下一个事件循环中执行
setTimeout(onloadend);
};
}
// Handle browser request cancellation (as opposed to a manual cancellation)
request.onabort = function handleAbort() {};
// Handle low level network errors
request.onerror = function handleError() {};
// Handle timeout
request.ontimeout = function handleTimeout() {};
// 处理用到CancelToken或AbortController的情况
if (config.cancelToken || config.signal) {
// 取消请求的函数
onCanceled = function (cancel) {
if (!request) {
return;
}
reject(
!cancel || (cancel && cancel.type) ? new CanceledError() : cancel
);
request.abort();
request = null;
};
// 如果是用CancelToken取消请求,则把onCanceled注册到CancelToken实例上,
// CancelToken实例本质上是一个观察者模式中的Subject
config.cancelToken && config.cancelToken.subscribe(onCanceled);
// 如果是用AbortController,则先从AbortController实例的signal.aborted判断其是否已调用abort,
// 如果已调用,直接执行onCanceled,如果没有则直接在signal上监听其事件,逻辑和开头展示AbortController取消XHR请求的例子一样
// axios.request在调用时,会return一条动态生成的promise链,链上的顺序是:
// Promise.resove(config)->所有请求拦截器(onFulfilled,onRejected)->(dispatchRequest,undefined)->所有响应拦截器(onFulfilled,onRejected)
// dispatchRequest就是调用config.adapter或default.adapter去发出请求,
// 因为存在执行请求拦截器途中,AbortController实例已调用aborted的情况,因此这里要对config.signal.aborted做判断处理
if (config.signal) {
config.signal.aborted
? onCanceled()
: config.signal.addEventListener("abort", onCanceled);
}
}
request.send(requestData);
});
};
替换原因
-
保持与
fetch
一样的调用方式,让开发者更好上手Axios
官方一直保持自身的调用方式与fetch
相似,如下所示: -
fetch(url,config).then().catch() axios(url,config).then().catch()
而目前
fetch
唯一中断请求的方式就是与AbortController
搭配使用。Axios
通过支持与fetch
一样调用AbortController
实现中断请求的方式,让开发者更方便地从fetch
切换到Axios
。目前就实用性而言,XHR
还是比fetch
要好,例如sentry
在记录面包屑的接口信息方面,XHR
请求可以比fetch
请求记录更多的数据。还有目前fetch
还不支持onprogress
这类上传下载进度事件。 -
CancelToken
存在内存泄露隐患【切片上传长时间没有中断】切片上传过程中没有发生中断或者很久才发生中断,则
cancelToken.promise
会一直存在在内存里,而由于xhrAdapter
中cancelToken.promise
通过.then(function onCancel(){...})
挂载了很多个onCancel
示例
axios
fetch
controller.current = new AbortController();
fetch("https://mdn.github.io/dom-examples/abort-api/sintel.mp4", {
// fetch配置中仅需把signal指向AbortController实例的signal即可
signal: controller.current.signal,
})
.then(() => {
setMessage("下载成功");
setLoading(false);
})
.catch((e) => {
setMessage("下载错误:" + e.message);
setLoading(false);
});
};
const abortDownload = () => {
controller.current.abort();
};