用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 差別等等。也只是一個拋磚引玉,重點在一步步如何實現。感謝閱讀~

相關文章
相關標籤/搜索