yliu

时来天地皆同力,运去英雄不自由

如何处理 loading 闪烁


8049f458-b600-429c-99fc-115002d30588

图片来源于踏青随手一拍 考虑一个场景,在使用 userequest hooks 的时候经常会返回 {data, loading, error} 结构,为了交互更好,通常会对 loading 字段做一些处理,例如在小程序中可能会调用 showLoading 来告知用户正在请求中。

在 loading 为 true 调用

wx.showLoading({
  mask: true,
  title: "正在请求中...",
});

在 loading 为 false 关闭

wx.hideLoading();

但是如果请求速度很快,用户可能就看到一个 loading 图标一闪而过,这种体验就非常不好。

下面就来探讨下如何避免这种情况发生,通常一个 loading 会重复经历以下两个阶段:

  1. 初始情况为 false
  2. 在完成时候为 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 触发太频繁等。

简单概括下就是:

  1. 初始情况下设置 newLoadingfalse
  2. 在 watch 内部注册防抖函数,每次 watch 的时候调用
  3. 根据防抖机制,在时间内会取消定时器不会执行
  4. 超出防抖时间,给 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: "正在提交中..." });

最后

如果文章有错误或者不对之处欢迎指出。