扩展 Promise 实现可取消、进度通知
最近对 Promise 的用法有了全新的认识。想和你分享一下让我豁然开朗的点,大概会更新三篇 Promise 的文章,这是第一篇。
Promise 的返回值是什么
首先需要知道的是 Promise 有三个状态:pending、fulfilled、rejected。fulfilled 和 rejected 也被叫做 settled 。Promise 进入了 settled 就不能再改变了。
接下来,让你看一个示例,请问你觉得会输出什么呢?
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(p1);
console.log(p1 === p2)
答案是:true。
为什么有这样的表现呢?是因为如果 Promise.resolve 的入参是一个 Promise,它会直接返回那个入参;如果不是 Promise,返回的结果就是将返回值包裹了一层 Promise.resolve ,返回的结果是一个 Promise 对象。
另外,值得注意的是,就算是一个拒绝状态的 Promise,根据我们的规则,也是原样返回,还是拒绝状态:
Promise.resolve(Promise.reject(1))
// Promise {<rejected>: 1}
基于此,再看一个例子:
// 例子 A
Promise.resolve().then(function onResolved() {
return 'hello world';
}).then(res => {
console.log(res);
})
这一段比较好理解,会输出 'hello world'。
如果这里 onResolved 的返回值是一个 Promise 呢,会有什么样的表现?
// 例子 B
function sleepOneSecondAndLog() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('hello world')
}, 1000)
})
}
Promise.resolve().then(function onResolved() {
return sleepOneSecondAndLog();
}).then(res => {
console.log(res);
})
结果是过了一秒,我们 console.log(res) 那一行打印出了 'hello world' 。
解释这个现象的话,就是延续我们举的第一个例子的思路,在上面的例子中,下一个 Promise.then 是根据前一个 onResolved 函数的返回值来确定的,如果返回值不是 Promise 对象,那就包裹一层 Promise.resolve ,作为下一个 Promise 对象。也就是说,我们例子 A 相当于
Promise.resolve('hello world').then...
这也解释了为什么有时候我们上一级走 onRejected 的处理方法,下一个 then 却是进入了 onResolved,因为与它进入的处理函数无关,只和它的返回值有关,最后的结果也是被包裹了一层 Promise.resolve:
Promise.reject(new Error).then(undefined, () => {}).then(() => {
console.log('resolved')
}, () => {
console.log('rejected')
})
// resolved
如果 onResolved 返回值是 Promise 对象呢?那它就原样返回,then 方法的触发时机也就是等返回的这个 Promise 对象处于 settled 状态后再触发。所以我们的例子 B 过了一秒,才显示了打印结果。
总之,搞懂这个现象的根本就是知道返回值被悄悄的处理了一下。反正之前一直没有注意到这个细节。
扩展 Promise 可取消
之前有一个关于 Promise 可取消的提案,不过那个提案已经被驳回了。这里也讨论了为什么这个提案被驳回的一些原因。总结一下的话,被驳回是因为在 TC39 委员会,有一群人认为增加了可取消会让 Promise 变得太复杂了。然后反对该提案的主力是谷歌的,谷歌在 TC39 里面占了很大一部分,想推进实施也没办法,会遭到大多数人的反对。 在整个辩论过程中,作者身心俱疲,到最后,便不想在争论了,也不想再参与此提案了。 这件事情不禁让我同情起他。
于是乎,我们只能自己扩展原生的 Promise 来实现。首先来看一个最基础的扩展:
function makeCancelable<T>(promise: Promise<T>) {
let hasCanceled = false;
const wrappedPromise = new Promise((resolve, reject) => {
promise.then((val) => {
hasCanceled ? reject({hasCanceled: true}) : resolve(val)
})
promise.catch((err) => {
hasCanceled ? reject({hasCanceled: true}) : resolve(err)
})
})
function cancel() {
hasCanceled = true;
}
return {
promise: wrappedPromise,
cancel
}
}
const {promise, cancel} = makeCancelable(fetch("https://jsonplaceholder.typicode.com/posts/1"));
cancel()
上面的取消只是表面上的取消,在最后的调用演示中,接口还是被调用了,只不过,在使用方拿不到结果。由于 Promise 的结果返回了,所以也不会阻塞回调里后续的操作。但如果我们想在接口层面取消呢?
如果我们就是想中断一个接口的调用呢?我们以上面的 fetch API 为例。浏览器的 fetch API 本身并不支持取消,需要借助 AbortController。
基本使用如下:
const controller = new AbortController();
const signal = controller.signal;
const downloadBtn = document.querySelector('.download');
const abortBtn = document.querySelector('.abort');
downloadBtn.addEventListener('click', fetchVideo);
abortBtn.addEventListener('click', function() {
controller.abort();
console.log('Download aborted');
});
function fetchVideo() {
...
fetch(url, {signal}).then(function(response) {
...
}).catch(function(e) {
reports.textContent = 'Download error: ' + e.message;
})
}
上面的关键点,也就是在 fetch 调用的时候多传一个 signal,然后后续如果想取消,就调用 controller.abort ,fetch 可以检测到 signal 的变化,从而中断请求。
根据这个特性,我们扩展一个可取消的 fetch API。
function makeCancelableFetch(url: string) {
const controller = new AbortController();
const signal = controller.signal;
const promise = fetch(url, {signal})
const cancel = () => {
controller.abort();
}
return {
promise,
cancel
}
}
const {promise, cancel} = makeCancelableFetch('https://jsonplaceholder.typicode.com/posts/1')
cancel()
promise.then(res => {
console.log(res)
}, (err) => {console.log(err)}) // DOMException: The user aborted a request.
支持进度通知
假设有一个列表页,在刚进入的时候需要同步,同步耗时会比较久,此时就需要后端提供一个查询进度的接口,我们前端这边不断的轮询查询接口,直到进度到达 100 %,便进行下一步的列表展示逻辑。
最基础的,我们可以这么写:
let timerId: undefined | number = undefined;
let progress = 0;
async function getList() {
console.log("开始查询列表");
}
function notify(progress: number) {
console.log("当前进度%d%", progress);
}
async function readProgress() {
progress += 20;
return progress;
}
async function run() {
let currentProgress = await readProgress();
console.log(currentProgress);
if (currentProgress >= 100) {
clearTimeout(timerId);
getList();
return;
}
notify(currentProgress);
timerId = setTimeout(() => {
run();
}, 100);
}
run();
如果想通过扩展 Promise 对象实现,结果如下:
let progress = 0;
async function getList() { console.log('开始查询列表') }
function notify(progress: number) { console.log('当前进度%d\%', progress); }
async function readProgress() {
progress += 20;
return progress;
}
type AnyFunction = (...arg: any) => void;
type Executor<T> = (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: any) => void,
notifyFn: (status: any) => void,
) => void
class TrackablePromise<T> extends Promise<T> {
subscribes: AnyFunction[]
constructor(executor: Executor<T>) {
const subscribes: AnyFunction[] = []
super((resolve, reject) => {
return executor(resolve, reject, (status: any) => {
subscribes.forEach((subscribe) => subscribe(status))
})
})
this.subscribes = subscribes;
}
addSubscribe(subscribe: AnyFunction) {
this.subscribes.push(subscribe)
return this
}
}
const p = new TrackablePromise((resolve, reject, notify) => {
async function run() {
let currentProgress = await readProgress();
if (currentProgress >= 100) {
resolve({success: true});
} else {
notify(currentProgress);
setTimeout(() => {
run();
}, 1000)
}
}
run();
} )
// 如果想添加多个订阅者,可以继续链式调用
p.addSubscribe((x: any) => {
console.log(x);
}).addSubscribe(...)
p.then(() => { getList()})
使用这种方式,我们可以让获取列表的接口和读取进度的接口分离开来了,也提高了这个读取进度接口在其他地方复用的可能性。不过更重要的是让我们知道了在基础上扩展 Promise 对象。
以上就是本文的全部内容了,谢谢阅读 ~
转载自:https://juejin.cn/post/7044526741758410789