如何自定义hooks实现接口轮询

1,926 阅读2分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

今天来记录一下业务中遇到的问题,如何实现前端轮询?轮询在前端中并不少见,实现的方案也多种多样,可能一开始都想到用定时器setInterval来实现,每隔多少秒发送一个请求,但是请求也需要时间,所以可能请求回来了还是会发一个请求出去,可能有点问题,但是这么实现也是咩有问题的,也能满足我们的需求。

实际上轮询并不是一个单纯的定时方法,而是依赖前一次返回结果来决定,更像是一个连续的链式调用,所以我们可以通过尾递归来实现,结合我们技术栈是React,所以我们可以通过一个自定义hooks来实现。

思路如下:

  • 我们传入一个请求方法polling,希望返回值是一个Promise,用来await他的结果,根据返回值来判断是否需要轮询,在这我们可以定义一个hasFinshed变量来进行标识
  • 这个自定义hooks返回一个方法doPolling,调用这方法时来轮询这个接口,但我们某些情况下也需要取消轮询,因此我们定义了一个cancelPolling方法
  • 在方法doPolling中定义了一个pollNext方法,用来递归调用我们的请求方法polling
  • 为了实现取消方法,我们定义了isPollingRefcancelRef两个Ref来作为标记,每次轮询前,判断是否取消了轮询,是就直接返回,不轮询,否则进行轮询
  • 为了防止并发场景,多个接口同时轮询可能会产生bug,所以定义了isPollingRef,判断是否正在轮询,是则返回,否则就轮询
  • 增加useUnMount钩子,在组件卸载时取消轮询
// 轮询接口

import { useCallback, useRef } from "react";
import useUnMount from './useUnMount'
import { sleep } from '../utils'

const usePolling = (polling: () => Promise<any>): [doPolling: () => void, cancelPolling: () => void] => {
    const isPollingRef = useRef(false)
    const cancelRef = useRef(false)

    const doPolling = useCallback(() => {
        // 是否正在轮询,是,返回,不能多个接口同时轮询,避免这些接口有先后顺序相互影响
        if (isPollingRef.current) {
            console.log(`[doPolling] isPolling, return immediately`);
            return;
        }
        isPollingRef.current = true;

        const pollNext = async () => {
            // 如果掉了取消轮询,那么就返回不执行
            if (cancelRef.current) {
                isPollingRef.current = false;
                cancelRef.current = false;
                return;
            }
            // 发送请求,返回值组装一下,给个 hasFinshed 判断是否还要继续轮询
            const { hasFinshed } = await polling();
            
            if (!hasFinshed) {
                await sleep(30000);
                pollNext()
            } else {
                isPollingRef.current = false;
            }
        };

        pollNext()
    }, [])

    const cancelPolling = useCallback(() => {
        if (isPollingRef.current) {
            cancelRef.current = true;
        }
    }, [])

    useUnMount(cancelPolling);

    return [doPolling, cancelPolling]
};

export default usePolling;
// 组件卸载时的钩子,componentWillUnMount

import { useEffect } from "react"

const useUnMount = (fn: Function): void => {
    useEffect(() => () => { fn() }, [])
};

export default useUnMount;