摘要:所以,我們可以將理解為計時結束是執行任務的必要條件,但是不是任務是否執行的決定性因素。的意思是,必須超過毫秒后,才允許執行。
先來回答一下下面這個問題:對于 setTimeout(function() { console.log("timeout") }, 1000) 這一行代碼,你從哪里可以找到 setTimeout 的源代碼(同樣的問題還會是你從哪里可以看到 setInterval 的源代碼)?
很多時候,可以我們腦子里面閃過的第一個答案肯定是 V8 引擎或者其它 VM們,但是要知道的一點是,所有我們所見過的 Javascript 計時函數,都沒有出現在 ECMAScript 標準中,也沒有被任何 Javascript 引擎實現,計時函數,其實都是由瀏覽器(或者其它運行時,比如 Node.js)實現的,并且,在不同的運行時下,其表現形式有可能都不一致。
在瀏覽器中,主計時器函數是 Window 接口的一部分,這保證了包括如 setTimeout、setInterval 等計時器函數以及其它函數和對象能被全局訪問,這才是你可以隨時隨地使用 setTimeout 的原因。同樣的,在 Node.js 中,setTimeout 是 global 對象的一部分,這拿得你也可以像在瀏覽器里面一樣,隨時隨地的使用它。
到現在可能會有一些人感覺這個問題其實并沒有實際的價值,但是作為一個 Javascript 開發者,如果不知道本質,那么就有可能不能完全的理解 V8 (或者其它VM)是到底是如何與瀏覽器或者 Node.js 相互作用的。
暫緩一個函數的執行計時器函數都是更高階的函數,它們可以用于暫緩一個函數的執行,或者讓一個函數重復執行(由他們的第一個參數執行需要執行的函數)。
下面這是一個暫緩執行的示例:
setTimeout(() => { console.log("距離函數的調用,已經過去 4 秒了") }, 4 * 1000)
在上面的示例中, setTimeout 將 console.log 的執行暫緩了 4 * 1000 毫秒,也就是 4 秒鐘, setTimeout 的第一個函數,就是需要暫緩執行的函數,它是一個函數的引用,下面這個示例是我們更加常見到的寫法:
const fn = () => { console.log("距離函數的調用,已經過去 4 秒了") } setTimeout(fn, 4 * 1000)傳遞參數
如果被 setTimeout 暫緩的函數需要接收參數,我們可以從第三個參數開始添加需要傳遞給被暫緩函數的參數:
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` 清除更加深入了認識取消計時器(Cancel Timers)
上面的示例只是簡單的給我們展現了 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 執行
第 1300 毫秒時,開始執行 readLargeFileSync 這會需要整整 5 秒鐘的時間
第 2 秒的時候,log 執行時間到了,但是當前任務并沒有完成,所以,它不會打印
第 5 秒的時候, 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)結語
標題是寫上了 你需要知道的一切都在這里,但是如果有什么沒有考慮到了,歡迎大家指出。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/106832.html
摘要:瀏覽器是多進程的,而瀏覽器的內核渲染進程是多線程的。如果已經將回調函數放進任務隊列,但是主線程正在執行一個非常耗時的任務,當這個任務執行完畢后,主線程去任務隊列中取任務,這個時候,就會出現連續執行的情況,也就是說相當于失效了。 前言 ??在刷筆試題的時候,經常會碰到setTimeout的問題,只知道這個是設置定時器;但是考察的重點一般是在一個方法中包含了定時器,定時器中的打印和方法中打...
摘要:圖片轉引自的演講和兩個定時器中回調的執行邏輯便是典型的機制。異步編程關于異步編程我的理解是,在執行環境所提供的異步機制之上,在應用編碼層面上實現整體流程控制的異步風格。 問題背景 在一次開發任務中,需要實現如下一個餅狀圖動畫,基于canvas進行繪圖,但由于對于JS運行環境中異步機制的不了解,所以遇到了一個棘手的問題,始終無法解決,之后在與同事交流之后才恍然大悟。問題的根節在于經典的J...
摘要:關于這部分有嚴格的文字定義,但本文的目的是用最小的學習成本徹底弄懂執行機制,所以同步和異步任務分別進入不同的執行場所,同步的進入主線程,異步的進入并注冊函數。宏任務微任務第三輪事件循環宏任務執行結束,執行兩個微任務和。 不論你是javascript新手還是老鳥,不論是面試求職,還是日常開發工作,我們經常會遇到這樣的情況:給定的幾行代碼,我們需要知道其輸出內容和順序。 因為javascr...
摘要:事件完成,回調函數進入。主線程從讀取回調函數并執行。終于執行完了,終于從進入了主線程執行。遇到,立即執行。宏任務微任務第三輪事件循環宏任務執行結束,執行兩個微任務和。事件循環事件循環是實現異步的一種方法,也是的執行機制。 本文的目的就是要保證你徹底弄懂javascript的執行機制,如果讀完本文還不懂,可以揍我。不論你是javascript新手還是老鳥,不論是面試求職,還是日常開發工作...
摘要:所以其實和所謂的異步調用事實上是通過將代碼段插入到代碼的執行隊列中實現的。當執行和的時候,會根據你設定的時間準確地找到代碼的插入點。綜上所述,其實終歸是單線程產物。無論如何異步都不可能突破單線程這個障礙。 發表過一片博客《跟著我用JavaScript寫計時器》,比較基礎.....有網友說應該寫一下setTimeout的原理和機制,嗯,今天就來寫一下吧: 直奔主題:setTimeout和...
閱讀 1241·2021-11-24 09:39
閱讀 386·2019-08-30 14:12
閱讀 2600·2019-08-30 13:10
閱讀 2443·2019-08-30 12:44
閱讀 967·2019-08-29 16:31
閱讀 852·2019-08-29 13:10
閱讀 2443·2019-08-27 10:57
閱讀 3158·2019-08-26 13:57