前言
我们开发中可能遇到形形色色的业务,其中会用到防抖节流,节流我们这里先不考虑,我们对于防抖功能通过一步一步的探索,并研究出针对其更加有效的处理方案,也能让我们在合适的场景用合适的防抖逻辑
防抖探索
模拟高响应差的网络请求
先模拟一个接口请求,但是响应时间随机,大约在0~3000ms之间,方便用于模拟高响应差下的连续请求情况
原因:两个不同搜索条件差距巨大,在庞大的数据量下,请求时间波动可能会略大,这里进一步扩大这种特殊情况
js
//模拟一个 3s 以内的随机请求相应时间的接口请求
const queryRecords = (text) => {
let res, rej;
const promise = new Promise((resolve, reject) => {
res = resolve;
rej = reject;
});
const interval = new Date().getTime() % 3000;
setTimeout(() => {
if (interval > 2990) {
rej("接口太累了,超时了");
} else {
res(text);
}
}, interval);
return promise;
};
模拟一个基础搜索请求
一个基础的请求是这样的,一般场景这么写很ok
场景:点击搜索按钮请求,点一次请求一次
js
//默认的搜索逻辑-待优化
const search = async (text) => {
const res = await queryRecords(text);
console.log("默认的搜索逻辑-待优化", res);
};
// search("初版本");
模拟防抖场景
有很多场景没有搜索按钮发挥的地方,或者带搜索按钮影响体验,因此为了增加体验,我们决定让用户边输入边请求,这样频繁调用接口肯定不合适,所以会用到防抖功能,大约用户输入暂停后500ms后发起请求
场景:首页的菜单搜索,本身地方有限,带着搜索按钮看着也不优雅,这种逻辑一般用于搜索时间相对稳定的场景,尤其是针对于本地拥有全部搜索数据的情况
使用,对搜索方法包装debounce防抖处理,调用防抖函数,并且本案例还模拟了多次请求场景(且模拟卡着500ms延迟边缘请求,用于后续发现问题)
js
//为了增加体验,我们的搜索决定,让用户输入后自动搜索,500ms无操作则请求接口
//因此加入的防抖功能,延迟 500ms,自动取消之前未发起的请求
const debounce = (fn, interval) => {
let timeout = null;
return (text) => {
timeout && clearTimeout(timeout);
timeout = setTimeout(() => {
fn(text);
timeout = null;
}, interval);
};
};
const searchWithDebounce = async (text) => {
const res = await queryRecords(text);
console.log("防抖搜索,基础版本", res);
};
const realSearch = debounce(searchWithDebounce, 500);
//进行如下测试,会发现,如果搜索时间比较长,切搜索时间不稳定,则可能出现后请求的先响应
//1. 假设防抖 300ms执行了第一个请求,此时又搜索了一个内容,假设第二个内容600ms后执行
//2. 如果后执行的任务速度快先响应(也就是后发先至),则搜索后的响应展示,则可能会展示已经失效的搜索内容(第一次搜索的内容,但内容已过期)
const testRealSearch = () => {
let i = 0;
let interval = setInterval(() => {
realSearch(i++);
if (i > 10) {
clearInterval(interval);
}
}, 500);
};
// testRealSearch()
针对接口高响应差的防抖逻辑处理
通过上面留下的案例,也可以看出来,正常防抖,面对耗时比较久,且根据不同搜索条件响应时间比较久的情况下,会出现响应错误问题,也就是最后响应的是先发起的请求,处理不当的话,可能会引起前端页面渲染错误的场景出现
因此进行下面两种优化:
- 外部存放一个时间戳,每次发起请求前,更新时间戳,以最后一次发起请求的时间戳进行响应,其他的均忽略
- 编写一个通用的请求控制逻辑接管请求promise函数,保证只响应最后一次发起的请求(算是1的通用封装版本,更实用)
步骤1,简易的利用发起的时间戳对比响应逻辑
js
//上面的防抖逻辑问题正常快速响应的情况下,或者搜索时间比较稳定的情况问题不大,一旦波动则会出现请求后发先至问题,从而响应错误
//1.正常情况,我们做个请求时间标记,只处理最新发起的一次请求即可,逻辑也比较简单
//打印表现,发起n->改进回调n(中间可能会有多个每次回调,但不能有新的发起),则表示一次正常有效回调
let currentDate = null;
const searchWithDebounce2 = async (text) => {
const date = new Date();
currentDate = date;
console.log("防抖搜索-优化版本1-发起请求", text);
await queryRecords(text);
console.log("防抖搜索-优化版本1-上版每次回调", text);
if (date === currentDate) {
console.log("防抖搜索-优化版本1-改进后实际回调", text);
}
};
const realSearch2 = debounce(searchWithDebounce2, 500);
const testRealSearch2 = () => {
let i = 0;
let interval = setInterval(() => {
realSearch2(i++);
if (i > 10) {
clearInterval(interval);
}
}, 600);
};
// testRealSearch2()
步骤2,通过接管请求函数的方式,封装时间戳对比逻辑,封装成统一的逻辑
js
//2.通用实现,我们做一个通用逻辑,直接传入一个请求,直接控制请求,不写那么多重复的currentDate之类的判断代码了
const singleRequest = (fn) => {
let currentDate = null;
return (...args) => {
const now = new Date();
currentDate = now;
let res, rej;
const newPromise = new Promise((resolve, reject) => {
res = resolve;
rej = reject;
});
//接管传入的请求的promise,控制promise的响应
fn(...args)
.then((result) => {
currentDate === now && res(result);
})
.catch((error) => {
currentDate === now && rej(error);
});
return newPromise;
};
};
//接管请求
const newRecordsRequest = singleRequest(queryRecords);
const searchWithDebounce3 = async (text) => {
const res = await newRecordsRequest(text);
console.log("防抖搜索-优化版本2(接口级别控制)", res);
};
const realSearch3 = debounce(searchWithDebounce3, 500);
const testRealSearch3 = () => {
let i = 0;
let interval = setInterval(() => {
realSearch3(i++);
if (i > 10) {
clearInterval(interval);
}
}, 600);
};
testRealSearch3();
最后
通过上面也可以看出,对于搜索逻辑,功能总是一步一步完善,只有考虑到更多的场景,才能进一步完善我们的程序,有些场景甚至基本不太可能出现,但存在一定的可能,我们就应该考虑到,这能大幅度提升我们的应用体验,减少偶先bug的可能,也是提高我们技术和阅历的机会😄