所有代码已经包含到 async-utilities 仓库中,并且已发布到 npm 原文链接:blog.bowen.cool/zh/posts/wh...
背景
在HTML
的表单里,有这样一种场景:
html
<form id="form">
<!-- <label for="name">Name:</label>
<input type="text" name="name" id="name"> -->
<button type="submit">submit</button>
</form>
点击"提交"就往服务端发送一个请求:
js
// 网络请求
function api(data) {
console.log("submiting", data);
return fetch("https://httpbin.org/delay/1.5", {
body: JSON.stringify(data),
method: "POST",
mode: "cors",
});
}
const form = document.getElementById("form");
const handler = async function (e) {
e.preventDefault();
const rez = await someApi({
msg: "some data to be sent",
});
console.log(rez);
};
form.addEventListener("submit", handler);
为防止用户重复提交,我们通常会维护一个loading
状态...但是写得多了,难免有一种机械劳动的感觉。而且,当一个表单出现很多按钮时,我岂不是维护很多loading
变量?
我看着眼睛好累,而且接口响应很快,偷偷少写一个loading
应该不会被发现吧🌚,可是万一接口要是挂了...算了,来不及想这些了
上面的场景不知道你有没有经历过呢?实际上,大多数产品的按钮都是没有loading
效果的,因为整个世界就是一个大草台班子😂,但是作为一个合格的前端,每个人都需要对用户体验负责!
能不能站着就把钱挣咯?
我们先来梳理一下:
- 短时间内每个事件都会产生一个
promise
,核心需求是降频。即"promise
三千,我只取一个结果" promise
的响应时间是不确定的
降频
先回想一下同步代码中事件降频:节流(throttle
)、防抖(debounce
)。关于这两者,相信你已经很熟悉了,我们一句话概括:
二者都是在单位时间内的多次相同事件中取一次调用(也可以说成:事件三千,我只取一次执行) ,不同的是前者取的第一次,后者取的最后一次。
把我们的需求也改成这种句式:在短时间内 的多次相同事件中取一次调用。所以,这个"短时间内"才是关键 !
重新定义间隔
我们希望上一个promise
结束之前,接下来的promise
创建操作统统丢弃 。所以,"短时间内"就等于"上一个promise
的pending
期间","接下来的promise
创建操作统统丢弃"意思就是"取第一次",promise
的丢弃可以通过创建"永远pending
"的promise
实现,所以我们的需求就是: 在上一个promise
的pending
期间,多次promise
创建操作中取第一次(就是这个正在pending
的promise
)执行。
编码
思路都参考了,代码也参考一下吧,这里贴个简易版的节流实现代码:
js
/**
* @description 节流
* @param {function} fn
* @param {number} ms 毫秒
* @returns {function} 节流后的function
*/
function throttle(fn, ms = 300) {
let lastInvoke = 0;
return function throttled(...args) {
const now = Date.now();
if (now - lastInvoke < ms) return;
lastInvoke = now;
fn.call(this, ...args);
};
}
依葫芦画瓢,简单改造一下:
js
/**
* @description 异步节流:上一次的promise pending期间,不会再次触发
* @param {() => Promise<any>} fn
* @returns {() => Promise<any>} 节流后的function
*/
function throttleAsyncResult(fn) {
let isPending = false;
return function (...args) {
if (isPending) return new Promise(() => {});
isPending = true;
return fn
.call(this, ...args)
.then((...args1) => {
isPending = false;
return Promise.resolve(...args1);
})
.catch((...args2) => {
isPending = false;
return Promise.reject(...args2);
});
};
}
使用方法(Demo)
tsx
import { throttleAsyncResult } from "@bowencool/async-utilities";
/* make a network request */
function api(data: { msg: string }) {
console.log("submiting", data);
return fetch("https://httpbin.org/delay/1.5", {
body: JSON.stringify(data),
method: "POST",
mode: "cors",
});
}
const throttledApi = throttleAsyncResult(api);
export default function ThrottleAsyncResultDemo() {
return (
<button
onClick={async function () {
const rez = await throttledApi({
msg: "some data to be sent",
});
console.log("completed");
}}
>
submit(click me quickly)
</button>
);
}
打开开发者工具可以看到,无论点击多快,始终不会出现请求并行的情况: 大功告成!
一个孪生兄弟
debounceAsyncResult
刚才的throttleAsyncResult
是控制如何创建promise
,那么如果已经创建了很多promise
,我们该如何才能取到最新的结果呢,毕竟哪个promise
跑得快,谁也不知道。
所以就会有debounceAsyncResult
(Demo):已经创建的众多promise
中,取最后创建的promise
结果。
"偷懒"是程序员第一生产力,学到了吗🤔?