[手写系列]setTimeout和clearTimeout模拟setInterval和clearInterval

2,380 阅读3分钟

都说setInterval不好,那么就用setTimeout来模拟一下吧

单纯记录一下常见面试题

setIntervalsetTimeout

setTimeoutsetIntevalwindow对象上两个主要的定时方法, 区别在于setTimeout是定时程序,即在到达指定时间之后执行,回调函数只会执行一次,而setInterval反复执行回调函数,但间隔一段时间。

所以通过setTimeout实现setInterval的本质在于递归调用

const mySetInterval = (callback, duration) => {
	function fn () {
    	callback()  // 执行回调
        setTimeout(()=> {
        	fn() // 在setTimeout内递归调用
        }, duration)
    }
    setTimeout(fn, duration) // 这里不能写fn(),大概只有菜鸡才会明白
}

我们实现了setInterval的功能,但是,这个setInterval没有办法停止运行,会一直执行下去,甚至比之前的setInterval更差了。

所以,我们需要实现clearInterval

clearIntervalclearTimeout

setTimeoutsetInterval在设置好之后,会返回一个定时器的排队序号,我们可以通过该序号,取消已存在的定时器。

let timer1 = setTimeout(() => {
    // 定时器即使清除了,其返回值也不会清除,之后设置定时器的返回值也会在其返回值的基础上继续向后排,
    // 类似于银行的排队领号,即使1号的业务办理完了,后面的人仍是从2号开始继续领号,而不是从1开始。
    clearTimeout(timer1)
  }, 1000)
  
  let timer2 = setInterval(() => {
    // 定时器需要手动清除,并且clearTimeout和clearInterval都可以清除setTimeout或setInterval,但并不建议这样做,容易造成混淆。
    clearInterval(timer2)
  }, 500)
  

由于setTimeoutsetInterval虽然具有不同功能,但其实质都是浏览器的定时器,所以返回的序号是依次排列的。另外,setInterval设置后会有一个固定的返回值,这个代表序号的返回值不变(设置定时器就有返回值,执行多少次是定时器的处理)。

所以,对于setTimeout模拟的setInterval,我们只要拿到了fn内部的定时器序号就可以实现clearInterval的功能。

我们可以通过闭包,维护一个不断更新的timeId

// 创建myTimer对象
const myTimer = (
	function () {
    let timeId // 创建timeId
    
    function mySetInterval (callback, duration) {
    	const preTimeId = timeId
        function fn () {
        	callback() // 执行回调
            // 更新timerId 
            timerId = setTimeout(() => {
            	fn() // 在setTimeout内递归调用
            }, duration)
            // 取消上一次的定时器
            clearTimeout(preTimeId)
        }
        // 第一次设置定时器,并初始化timerId
        timerId = setTimeout(fn, duration)
        // 将timerId返回
        return timerId
    }
    
    function myClearInterval (id) {
    // 调用时传入实时更新的timerId
    	clearTimeout(id)
    }
    
    retrun {
    	setInterval: mySetInterval,
        clearInterval: myClearInterval
    }
})()

现在,我们通过myTimer可以调用mySetIntervalmyClearInterval

let id = myTimer.setInterval(()=>{
   console.log('hi')
}, 500)

setTimeout(() =>{myTimer.clearInterval(id)}, 5000)

但是,定时器并没有如我们相信的停止

因为我们的id,只获得了第一次setTimeout的id,因为我们的idNumber类型,它是timerId值拷贝而非引用,而setTimeoutNumber类的包对象。所以id不能如我们所想的那样,如实时更新timerId的值。

解决办法也很简单,让我们的id引用timerId

let timerId = { id: null}

function mySetInterval (callback, duration) {
    const preTimeId = timeId
    function fn () {
        callback() // 执行回调
        // 更新timerId 
        	timerId.id = setTimeout(() => {
        	fn() // 在setTimeout内递归调用
        }, duration)
        // 取消上一次的定时器
        clearTimeout(preTimeId)
    }
    // 第一次设置定时器,并初始化timerId
    timerId.id = setTimeout(fn, duration)
    // 将timerId返回
    return timerId
    }

function myClearInterval (id) {
   // 调用时传入实时更新的timerId
   clearTimeout(id.id)
}

这样,我们就获得了setTimeout和clearTimeout模拟的setInterval和clearInterval