先來回答一下下面這個問題:對於 setTimeout(function() { console.log('timeout') }, 1000)
這一行代碼,你從哪裏能夠找到 setTimeout
的源代碼(一樣的問題還會是你從哪裏能夠看到 setInterval
的源代碼)?php
不少時候,能夠咱們腦子裏面閃過的第一個答案確定是 V8 引擎或者其它 VM們,可是要知道的一點是,全部咱們所見過的 Javascript 計時函數,都沒有出如今 ECMAScript 標準中,也沒有被任何 Javascript 引擎實現,計時函數,其實都是由瀏覽器(或者其它運行時,好比 Node.js)實現的,而且,在不一樣的運行時下,其表現形式有可能都不一致。瀏覽器
在瀏覽器中,主計時器函數是 Window
接口的一部分,這保證了包括如 setTimeout
、setInterval
等計時器函數以及其它函數和對象能被全局訪問,這纔是你能夠隨時隨地使用 setTimeout
的緣由。一樣的,在 Node.js 中,setTimeout
是 global
對象的一部分,這拿得你也能夠像在瀏覽器裏面同樣,隨時隨地的使用它。async
到如今可能會有一些人感受這個問題其實並無實際的價值,可是做爲一個 Javascript 開發者,若是不知道本質,那麼就有可能不能徹底的理解 V8 (或者其它VM)是究竟是如何與瀏覽器或者 Node.js 相互做用的。函數
計時器函數都是更高階的函數,它們能夠用於暫緩一個函數的執行,或者讓一個函數重複執行(由他們的第一個參數執行須要執行的函數)。oop
下面這是一個暫緩執行的示例:post
setTimeout(() => { console.log('距離函數的調用,已通過去 4 秒了') }, 4 * 1000)
在上面的示例中, setTimeout
將 console.log
的執行暫緩了 4 * 1000
毫秒,也就是 4 秒鐘, setTimeout
的第一個函數,就是須要暫緩執行的函數,它是一個函數的引用,下面這個示例是咱們更加常見到的寫法:this
const fn = () => { console.log('距離函數的調用,已通過去 4 秒了') } setTimeout(fn, 4 * 1000)
若是被 setTimeout
暫緩的函數須要接收參數,咱們能夠從第三個參數開始添加須要傳遞給被暫緩函數的參數:code
const fn = (name, gender) => { console.log(`I'm ${name}, I'm a ${gender}`) } setTimeout(fn, 4 * 1000, 'Tao Pan', 'male')
上面的 setTimeout
調用,其結果與下面這樣調用相似:對象
setTimeout(() => { fn('Tao Pan', 'male') }, 4 * 1000)
可是記住,只是結果相似,本質上是不同的,咱們能夠用僞代碼來表示 setTimeout
的函數實現:接口
const setTimeout = (fn, delay, ...args) => { wait(delay) // 這裏表示等待 delay 指定的毫秒數 fn(...args) }
編寫一個函數:
delay
爲 4 秒的時候,打印出:距離函數的調用,已通過去 4 秒了 delay
爲 8 秒的時候,打印出:距離函數的調用,已通過去 8 秒了 delay
爲 N 秒的時候,打印出:距離函數的調用,已通過去 N 秒了 下面這個是個人一個實現:
const delayLog = delay => { setTimeout(console.log, delay * 1000, `距離函數的調用,已通過去 ${delay} 秒了`) } delayLog(4) // 輸出:距離函數的調用,已通過去 4 秒了 delayLog(8) // 輸出:距離函數的調用,已通過去 8 秒了
咱們來理一下 delayLog(4)
的整個執行過程:
delay = 4
setTimeout
執行4 * 1000
毫秒後, setTimeout
調用 console.log
方法setTimeout
計算其第三個參數 距離函數的調用,已通過去 ${delay} 秒了
獲得 距離函數的調用,已通過去 4 秒了
setTimeout
將計算獲得的字符串看成 console.log
的第一個參數console.log('距離函數的調用,已通過去 4 秒了')
執行,輸出結果若是咱們如今要每 4 秒第印一次呢?這裏面就有不少種實現方式了,假如咱們仍是使用 setTimeout
來實現,咱們能夠這樣作:
const loopMessage = delay => { setTimeout(() => { console.log('這裏是由 loopMessage 打印出來的消息') loopMessage(delay) }, delay * 1000) } loopMessage(1) // 此時,每過 1 秒鐘,就會打印出一段消息:*這裏是由 loopMessage 打印出來的消息*
可是這樣有一個問題,就是開始以後,咱們就沒有辦法中止,怎麼辦?能夠稍稍改改實現:
let loopMessageTimer const loopMessage = delay => { loopMessageTimer = setTimeout(() => { console.log('這裏是由 loopMessage 打印出來的消息') loopMessage(delay) }, delay * 1000) } loopMessage(1) clearTimeout(loopMessageTimer) // 咱們隨時均可以使用 `clearTimeout` 清除這個循環
可是這樣仍是有問題的,若是 loopMessage
被調用屢次,那麼他們將共用一個 loopMessageTimer
,清除一個,將清除全部,這是確定不行的,因此,還得再改造一下:
const loopMessage = delay => { let timer const log = () => { timer = setTimeout(() => { console.log(`每 ${delay} 秒打印一次`) log() }, delay * 1000) } log() return () => clearTimeout(timer) } const clearLoopMessage = loopMessage(1) const clearLoopMessage2 = loopMessage(1.5) clearLoopMessage() // 咱們在任什麼時候候均可以取消任何一個重複調用,而不影響其它的
這…… 實現是實現了,可是其它有更好的解決辦法:
const timer = setInterval(console.log, 1000, '每 1 秒鐘打印一次') clearInterval(timer) // 隨時能夠 `clearInterval` 清除
上面的示例只是簡單的給咱們展示了 setTimeout
以及 setInterval
,也看到了,咱們能夠經過 clearTimeout
或者 clearInterval
取消計時器,可是關於計時器,遠遠不止這點知識,請看下面的代碼(請):
const cancelImmediate = () => { const timerId = setTimeout(console.log, 0, '暫緩了 0 秒執行') clearTimeout(timerId) } cancelImmediate() // 這裏並不會有任何輸出
或者看下面這樣的代碼:
const cancelImmediate2 = () => setTimeout(console.log, 0, '暫緩了 0 秒執行') const timerId = cancelImmediate2() clearTimeout(timerId)
請將上面的的任一代碼片斷同時複製到瀏覽器的控制檯中(有多行復制多行)執行,你會發現,兩個代碼片斷都沒有任何輸出,這是爲何?
這是由於,Javascript 的運行機制致使,任什麼時候刻都只能存在一個任務在進行,雖然咱們調用的是暫緩 0 秒,可是,因爲當前的任務尚未執行完成,因此,setTimeout
中被暫緩的函數即便時間到了也不會被執行,必須等到當前的任務徹底執行完成,那麼,再試着,上面的代碼分行復制到控制檯,看看結果是否是會打印出 暫緩了 0 秒執行 了?答案是確定的。
當你一行一行復制執行的時候, cancelImmediate2
執行完成以後,當前任務就已經所有執行完成了,因此開始執行下一個任務(console.log
開始執行)。
從上面的示例中,咱們能夠看出,setTimeout
實際上是將一個任務安排進一個 Javascript 的任務隊列裏面去,當前面的全部任務都執行完成以後,若是這個任務時間到了,那麼就當即執行,不然,繼續等待計時結束。
此時,你應該發現,只要是 setTimeout
所暫緩的函數沒有被執行(任務尚未完成),那麼,咱們就能夠隨時使用 clearTimeout
清除掉這個暫緩(將這條任務從隊列裏面移除)
經過前面的例子,咱們知道了 setTimeout
的 delay
爲 0
時,並不表示立馬就會執行了,它必須等到全部的當前任務(對於一個 JS 文件來說,就是須要執行完當前腳本中的全部調用)執行完成以後都會執行,而這裏面就包括咱們調用的 clearTimeout
。
下面用一個示例來更清楚了說明這個問題:
setTimeout(console.log, 1000, '1 秒後執行的') // 開始時間 const startTime = new Date() // 距離開始時間已通過去幾秒 let secondsPassed = 0 while (true) { // 距離開始時間的毫秒數 const duration = new Date() - startTime // 若是距離開始時間超過 5000 毫秒了, 則終止循環 if (duration > 5000) { break } else { // 若是距離開始時間增加一秒,更新 secondsPassed if (Math.floor(duration / 1000) > secondsPassed) { secondsPassed = Math.floor(duration / 1000) console.log(`已通過去 ${secondsPassed} 秒了。`) } } }
大家猜上面這段代碼會有什麼樣的輸出?是下面這樣的嗎?
1 秒後執行的 已通過去 1 秒了。 已通過去 2 秒了。 已通過去 3 秒了。 已通過去 4 秒了。 已通過去 5 秒了。
並非這樣的,而是下面這樣的:
已通過去 1 秒了。 已通過去 2 秒了。 已通過去 3 秒了。 已通過去 4 秒了。 已通過去 5 秒了。 1 秒後執行的
怎麼會這樣?這是由於 while(true)
這個循環必需要執行超過 5 秒鐘的時間以後,纔算當前全部任務完成,在它 break
以前,其它全部的操做都是沒有用的,固然,咱們不會在開發的過程當中去寫這樣的代碼,可是並不表示就不存在這樣的狀況,想象如下下面這樣的場景:
setTimeout(somethingMustDoAfter1Seconds, 1000) openFileSync('file more then 1gb')
這裏面的 openFileSync
只是一個僞代碼,它表示咱們須要同步進行一個特別費時的操做,這個操做頗有可能會超過 1 秒,甚至更長的時間,可是上面那個 somethingMustDoAfter1Seconds
將一直處於掛起狀態,只要這個操做完成,它纔有可能執行,爲何叫有可能?那是由於,有可能還有別的任務又會佔用資源。因此,咱們能夠將 setTimeout
理解爲:計時結束是執行任務的必要條件,可是不是任務是否執行的決定性因素。
setTimeout(somethingMustDoAfter1Seconds, 1000)
的意思是,必須超過 1000 毫秒後,somethingMustDoAfter1Seconds
才容許執行。
那若是我須要每一秒鐘都打印一句話怎麼辦?從上面的示例中,已經很明顯的看到了,setTimeout
是確定解決不了這個問題了,不信咱們能夠試試下面這個代碼片斷:
const log = (delay) => { timer = setTimeout(() => { console.log(`每 ${delay} 秒打印一次`) log(delay) }, delay * 1000) } log(1)
上面的代碼是沒有任何問題的,在瀏覽器的控制檯觀察,你會發現確實每一秒鐘都打印了一行,可是再試試下面這樣的代碼:
const log = (delay) => { timer = setTimeout(() => { console.log(`每 ${delay} 秒打印一次`) log(delay) }, delay * 1000) } const readLargeFileSync = () => { // 開始時間 const startTime = new Date() // 距離開始時間已通過去幾秒 let secondsPassed = 0 while (true) { // 距離開始時間的毫秒數 const duration = new Date() - startTime // 若是距離開始時間超過 5000 毫秒了, 則終止循環 if (duration > 5000) { break } else { // 若是距離開始時間增加一秒,更新 secondsPassed if (Math.floor(duration / 1000) > secondsPassed) { secondsPassed = Math.floor(duration / 1000) console.log(`已通過去 ${secondsPassed} 秒了。`) } } } } log(1) setTimeout(readLargeFileSync, 1300)
輸出結果是:
每 1 秒打印一次 已通過去 1 秒了。 已通過去 2 秒了。 已通過去 3 秒了。 已通過去 4 秒了。 已通過去 5 秒了。 每 1 秒打印一次
log
執行readLargeFileSync
這會須要整整 5 秒鐘的時間log
執行時間到了,可是當前任務並無完成,因此,它不會打印readLargeFileSync
執行完成了,因此 log
繼續執行關於這個具體怎麼實現,就不在本文討論了
當咱們在一個 function
中調用 this
時,this
關鍵字會指向當前函數的 caller
:
function whoCallsMe() { console.log('My caller is: ', this) }
當咱們在瀏覽器的控制檯中調用 whoCallsMe
時,會打印出 Window
,當在 Node.js 的 REPL 中執行時,會執行出 global
,若是咱們將 whoCallsMe
設置爲一個對象的屬性:
function whoCallsMe() { console.log('My caller is: ', this) } const person = { name: 'Tao Pan', whoCallsMe } person.whoCallsMe()
這會打印出:My caller is: Object { name: "Tao Pan", whoCallsMe: whoCallsMe() }
那麼?
function whoCallsMe() { console.log('My caller is: ', this) } const person = { name: 'Tao Pan', whoCallsMe } setTimeout(person.whoCallsMe, 0)
這會打印出什麼?這個很容易被忽視的問題,其實真的值得咱們去思考。
請直接將上面這個代碼片斷複製進瀏覽器的控制檯,看執行的結果:
My caller is: Window https://pantao.parcmg.com/admin/write-post.php?cid=2952
再打開系統終端,進入 Node.js REPL 中,執行一樣的代碼,看執行結果:
My caller is: Timeout { _idleTimeout: 1, _idlePrev: null, _idleNext: null, _idleStart: 7052, _onTimeout: [Function: whoCallsMe], _timerArgs: undefined, _repeat: null, _destroyed: false, [Symbol(refed)]: true, [Symbol(asyncId)]: 221, [Symbol(triggerId)]: 5 }
回到這句話:當咱們在一個 function
中調用 this
時,this
關鍵字會指向當前函數的 caller
,當咱們使用 setTimeout
時,這個 caller
是跟當前的運行時有關係的,若是我想 this
老是指向 person
對象呢?
function whoCallsMe() { console.log('My caller is: ', this) } const person = { name: 'Tao Pan' } person.whoCallsMe = whoCallsMe.bind(person) setTimeout(person.whoCallsMe, 0)
標題是寫上了 你須要知道的一切都在這裏,可是若是有什麼沒有考慮到了,歡迎你們指出。