字节跳动二面遇到的两道setTimeout相关问题,记录一下

217 阅读2分钟

使用 setTimeout 模拟 setInterval

使用 setTimeout 模拟 setInterval,实现如下功能:

const intervalID = setInterval(cb, interval)
clearInterval(intervalID)

首先想到方案:

function _setInterval(cb, interval) {
    let intervalID;
    const recur = () => {
        cb();
        // 闭包更新intervalID
        intervalID = setTimeout(() => {
            // 递归调用自己
            recur();
        }, interval);
    };
    intervalID = setTimeout(recur, interval);
    return intervalID;
}
function _clearInterval(intervalID) {
    clearTimeout(intervalID);
}

本方案存在问题:执行 _setInterval 的时候返回的 intervalID 依然不是最新的 intervalID

改进方案:

let _timer = {};
function _setInterval(cb, interval) {
    let timeoutID = parseInt(Math.random() * 10) + 1;
    const recur = () => {
        cb();
        // 闭包更新intervalID
        _timer[timeoutID] = setTimeout(() => {
            // 递归调用自己
            recur();
        }, interval);
    };
    _timer[timeoutID] = setTimeout(recur, interval);
    return timeoutID;
}
function _clearInterval(intervalID) {
    clearTimeout(_timer[intervalID]);
    delete _timer[intervalID];
}

注:setInterval返回值intervalID是一个非零数值,用来标识通过setInterval()创建的计时器,这个值可以用来作为clearInterval()的参数来清除对应的计时器

若全局变量intervalID定义为Number类型,虽然每次递归会更新intervalID的值,但是_clearInterval只能取到intervalID的值拷贝而不是引用,导致clearTimeout无法清理最新的定时器, 故将intervalID定义为Object类型let _timer = {}_setInterval返回intervalID作为_timerkey,每次setTimeout更新的是_timer[timeoutID]的引用。这样,调用 _clearInterval 虽然获取到的 intervalID 是不变的,但是可以通过引用关系 _timer[timeoutID] 获取到 setTimeout 返回的最新timeoutID,使用clearTimeout清除即可

注:const intervalID = setInterval(cb, interval); clearInterval(intervalID); 这里需要返回一个 intervalID 的引用,但是intervalID又只能是正整数,只能采取重写 valueOf 的方式实现

不使用全局变量方案如下:

function _setInterval(cb, interval, ...args) {
    const _timer = {
        value: -1,
        valueOf: function () {
            return this.value;
        },
    };

    const recur = () => {
        _timer.value = setTimeout(recur, interval);
        cb.apply(this, args);
    };
    _timer.value = setTimeout(recur, interval);
    return _timer;
}
function _clearInterval(intervalID) {
    clearTimeout(intervalID);
}

实现自定义Hook useTimeout

import React, { useEffect, useState } from "react";

const App = () => {
    const [count, setCount] = useState(0);
    const [timeoutCount, setTimeoutCount] = useState(0);
    
    useEffect(() => {
        setTimeout(()=> {
            setTimeoutCount(count)
        }, 3000)
        setCount(5);
    }, []);
    
    return (
        <div>
            <div>Count: {count}</div>
            <div>timeoutCount: {timeoutCount}</div>
        </div>
    );
};

第一问:渲染结果

Count: 5
timeoutCount: 0

第二问:实现自定义Hook useTimeout(cb, delay) 实现 3000ms后,timeoutCount更新为count值

// useTimeout

import { useEffect, useRef } from "react";

function useTimeout(cb, timer, deps = []) {
    const callbackRef = useRef(cb);
    useEffect(() => {
        if (timer < 0 || typeof callbackRef.current !== "function") return;
        callbackRef.current = cb;
        const timeId = setTimeout(() => {
            callbackRef.current()
        }, timer);
        return () => clearTimeout(timeId);
    }, deps);
}

export default useTimeout;

import React, { useEffect, useState } from "react";
import useTimeout from "./useTimeout";

const App = () => {
    const [count, setCount] = useState(0);
    const [timeoutCount, setTimeoutCount] = useState(0);
    
    useTimeout(() => {
        setTimeoutCount(count)
    }, 3000, [count])
    
    useEffect(() => {
        setCount(5);
    }, []);
    
    return (
        <div>
            <div>Count: {count}</div>
            <div>timeoutCount: {timeoutCount}</div>
        </div>
    );
};

参考文章

用setTimeout和clearTimeout简单实现setInterval与clearInterval

第 133 题:用 setTimeout 实现 setInterval,阐述实现的效果与setInterval的差异

React Hooks 奇技淫巧 —— 副作用, 闭包 与 Timer

再学 React Hooks (一):闭包陷阱