这个问题实际上是前一段时间舍友的一道面试题。我以为相似用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)
复制代码
嗯,没啥问题,实现了咱们想要的功能。。。等一下,怎么停下来?总不能执行了就无论了吧。。。测试
平时若是用到了 setInterval
的同窗应该都知道 clearInterval
的存在(否则你怎么停下 interval
呢)。ui
clearInterval
的用法是 clearInterval(id)
。而这个 id
是 setInterval
的返回值,经过这个 id
值就可以清除指定的定时器。spa
const id = setInterval(() => {
// ...
}, 1000)
// ...
clearInterval(id)
复制代码
不过你有没有想到 clearInterval
是如何实现的?回答这个问题以前,咱们须要先实现 mySetInterval
的返回值。code
回到咱们简单版本的 mySetInterval
:cdn
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
是第一个 setTimeout
的 id
,然而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
}
复制代码
可是这样有个问题,因为 timeId
是Number
类型,当咱们这样使用的时候:
const id = mySetInterval(() => { // 此处id是Number类型,是值的拷贝而不是引用
console.log(new Date())
}, 1000)
setTimeout(() => { // 2秒后清除定时器
clearTimeout(id)
}, 2000)
复制代码
因为 id
是 Number
类型,咱们拿到的是全局变量 timeId
的值拷贝而不是引用,因此上面那段代码依然无效。不过咱们已经能够经过全局变量 timeId
来清除计时器了:
setTimeout(() => { // 2秒后清除定时器
clearTimeout(timeId) // 全局变量 timeId
}, 2000)
复制代码
可是上面的实现,不只与咱们平时使用的 clearInterval
的用法有所出入,而且因为 timeId
是一个 Number
类型的变量,致使同一时刻全局只能有一个 mySetInterval
的 id
存在,也即没法作到清除多个 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
里的一个键的内容。
咱们每次更新 setTimeout
的 id
并非去更新 timeId
,相应的,咱们去更新 timeMap[timeId]
里的值。
这样实现后,咱们调用 mySetInterval
虽然获取到的 timeId
是不变的,可是咱们经过 timeMap[timeId]
获取到的真正的 setTimeout
的 id
值是会一直更新的。
另外为了保证 timeId
的惟一性,在这里我简单用了一个自增的全局变量 id
来保证惟一。
好了,id
值有了,剩下的就是 myClearInterval
的实现了。
因为咱们的 mySetInterval
返回的 timeId
并非真正的 setTimeout
返回的 id
,因此并不能简单地经过 clearTimeout(timeId)
来清除计时器。
不过其实原理也是很相似的,咱们只要能拿到真正的 id
就好了:
const myClearInterval = (id) => {
clearTimeout(timeMap[id]) // 经过timeMap[id]获取真正的id
delete timeMap[id]
}
复制代码
测试一下:
没毛病~
至此咱们就用 setTimeout
和 clearTimeout
简单实现了 setInterval
与clearInterval
。固然本文说的是简单实现,毕竟还有一些东西没有完成,好比setTimeout
的 args
参数、Node和浏览器端的 setTimeout
差别等等。也只是一个抛砖引玉,重点在一步步如何实现。感谢阅读~