如何处理 loading 闪烁
图片来源于踏青随手一拍 考虑一个场景,在使用
userequest
hooks 的时候经常会返回{data, loading, error}
结构,为了交互更好,通常会对 loading 字段做一些处理,例如在小程序中可能会调用showLoading
来告知用户正在请求中。
在 loading 为 true
调用
wx.showLoading({
mask: true,
title: "正在请求中...",
});
在 loading 为 false
关闭
wx.hideLoading();
但是如果请求速度很快,用户可能就看到一个 loading 图标一闪而过,这种体验就非常不好。
下面就来探讨下如何避免这种情况发生,通常一个 loading 会重复经历以下两个阶段:
- 初始情况为
false
- 在完成时候为
true
发生 loading 闪烁则是因为步骤 2 距离步骤 1 很近,所以核心点就是处理步骤 2 - 步骤 1 时间的过程。
延长 loading
最简单的方式就是给步骤 2 和步骤 1 设置一个延迟,让其只有超过一定时间才能触发。
根据这个思路很容易写下一版代码
let time = Date.now();
let id = null;
const newLoading = ref(false);
watch(
() => loading,
(value) => {
if (value) {
newLoading.value = true;
time = Date.now();
return;
}
// 关闭的时候判断
clearInterval(id);
if (Date.now() - time < 1000) {
id = setTimeout(() => {
newLoading.value = false;
}, Date.now() - time);
return;
}
newLoading.value = false;
// ...
}
);
在初始的时候给时间戳,在要关闭的时候进行判断,如果小于规定时间就直接返回不做处理。
但是这种体验就太差了,明明我的网速很快还要让我看到 loading 展示
产品:你头伸出来我保证不打死你
防抖
延长 loading 时间肯定是行不通的,但是仔细分析一下上面的步骤,发现 loading 抖动其实是因为触发时间太快,这个场景很容易想到可以使用防抖来解决。
防抖:一定时间内如果频繁触发则不会执行,常见使用防抖的场景有 input 输入太快,或者 resize 触发太频繁等。
简单概括下就是:
- 初始情况下设置
newLoading
为false
- 在 watch 内部注册防抖函数,每次 watch 的时候调用
- 根据防抖机制,在时间内会取消定时器不会执行
- 超出防抖时间,给
newLoading
设置loading
值
防抖函数有很多具体的实现,这里就不过多介绍,具体看一下 antd spin 组件 是如何实现的。
// 隐藏无关代码
import { debounce } from "throttle-debounce";
function shouldDelay(spinning?: boolean, delay?: number): boolean {
return !!spinning && !!delay && !isNaN(Number(delay));
}
const Spin: SpinType = (props) => {
const { spinning: customSpinning = true, delay = 0 } = props;
// 这段代码的意思是判断 delay 是否存在,如果 customSpinning 为true,但是指定了delay则初始必须为false
const [spinning, setSpinning] = React.useState<boolean>(
() => customSpinning && !shouldDelay(customSpinning, delay)
);
React.useEffect(() => {
if (customSpinning) {
const showSpinning = debounce(delay, () => {
setSpinning(true);
});
showSpinning();
return () => {
showSpinning?.cancel?.();
};
}
setSpinning(false);
}, [delay, customSpinning]);
return spinning && <div key="loading">{spinElement}</div>;
};
export default Spin;
根据上面的思路写一版 vue3 的 hooks
import { onBeforeUnmount, ref, toValue, watch, watchEffect } from "vue";
import { debounce } from "lodash-es";
function shouldDelay(spinning, delay) {
return !!spinning && !!delay && !isNaN(Number(delay));
}
export const useShowLoading1 = (loading, options = {}) => {
const { title = "加载数据中...", delay = 200 } = options;
const spinning = ref(
toValue(loading) && !shouldDelay(toValue(loading), delay)
);
let debounced;
// 防止闪烁
watch(
loading,
() => {
debounced?.cancel();
debounced = debounce(() => {
spinning.value = toValue(loading);
}, delay);
debounced();
},
{ immediate: true, flush: "post" }
);
// 组件卸载调用
onBeforeUnmount(() => {
debounced?.cancel();
});
// 观察真正的loading变化
watchEffect(() => {
if (spinning.value) {
return uni.showLoading({
mask: true,
title,
});
}
uni.hideLoading().catch(() => {});
});
};
使用
const { run, loading } = useRequest(toRealName, {
manual: true,
onSuccess: async () => {
refresh();
uni.navigateBack({
delta: 1,
});
},
});
useShowLoading(loading, { title: "正在提交中..." });
最后
如果文章有错误或者不对之处欢迎指出。