用setTimeout和clearTimeout简单实现setInterval与clearInterval

这个问题实际上是前一段时间舍友的一道面试题。我以为相似用reduce实现map、用xxx实现yyy的题目其实都挺有意思,考察融会贯通的本领。不过相比之下这道题可能更有实际意义。好比咱们常常会用 setTimeout 来实现倒计时。下面来讲说我对这个问题的思考。面试

简单版本

首先咱们先用 setTimeout 实现一个简单版本的 setInterval浏览器

setInterval 须要不停循环调用,这让咱们想到了递归调用自身:闭包

const mySetInterval = (cb, time) => {
  const fn = () => {
    cb() // 执行传入的回调函数
    setTimeout(() => {
      fn() // 递归调用本身
    }, time)
  }
  setTimeout(fn, time)
}
复制代码

让咱们来写段代码测试一下:函数

mySetInterval(() => {
  console.log(new Date())
}, 1000)
复制代码

setTimeout-1

嗯,没啥问题,实现了咱们想要的功能。。。等一下,怎么停下来?总不能执行了就无论了吧。。。测试

clearInterval的实现

平时若是用到了 setInterval 的同窗应该都知道 clearInterval 的存在(否则你怎么停下 interval 呢)。ui

clearInterval 的用法是 clearInterval(id)。而这个 idsetInterval的返回值,经过这个 id 值就可以清除指定的定时器。spa

const id = setInterval(() => {
  // ...
}, 1000)
// ...
clearInterval(id)
复制代码

不过你有没有想到 clearInterval 是如何实现的?回答这个问题以前,咱们须要先实现 mySetInterval 的返回值。code

mySetInterval的返回值

回到咱们简单版本的 mySetIntervalcdn

const mySetInterval = (cb, time) => {
  const fn = () => {
    cb() // 执行传入的回调函数
    setTimeout(() => {
      fn() // 递归调用本身
    }, time)
  }
  setTimeout(fn, time)
}
复制代码

如今它的返回值由于没有显示指定,因此是 undefined。所以第一步,咱们先要返回一个 id 出去。blog

那么直接 return setTimeout(fn, time) 能够吗?由于咱们知道 setTimeout 也会返回一个id,那么初步构想就是经过 setTimeout 返回的 id,而后调用 clearTimeout(id) 来实现咱们的 myClearInterval

以下:

const mySetInterval = (cb, time) => {
  const fn = () => {
    cb() // 执行传入的回调函数
    setTimeout(() => { // 第二个、第三个...
      fn() // 递归调用本身
    }, time)
  }
  return setTimeout(fn, time) // 第一个setTimeout
}

const id = mySetInterval(() => {
  console.log(new Date())
}, 1000)

setTimeout(() => { // 2秒后清除定时器
  clearTimeout(id)
}, 2000)
复制代码

这显然是不行的。由于 mySetInterval 返回的 id 是第一个 setTimeoutid,然而2秒后,要 clearTimeout 时,递归执行的第二个、第三个 setTimeout 等等的 id 已经再也不是第一个 id 了。所以此时没法清除。

因此咱们须要每次执行 setTimeout的时候把新的 id 存下来。怎么存?咱们应该会想到用闭包:

const mySetInterval = (cb, time) => {
  let timeId
  const fn = () => {
    cb() // 执行传入的回调函数
    timeId = setTimeout(() => { // 闭包更新timeId
      fn() // 递归调用本身
    }, time)
  }
  timeId = setTimeout(fn, time) // 第一个setTimeout
  return timeId
}
复制代码

很不错,到这步咱们已经可以将 timeId 进行更新了。不过还有问题,那就是执行 mySetInterval 的时候返回的 id 依然不是最新的 timeId。由于 timeId 只在 fn 内部被更新了,在外部并不知道它的更新。那有什么办法让 timeId 的更新也让外部知道呢?

有的,答案就是用全局变量。

let timeId // 全局变量
const mySetInterval = (cb, time) => {
  const fn = () => {
    cb() // 执行传入的回调函数
    timeId = setTimeout(() => { // 闭包更新timeId
      fn() // 递归调用本身
    }, time)
  }
  timeId = setTimeout(fn, time) // 第一个setTimeout
  return timeId
}
复制代码

可是这样有个问题,因为 timeIdNumber类型,当咱们这样使用的时候:

const id = mySetInterval(() => { // 此处id是Number类型,是值的拷贝而不是引用
  console.log(new Date())
}, 1000)

setTimeout(() => { // 2秒后清除定时器
  clearTimeout(id)
}, 2000)
复制代码

因为 idNumber 类型,咱们拿到的是全局变量 timeId 的值拷贝而不是引用,因此上面那段代码依然无效。不过咱们已经能够经过全局变量 timeId 来清除计时器了:

setTimeout(() => { // 2秒后清除定时器
  clearTimeout(timeId) // 全局变量 timeId
}, 2000)
复制代码

可是上面的实现,不只与咱们平时使用的 clearInterval 的用法有所出入,而且因为 timeId 是一个 Number 类型的变量,致使同一时刻全局只能有一个 mySetIntervalid 存在,也即没法作到清除多个 mySetInterval 的计时器。

因此咱们须要一种类型,既能支持多个 timeId 存在,又能实现 mySetInterval 返回的 id 可以被咱们的 myClearInterval 使用。你应该能想到,咱们要用一个全局的 Object 来作。

修改代码以下:

let timeMap = {}
let id = 0 // 简单实现id惟一
const mySetInterval = (cb, time) => {
  let timeId = id // 将timeId赋予id
  id++ // id 自增实现惟一id
  let fn = () => {
    cb()
    timeMap[timeId] = setTimeout(() => {
      fn()
    }, time)
  }
  timeMap[timeId] = setTimeout(fn, time)
  return timeId // 返回timeId
}
复制代码

咱们的 mySetInterval 依然返回了一个 id 值。只不过这个 id 值是全局变量 timeMap 里的一个键的内容。

咱们每次更新 setTimeoutid 并非去更新 timeId,相应的,咱们去更新 timeMap[timeId] 里的值。

这样实现后,咱们调用 mySetInterval 虽然获取到的 timeId 是不变的,可是咱们经过 timeMap[timeId] 获取到的真正的 setTimeoutid 值是会一直更新的。

另外为了保证 timeId 的惟一性,在这里我简单用了一个自增的全局变量 id 来保证惟一。

好了,id 值有了,剩下的就是 myClearInterval 的实现了。

myClearInterval实现

因为咱们的 mySetInterval 返回的 timeId 并非真正的 setTimeout 返回的 id ,因此并不能简单地经过 clearTimeout(timeId) 来清除计时器。

不过其实原理也是很相似的,咱们只要能拿到真正的 id 就好了:

const myClearInterval = (id) => {
  clearTimeout(timeMap[id]) // 经过timeMap[id]获取真正的id
  delete timeMap[id]
}
复制代码

测试一下:

没毛病~

至此咱们就用 setTimeoutclearTimeout 简单实现了 setIntervalclearInterval。固然本文说的是简单实现,毕竟还有一些东西没有完成,好比setTimeoutargs 参数、Node和浏览器端的 setTimeout 差别等等。也只是一个抛砖引玉,重点在一步步如何实现。感谢阅读~

相关文章
相关标签/搜索