這個問題實際上是前一段時間舍友的一道面試題。我以爲相似用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
差別等等。也只是一個拋磚引玉,重點在一步步如何實現。感謝閱讀~