這個問題其實是前一段時間舍友的一道面試題。我覺得類似用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)
複製程式碼
嗯,沒啥問題,實現了我們想要的功能。。。等一下,怎麼停下來?總不能執行了就不管了吧。。。
clearInterval的實現
平時如果用到了 setInterval
的同學應該都知道 clearInterval
的存在(不然你怎麼停下 interval
呢)。
clearInterval
的用法是 clearInterval(id)
。而這個 id
是 setInterval
的返回值,通過這個 id
值就能夠清除指定的定時器。
const id = setInterval(() => {
// ...
}, 1000)
// ...
clearInterval(id)
複製程式碼
不過你有沒有想到 clearInterval
是如何實現的?回答這個問題之前,我們需要先實現 mySetInterval
的返回值。
mySetInterval的返回值
回到我們簡單版本的 mySetInterval
:
const mySetInterval = (cb, time) => {
const fn = () => {
cb() // 執行傳入的回撥函式
setTimeout(() => {
fn() // 遞迴呼叫自己
}, time)
}
setTimeout(fn, time)
}
複製程式碼
現在它的返回值因為沒有顯示指定,所以是 undefined
。因此第一步,我們先要返回一個 id
出去。
那麼直接 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
的實現了。
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
差異等等。也只是一個拋磚引玉,重點在一步步如何實現。感謝閱讀~