用setTimeout模拟setInterVal有什么优点。那让我们来学习一下几种定时器吧。

844 阅读5分钟

用setTimeout模拟setInterVal到底隐藏了哪些知识点。

浏览器的事件循环机制。

什么是浏览器的事件循环机制(Event loop)呢?我们知道js是单线程的语言。如果是同步执行的话,那么如果碰到一些耗时任务的话。会阻塞线程,导致页面的渲染受阻。这是我们不愿意看到的。于是浏览器就有一个事件循环机制。将事件分为同步任务和异步任务。同步任务或根据代码编写的顺序依次执行。而异步任务也就是一些耗时任务不会立即执行而是放进一个任务队列,等待执行。异步任务又分为宏任务(MacroTask)和微任务(MicroTask)。

常见的宏任务有setTimeout,setInterVal,I/O操作等。常见的微任务有Promise.then(),、MutationObserver(用于监听 DOM 变化的回调)、process.nextTick

这些任务在浏览器中怎么执行呢?先执行同步代码,碰到微任务进微任务队列,碰到宏任务进宏任务队列。等同步代码执行完之后,微任务出队列,微任务全部出队完之后,宏任务出队执行。当宏任务中有同步代码,微任务,宏任务。再按照这种执行顺序循环,这就是事件循环机制。那我们下面的定时器就是宏任务啦,当作一个小小的知识点去进行补充。

各种定时器的用法

  • setTimeout 来看看mdn的解释。Window 接口的 setTimeout()  方法设置一个定时器,一旦定时器到期,就会执行一个函数或指定的代码片段。它接受三种参数,一个时定时器到时间后执行的代码片段,一个设定的时间,还有各种传递给执行代码片段的各种参数。

用法

setTimeout(functionRef, delay, param1, param2, /* …, */ paramN)

它的返回值timeoutID是一个整数。可以传递给clearTimeout()来取消该定时器。

  • setInterVal Window 接口的 setInterval()  方法重复调用一个函数或执行一个代码片段,在每次调用之间具有固定的时间间隔。

此方法返回一个间隔 ID,其唯一地标识时间间隔,因此你可以稍后通过调用 clearInterval() 来移除它。

它的用法跟setTimeout差不多,只不过是周期性的执行这个函数或代码片段。那我们用setTimeout模拟setInterval就可以考虑递归调用setTimeout。

用setTimeout模拟setInterVal

我们来分析一下,setTimeout是延迟一段时间后执行回调。而setInterVal是周期性的执行回调。那么我们可以考虑递归的调用setTimeout。确保每一次setTimeout执行完之后,再执行另一个setTimeout。

    function MySetInterVal(callback,time){
        function loop(){
            setTimeout(()=>{
                callback()
                loop()
            },time)
        }
        loop()
    }

这样一个简单的setInterVal就完成啦。你问我能不能运,废话,那当然可以运行啦。不能运你来找我。

闭包及其成因和作用。

看到上面的实现,到现在就完成了嘛。实则不然。还可以优化嘛。清除定时器还没写。这个就需要用到闭包啦。闭包不会写了白搭。闭包一会,事半功倍。

  • 什么是闭包 闭包是指有权访问另一个函数作用域中的变量的函数。换句话说,闭包是一个函数,它能够记住并访问它的词法作用域(lexical scope),即使这个函数是在其词法作用域之外执行的。

闭包具有以下几个特点:

  1. 函数嵌套函数:闭包通常是通过在一个函数内部创建另一个函数来实现的。
  2. 内部函数可以访问外部函数的变量:内部函数可以访问其外部函数作用域中的变量,即使外部函数已经执行完毕。
  3. 参数和变量不会被回收:由于闭包的存在,外部函数的参数和变量不会被垃圾回收机制回收,即使外部函数已经执行完毕。

我们来看一个例子

function outerFunction(){ 

let outerVariable = 'I am outside!'; 
function innerFunction() { 
console.log(outerVariable); 
} 
return innerFunction; 
} 
const closure = outerFunction(); 
closure(); // 输出: I am outside!

从这个例子当中可以看到innerFunction就是闭包。由于它用到了它的外部函数outerFunction里面的变量。所以当外部函数执行完之后,变量不会被清除。而且能被闭包访问到。这样做的好处我们可以明显的看到,本来变量应该被回收掉的,但是没有回收,延长了变量的使用周期。创建私有变量和方法。

缺点

  • 内存泄漏:由于闭包会使得变量的值始终保持在内存中,如果不小心管理闭包,可能会导致内存泄漏问题。
  • 性能影响:闭包的使用可能会增加内存使用量和执行时间,影响程序的性能。

好了,既然我们已经清楚的了解了闭包,那么就可以利用闭包来完善我们的MySetInterval来创建一个私有函数清除定时器。不多说开干。先复制一份上面的代码。

     function MySetInterVal(callback,time){
         let timer = null
        function loop(){
            timer = setTimeout(()=>{
                callback()
                loop()
            },time)
        }
        loop()
        return ()=>{
            clearTimeout(timer)
        }
    }

you see 我们创建了一个闭包作为这个函数的返回值,可以访问到外层函数执行留下的timer也就是定时器执行完后的ID。执行这个函数就可以清除定时器啦。

截屏2024-12-25 22.54.10.png

可以看到我们的清除函数成功的执行了。写到这就完了嘛,我看到有小伙伴在我其他文章留言,函数的附加参数这个功能还没实现呢?那我们得再加一个参数,传给我们的callback回调。再复制一份上面的代码。

     function MySetInterVal(callback,time,...params){
         let timer = null
        function loop(){
            timer = setTimeout(()=>{
                callback(...params)
                loop()
            },time)
        }
        loop(...params)
        return ()=>{
            clearTimeout(timer)
        }
    }

然后再加上剩余参数。可以看到我们的手写就差不多收官了。

截屏2024-12-25 23.13.05.png 正常运行,好了家人们。就此,这个手写setInterval也就告一段落了。