1 Promise
Promise
有一个缺点是一旦创建无法取消,所以本质上Promise
是无法被终止的.
但是我们可以通过中断调用链
或中断Promise
来模拟请求的中断.
中断调用链
中断调用链就是在某一个then/catch
执行之后,后续的链式调用(包括then,catch,finally)
不再继续执行.
方法是在then/catch
返回一个新的Promise
实例,并保持pending
状态:
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('result');
});
}).then(res => {
// 达到某种条件,return一个pending状态的Promise实例,以中断调用链
if (res === 'result') {
return new Promise(() => {});
}
console.log(res); // 不打印
}).then(() => {
console.log('then不执行'); // 不打印
}).catch(() => {
console.log('catch不执行'); // 不打印
}).finally(() => {
console.log('finally不执行'); // 不打印
});
中断Promise
中断Promise
不等同于中止Promise
,因为Promise
是无法被终止的.
这里的中断指的是,在合适的时机,把pending
状态的promise
给reject
掉.例如一个常见的应用场景就是给网络请求设置超时时间,一旦超时就中断.
老规矩,用setTimeout
来模拟网络请求.阀值设置为Math.random() * 3000
表示随机3秒之内返回结果.
const request = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('收到服务端数据')
}, Math.random() * 3000)
})
假设超过2秒就是网络超时,我们可以封装一个超时处理函数.
由于网络请求所需的事件是随机的,因此可以利用Promise.race
方法,达到超时reject
的目的.
const timeoutReject = (p1, timeout = 2000) => {
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('网络超时');
}, timeout);
});
return Promise.race([p1, p2]);
};
timeoutReject(request).then(res => {
console.log(res);
}).catch(err => {
console.log(err);
});
包装abort
方法——仿照Axios
的CancelToken
上面实现的方式并不灵活,因为中断Promise
的方式有很多,不单单是网络超时.
我们可以仿照Axios
中CancelToken
的核心源码,简单包装一个abort
方法,供使用者随时调用.
function abortWrapper(p1) {
let abort;
const p2 = new Promise((resolve, reject) => {
abort = reject;
});
// 如果没有resolve或reject,p2的状态永远是pending
const p = Promise.race([p1, p2]);
p.abort = abort;
return p;
}
const req = abortWrapper(request);
req.then(res => {
console.log(res);
}).catch(err => {
console.log(err);
});
setTimeout(() => {
// 手动调用req.abort,将p2的状态改变为rejected
req.abort('手动中断请求');
}, 2000);
如此封装的主要目的就是为了能够在Promise
外部控制其resolve
或reject
,让使用者可以随时手动调用resolve(触发.then)
或reject(触发.catch)
.
需要注意的是,虽然
Promise
请求被中断了,但是promise
并没有终止,网络请求依然可能返回,只不过那时我们已经不关心请求结果了.
2 RXJS
的unsubscribe
方法
rxjs
本身提供了取消订阅的方法,即unsubscribe
.
let stream1$ = new Observable(observer => {
let timeout = setTimeout(() => {
observer.next('observable timeout');
}, 2000);
return () => {
clearTimeout(timeout);
}
});
let disposable = stream1$.subscribe(value => console.log(value));
setTimeout(() => {
disposable.unsubscribe();
}, 1000);
3 Axios
的CancelToken
Axios
的CancelToken
有两种使用方法:
-
方法一
import axios from 'axios'; 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 } }); source.cancel('Operation canceled by the user.');
-
方法二
import axios from 'axios'; const CancelToken = axios.CancelToken; // 创建一个变量如 cancel 用于存储这个中断某个请求的方法 let cancel; axios.get('/user/12345', { cancelToken: new CancelToken(function executor(c) { cancel = c; // 将参数 c 赋值给 cancel }) }); // 判断 cancel 是否为函数,确保 axios 已实例化一个CancelToken if (typeof cancel === 'function') { cancel(); cancel = null; }
CancelToken
的核心源码:(axios/lib/cancel/CancelToken.js)
'use strict';
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 resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
executor(function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}
/**
* Throws a `Cancel` if cancellation has been requested.
*/
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
if (this.reason) {
throw this.reason;
}
};
/**
* 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;
可以看到,在Axios
底层,CancelToken
的核心源码所体现的思想,与上面中断Promise
包装abort
方法的思想一致.
只不过Axios
在外部手动调用resolve
(用户触发cancel
方法),而resolve
一旦调用,就会触发promise
的then
方法,来看这个promise.then
的源码:(axios/lib/adapters/xhr.js)
if (config.cancelToken) {
// Handle cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
// Clean up request
request = null;
});
}
可以看到then
方法中会执行abort
方法取消请求,同时调用reject
让外层的promise
失败.