用 setTimeout 代替 setInterval

4,771 阅读3分钟

setInterval 准吗

只能确保回调函数不会再指定的事件间隔之前运行,但可能会在那个时刻运行,也可能在那之后运行,要根据事件队列的状态而定。——《你不知道的 JavaScript》 p143

用 setTimeout 代替 setInterval

  • 为什么要替代?

根据事件循环的执行机制,就会知道 setInterval 是等待 call stack 为空的时候,才会执行回调,如果前面的代码执行太久了,超出了给 setInterval 设定的时间间隔,此时回调函数已经进入队列,等 stack 终于为空的时候就会立即执行队列中的回调函数,这时候 web APIs 中的 setInterval 计时也已经在计时了一段时间了,很快又会把回调函数放入队列,就会发现怎么上一次执行完后,没到以为的事件间隔又执行了······· 另外,如果 setInterval 本身给定的回调函数执行的时间比设定的时间间隔长,也会带来这样的问题,失去了设置时间间隔的意义。

  • 如何代替?
思路

可以参考这个视频,思路说得很详细JavaScript用setTimeout模拟实现setInterval ,不过视频中没有说如何清除定时器,下面记录下实现清除定时器的思考过程。

设计接口:调用方式和 setInterval 一样 使用:timer = mySetInterval(fn, delay) 清除:myClearInterval(timer)

// 方法一(这种不好想清除定时器,因为不好设置全局的 timer,视频中没有说这个方法)
function mySetInterval(fn, delay) {
    setTimeout(() => {
        console.log(new Date().toLocaleString()) // 可以打印时间看看
        fn()
        mySetInterval(fn, delay)
    }, delay)
}

// 测试
function testmySetInterval() {
    console.log('testmySetInterval')
}

mySetInterval(testmySetInterval, 1000)

方法一不好清除定时器,因为 mySetInterval 肯定要返回一个 timer 才能清除对不对?来看方法二。

// 方法二(视频的方法,没有写怎么清除定时器)
function mySetInterval(fn, delay) {
    function inside() {
        console.log(new Date().toLocaleString()) // 可以打印时间看看
        fn()
        setTimeout(inside, delay)
    }
    setTimeout(inside, delay)
}

// 开始想:
// 假如 mySetInterval 需要返回一个 timer,因为使用方式是 timer = mySetInterval(fn, delay),这样一写 timer 就被固定了对不对? 
function mySetInterval(fn, delay) {
    let timer = null
    function inside() {
        clearTimeout(timer)
        fn()
        timer = setTimeout(inside, delay)
    }
    timer = setTimeout(inside, delay)
    return timer  // timer = mySetInterval(fn, delay) 的时候 timer 被固定
}

// mySetInterval 只调用了一次,这样的直接返回的永远都是第一个 setTimeout 的 timer。如何让 timer 不固定呢?对象!返回一个对象 clearTimeout() 作为属性值返回!属性值是个方法,清除定时器,就是让myClearInterval 调用这个方法!这样写:
function mySetInterval(fn, delay) {
    let timer = null
    function inside() {
        console.log(new Date().toLocaleString()) // 打印看看时间
        clearTimeout(timer) // 把上一次的 timer 掉,这里使用了闭包, inside 访问了不属于自己作用域的变量,也就是 mySetInterval 下的 timer
        fn()
        timer = setTimeout(inside, delay)
    }
    timer = setTimeout(inside, delay)
    return { // 返回一个对象 clearTimeout() 作为属性值返回!
        clear() {
            clearTimeout(timer)
        }
    }
}

// 清除定时器
function myClearInterval(flagTimer) {
    flagTimer.clear()
}

// 测试
function testmySetInterval() {
    console.log('testmySetInterval')
}
const timer = mySetInterval(testmySetInterval, 1000)
// 控制台直接调用 myClearInterval(timer)
封装
function mySetInterval(fn, delay) {
    let timer = null
    function inside() {
        console.log(new Date().toLocaleString()) // 打印看看时间
        clearTimeout(timer)
        fn()
        timer = setTimeout(inside, delay)
    }
    timer = setTimeout(inside, delay)
    return {
        clear() {
            clearTimeout(timer)
        }
    }
}

// 清除定时器
function myClearInterval(flagTimer) {
    flagTimer.clear()
}

// 测试
function testmySetInterval() {
    console.log('testmySetInterval')
}
const timer = mySetInterval(testmySetInterval, 1000)
// 控制台直接调用 myClearInterval(timer)

requestAnimationFrame 代替定时器

const RAF = {
      intervalTimer: null,
      timeoutTimer: null,
      setTimeout(cb, interval) { // 实现setTimeout功能
        let now = Date.now
        let stime = now()
        let etime = stime
        let loop = () => {
          this.timeoutTimer = requestAnimationFrame(loop)
          etime = now()
          if (etime - stime >= interval) {
            cb()
            cancelAnimationFrame(this.timeoutTimer)
          }
        }
        this.timeoutTimer = requestAnimationFrame(loop)
        return this.timeoutTimer
      },
      clearTimeout() {
        cancelAnimationFrame(this.timeoutTimer)
      },
      setInterval(cb, interval) { // 实现setInterval功能
        let now = Date.now
        let stime = now()
        let etime = stime
        let loop = () => {
          this.intervalTimer = requestAnimationFrame(loop)
          etime = now()
          if (etime - stime >= interval) {
            stime = now()
            etime = stime
            cb()
          }
        }
        this.intervalTimer = requestAnimationFrame(loop)
        return this.intervalTimer
      },
      clearInterval() {
        cancelAnimationFrame(this.intervalTimer)
      }
    }

    let count = 0
    function a() {
      console.log(count)
      count++
    }
    RAF.setTimeout(a, 1000)