定时器轮询优化:封装一个hook,实现在上一个请求结束后1s再执行下一个请求

422 阅读3分钟

一、故事的开始

晚上领导在工作群里发了一张华为云服务器的性能监控图:

e0fb81d5148cec7f22a517270d4d88f.jpg

二、主因

我们的系统在多人同时使用的情况下短时间内出现了服务器撑不住的情况,这是因为在监控的业务中前端轮询了一个性能很差的后端接口

但在极端情况下,我认为前后端都有自己应该兜底的场景:

  1. 弱网环境下、用户频繁操作、大数据量等情况下的页面展示前端需要做兜底,不能出现不合理的表现,更不能接受页面崩溃。
  2. 而面对高并发的请求,显然后端应该做兜底,出现服务器崩溃是比较大的事故。

三、解决方案和分析

方案:

后端短时间内搞定不了,那只能前端在轮询方面做一下优化,目标是让上次的接口请求完成(fulfilled/rejected)后再进行下一次请求

分析:

原来代码使用vueuse的useIntervalFn来做轮询,这个hook的好处是提供了现成的重启、停止的方法来控制定时器,另外不用担心页面销毁时定时器未被移除: image.png

如果我做大幅度的改动和业务代码耦合在一起,风险比较大

  1. 可能会忘记移除定时器;
  2. 可能不小心对原来的业务代码进行了删减;
  3. 耦合在业务中,命令式得实现这个需求会让代码上下跨度比较大,易导致逻辑混乱,且可维护性差。

尝试解决:

coding的思路是,封装一个和useIntervalFn用法一样的hook,但在动手之前我需要找一下vueuse是否有现成的,这样会节省时间,找了一圈没有。(我甚至后面还找vueuse的官方成员远方大佬确认了一下确实没有)

实现了一版:

1f9a33bb5052594ba0d791e6c1d0e81.png

最后完善:

经过单元测试发现,当快速连续执行resume时,会短时间内存在多个计时器,review代码发现当快速resume时,clean和fn先后同步执行,但是下一个timer要等待fn完成后才会被赋值,所以clean不能及时清除上一个计时器:

image.png

解决方法就是每次resume时记录最新的resumeId,finally的回调中只对最新的resumeId进行定时器的赋值:

image.png

另外还做了其他完善,比如hook的delay参数可以是一个getter或者ref。

最后,业务代码中的改动几乎就只是将useIntervalFn改成useDelayIntervalFn

image.png

完整代码:

import { MaybeRefOrGetter, toValue } from "@vueuse/core";
import _ from "lodash";
import { isRef, onUnmounted, ref, watch } from "vue";


interface UseDelayIntervalFnOptions {
    immediateCallback?: boolean;
}
export function useDelayIntervalFn(
    fn: () => Promise<any>,
    delay: MaybeRefOrGetter<number> = 1000,
    options: UseDelayIntervalFnOptions
) {
    const { immediateCallback = false } = options;

    let timer: ReturnType<typeof setTimeout> | null = null;
    const isActive = ref(false);

    function clean() {
        if (timer) {
            clearInterval(timer);
            timer = null;
        }
    }

    function pause() {
        isActive.value = false;
        clean();
    }

    let resumeId = "";
    let resume = () => {
        let tempResumeId = _.uniqueId();
        resumeId = tempResumeId;

        const intervalValue = toValue(delay);
        if (intervalValue <= 0) return;

        isActive.value = true;
        clean();
        fn().finally(() => {
            // 只执行最近的一次resume的回调
            if (tempResumeId !== resumeId) return;

            timer = setTimeout(() => {
                if (!isActive.value) return;
                resume();
            }, intervalValue);
        });
    };

    if (immediateCallback) {
        resume();
    }

    let stopWatch = () => { };
    if (isRef(delay) || typeof delay === "function") {
        stopWatch = watch(delay, () => {
            if (isActive.value) resume();
        });
    }

    onUnmounted(() => {
        pause();
        stopWatch();
    });

    return {
        isActive,
        resume,
        pause,
    };
}

如发现代码实现有问题,欢迎指点!