起因
最近碰到一个面试题
页面里面有一个按钮,点击之后需要向服务端发起一个请求,在这个请求没有返回之前,无论点击多少次都不会发送第二遍请求。
基础写法
typescript
const fetchData: (
duration: number,
status: "success" | "error"
) => Promise<"success" | "error"> = (duration, status = "success") => {
console.log('Enter FetchData Function');
return new Promise((resolve, reject) => {
setTimeout(() => {
status === "success" ? resolve(status) : reject(status);
}, duration);
});
};
let lock = false;
const baseButtonDom = document.getElementById("base") as HTMLButtonElement;
const onClickFunc = async () => {
if (!lock) {
lock = true;
const data = await fetchData(3000, "success");
console.log(data);
lock = false;
}
};
baseButtonDom.addEventListener("click", onClickFunc);
如果只针对这个需求的话,是可以满足的,但是缺点也很明显,这个写法不够通用。
采用 Lodash 的防抖节流
先看看 他们的异同点
相同点:
- 都可以通过使用
setTimeout
实现 - 目的都是,降低回调执行频率。节省计算资源
不同点:
实现 | 执行时机 | |
---|---|---|
防抖 | 在一段连续操作结束后,处理回调,利用clearTimeout 和 setTimeout 实现 |
一定时间连续触发的事件,只在最后执行一次 |
节流 | 在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能 | 一段时间内只执行一次 |
javascript
import { debounce, throttle } from "lodash";
const clickCallback = async () => {
const data = await fetchData(3000, "success");
console.log(data);
};
const debounceButtonDom = document.getElementById("debounce") as HTMLButtonElement;
const throttleButtonDom = document.getElementById("throttle") as HTMLButtonElement;
throttleButtonDom.addEventListener(
"click",
throttle(clickCallback, 3000, {
leading: false, // 指定调用在节流开始前
})
);
debounceButtonDom.addEventListener("click", debounce(clickCallback, 3000));
在快速点击三下的场景下,防抖和节流的打印表现都是一致的
在当连续点击的场景下会有所不同
- 防抖 : 由于 防抖 会一直重置事件,所以会一直卡着不会进行输出,直到放开鼠标才会执行事件。
- 节流 : 节流的实现逻辑是在指定时间内只会实现一次,但在连续点击时,只要超过设置的时间,就会进行下一次调用,没办法满足需求 " 在请求没有返回之前,无论点击多少次都不会发送第二遍请求。 "
所以 防抖和节流 达不到预期想要的结果;
最终实现
采用节流的逻辑实现,修改他的判断条件即可。节流是在规定时间内只执行一次,那么把这个条件修改成 Promise 状态判断就可以满足。pending
状态阻止调用,fulfilled
状态可以继续调用。那么函数 重新执行 的时机就是 fulfilled
的时候。也就满足了需求中的 " 在请求没有返回之前,无论点击多少次都不会发送第二遍请求。 "
typescript
// 获取 Promise Status
const promiseState = (promise: Promise<unknown>) => {
const target = {};
return Promise.race([promise, target]).then(
(value) => (value === target ? "pending" : "fulfilled"),
() => "rejected"
);
};
// 判读是否是 Promise
const isPromise = (promise: Promise<unknown>) => {
return promise && promise.then && typeof promise.then === "function";
}
/**
*
* @param {Asyncfunction} fn - 传入一个异步方法
* @returns {function}
*/
export const throttleForPromise = (fn: Function) => {
let status: Promise<unknown> | null;
return function (this: any) {
if (status && isPromise(status)) {
promiseState(status).then((resultStatus) => {
if (resultStatus === "fulfilled") status = fn.apply(this, arguments);
});
} else {
status = fn.apply(this, arguments);
}
};
};
- 为什么不用
instanceof
判断 Promise ?
instanceof
只能判断符合 ES 标准的 Promise ,不考虑兼容 Promise A+ 规范的的情况下完全可以 - 还有其他办法拿到 Promise 的状态吗 ?
Promise 的状态是一个内部状态,暂时没有办法直接拿到,如果有其他更好的获取方式的话,望告知
typescript
const clickCallback = async () => {
try {
const data = await fetchData(3000, "success");
console.log(data);
} catch (error) {
console.log(error);
}
};
const promiseButtonDom = document.getElementById("throttle-promise") as HTMLButtonElement;
promiseButtonDom.addEventListener("click", throttleForPromise(clickCallback));
连续点击三下
连续不停点击 且 状态为 reject 时
均可以满足在 Promise 状态为 pending
时阻止继续调用,状态为 fulfilled
时重新执行。
以上 ,就是 throttlePromise
函数的简单实现了。函数只针对该需求的一个扩展,如果有更好的写法,欢迎讨论。