定时器的技术方案

2025-3-29

前言

项目需求中,如果遇到需要使用定时触发某个任务的时候,我们一般会使用 setTimeout 或者 setInterval 来实现,但是这两种方法都有一些问题,比如 setTimeout 在某些情况下可能会因为浏览器的渲染导致任务执行时间不准确,setInterval 在某些情况下可能会因为任务执行时间过长导致任务堆积,所以我们需要一种更可靠的方式来处理定时任务。

第一个版本

如果直接使用setInterval,这个方法是最笨的方案,会出现越来越不精准情况

我们可以想到最简便的优化方案就是每次执行完毕之后重新在执行一次写一个递归即可,这样能够缓解一点随着任务执行时间边长,任务堆积的问题

function loopIntervalHandler() {
    let interval;

    function execute() {
        console.log('...');
        clearInterval(interval);
        interval = setInterval(execute, 1000);
    }

    interval = setInterval(execute, 1000);

    return () => {
        if (interval) {
            clearInterval(interval);
        }
    };
}

const stopLoop = loopIntervalHandler()

document.addEventListener('beforeunload', () => {
    stopLoop()
})

第二个版本

通过使用 requestAnimationFrame 来实现定时任务,其实 requestAnimationFrame 这个就单纯该问题解决方案和第一个优化版本一样,本质都是通过递归执行,本质还是并没有改变随着任务执行,任务堆积问题,只是缓解与第一版一样。如果一定要说一个特别的地方就是可以通过 requestAnimationFrame 来解决页面不可见的时候,定时任务不执行的问题, requestAnimationFrame 天然支持。


function loop(fn, interval = 1000) {
    let lastTime = performance.now();
    let requestInstance

    function animate(currentTime) {
        if (currentTime - lastTime >= interval) {
            fn();
            lastTime = currentTime;
        }
        requestInstance = requestAnimationFrame(animate);
    }

    requestInstance = requestAnimationFrame(animate);

    return () => {
        if (requestInstance) {
            cancelAnimationFrame(requestInstance);
            requestInstance = null
        }
    }
}

function animationHandler() {
    console.log("execute...")
}

const stop = loop(animationHandler, 1000); 

document.addEventListener('beforeunload', () => {
    stop()
})

第三个版本

js是单线程的,采用web Work技术方案来把定时任务放到web worker中执行,这样就不会影响到主线程的渲染,但是web worker中不能使用DOM操作,所以这个方案只适用于一些不涉及DOM操作的任务。


<button>stop</button>

<script>
const worker = new Worker('./work.js');
let lastTime
worker.onmessage = (event) => {
    let diffTime = event.data - lastTime
    lastTime = event.data
    console.log(diffTime)
};

worker.onerror = (error) => {
    console.error('Worker 错误:', error);
};
lastTime = performance.now()
worker.postMessage('start');

// 如果需要停止 Worker
const stopWorker = () => {
    worker.postMessage('stop')
    worker.terminate();
}

document.addEventListener('DOMContentLoaded', () => {
    const btn = document.querySelector('button')
    btn?.addEventListener('click', () => {
        stopWorker()
    })
})
</script>

work.js脚本内容


let timer = null;

self.onmessage = (event) => {
    if (event.data === 'start') {
        if (timer) {
            clearInterval(timer);
        }
        
        timer = setInterval(() => {
            self.postMessage(performance.now());
        }, 1000);
    } else if (event.data === 'stop') {
        if (timer) {
            clearInterval(timer);
            timer = null;
        }
    }
};

结尾

以上都是我能想到的一些解决方案,如果还有更优的方案,欢迎补充交流。