按照惯例,我们先来看下官方的例子:
你可以通过axios的CancelToken工厂函数,生成一个source,然后把这个对象作为参数传递给axios,最后,需要取消的时候调用source的cancel方法即可。
你还可以通过在参数中绑定new CancelToken的参数中的回调,来赋值执行取消操作。
最后,你还可以通过fetch的API来执行取消操作。OK,我们来看下如何实现这样的取消功能。首先,我们先在lib目录下创建一个cancel文件夹,然后在cancel下创建个Cancel.js:
// 这这个是啥呢?就是你取消请求的时候抛出去的错误类型的类 // 很简单,就是个message function Cancel(message) { this.message = message; } // 把它转换成字符串的toString方法的自定义实现 Cancel.prototype.toString = function toString() { return "Cancel" + (this.message ? ": " + this.message : ""); }; // 原型上绑定个__CANCEL__,给isCancel做判断的条件。 Cancel.prototype.__CANCEL__ = true; export default Cancel;
代码很简单,实际上就是一个取消的消息字符串。并改写了原型上的toString方法。然后我们isCancel.js:
export default function isCancel(value) { return !!(value && value.__CANCEL__); }
它是用来判断当前的请求是否已经被取消了,以便我们去做判断条件后的其他操作。关键的来了,我们来创建一个CancelToken.js:
function CancelToken(executor) { if (typeof executor !== "function") { throw new TypeError("executor must be a function."); } // 存储promise resolve方法的便利 var resolvePromise; // 给实例绑定个promise,并把resolve赋值给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) { 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; }; executor(function cancel(message) { if (token.reason) { // Cancellation has already been requested return; } token.reason = new Cancel(message); resolvePromise(token.reason); }); }
我们来看下他做了啥。注意,重点难点来了,这是我觉得整个axios里最不好理解的地方(原谅我水平有限)。首先,判断下传入的参数是不是一个函数,如果不是函数就抛出一个错误。然后通过
resolvePromise变量保存一个promise的resolve方法。下面呢,重点来了,把CancelToken的实例也就是this,赋值给token变量,然后: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; });
这段代码,循环执行token上订阅的事件,执行后置为null。要注意,这里的this.promise.then中的then方法,在现在这个阶段,跟promise一点关系都没有,你就把这个then方法,当成一个函数,传了一个回调函数而已。OK,先记住这个,我们继续往下:
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; };
看到了吧,这个就是我上面说的自定义的promise.then方法。他传入的这个形参onfuifilled就是:
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; }
对吧?理解了没?然后我们再看this.promise.then = “函数”的这个“函数”干了啥,这就比较好理解了,同样了,声明了个变量,声明了一个该函数作用域内的真正的promise,然后promise内的同步代码中,让this也就是token,记得之前最开始的地方,把this赋给了token,这里的token就是cancelToken的实例,执行订阅的subscribe方法,订阅这个resolve,然后,在resolve的时候执行
onfulfilled方法,也就是订阅的内容。再然后,在promise变量上添加一个cancel方法,用来取消订阅。 OK,我们再来捋一下整个CancelToken类的执行过程。再然后,咱们回到xhr文件中,添加些相关代码:
- 给实例this绑定一个真正的promise对象,把这个对象的resolve执行函数存储给resolvePromise变量,以便在恰当的时候调用。
- 自定义一个then方法,这个方法返回一个promise,并把这个promise的resolve状态订阅到listener中,并在promise上添加一个cancel取消订阅的方法。
- 然后等待外部调用
再往下,我们看下:
CancelToken.source = function source() { var cancel; var token = new CancelToken(function executor(c) { cancel = c; }); return { token: token, cancel: cancel }; };
CancelToken的静态类上绑定了这样的方法。他这里的c其实就是CancelToken中executor的回调函数cancel:
executor(function cancel(message) { if (token.reason) { // Cancellation has already been requested return; } token.reason = new Cancel(message); resolvePromise(token.reason); });
所以,我们会在外部调用这个cancel。回头看下我们cancel的使用方法文档。是不是就理解了?接下来我们继续:
var onCanceled; function done() { if (config.cancelToken) { config.cancelToken.unsubscribe(onCanceled); } if (config.signal) { config.signal.removeEventListener("abort", onCanceled); } }
首先,onCanceled就是如果存在取消的cancelToken参数的话,那么就会生成一个onCanceled函数,done方法就是移除的函数。很好理解吧。然后咱们往下:
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); } }
这一块,就是如果存在着两个参数,那么给onCanceled赋值一个函数,之前说过了,signal是啥呢,其实是类似fetch的使用方法的一个参数,最开始的例子里说过了。看一下哈,其实这块的代码很好理解,就是绑定或执行原生的取消方法嘛。最最核心的一行代码:
config.cancelToken && config.cancelToken.subscribe(onCanceled);
我们这里订阅了也就是在这里我们会真正的把取消请求加入到订阅的列表中去。当我们调用source.cancel方法时,就会执行取消了。
我们来回顾下,如果存在取消函数的话,还调用了cancelToken上的subscribe方法订阅了一下onCanceled函数,subscribe很简单,就是一个添加操作:
CancelToken.prototype.subscribe = function subscribe(listener) { if (this.reason) { listener(this.reason); return; } if (this._listeners) { this._listeners.push(listener); } else { this._listeners = [listener]; } };
有subscribe自然也有unsubscribe:
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, }; };
就是把CancelToken的实例和取消函数返回了。所以,我们在使用的时候,可以像这样来使用:
const CancelToken = axios.CancelToken; const source = CancelToken.source(); axios .get("/c7/get", { cancelToken: source.token, }) .catch(function (e) { if (axios.isCancel(e)) { console.log("Request canceled", e.message); } }); source.cancel("Operation canceled by the user.");
好了,到这里CancelToken的实现就完成了,详细的代码大家可以去项目里查看,这块还是稍微有点绕的,大家可以自己调试,捋一下。那么下一篇,就是最后一篇了,我们会新增一些小的功能点,十分简单。
站在巨人的肩膀上,希望我可以看的更远。