用setTimeout和clearTimeout簡單實現setInterval與clearInterval

Molunerfinn發表於2019-05-08

這個問題其實是前一段時間舍友的一道面試題。我覺得類似用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 呢)。

clearInterval 的用法是 clearInterval(id)。而這個 idsetInterval的返回值,通過這個 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 是第一個 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]
}
複製程式碼

測試一下:

用setTimeout和clearTimeout簡單實現setInterval與clearInterval

沒毛病~

至此我們就用 setTimeoutclearTimeout 簡單實現了 setIntervalclearInterval。當然本文說的是簡單實現,畢竟還有一些東西沒有完成,比如setTimeoutargs 引數、Node和瀏覽器端的 setTimeout 差異等等。也只是一個拋磚引玉,重點在一步步如何實現。感謝閱讀~

相關文章