摘要:所以,我們可以將理解為計時結(jié)束是執(zhí)行任務(wù)的必要條件,但是不是任務(wù)是否執(zhí)行的決定性因素。的意思是,必須超過毫秒后,才允許執(zhí)行。
先來回答一下下面這個問題:對于 setTimeout(function() { console.log("timeout") }, 1000) 這一行代碼,你從哪里可以找到 setTimeout 的源代碼(同樣的問題還會是你從哪里可以看到 setInterval 的源代碼)?
很多時候,可以我們腦子里面閃過的第一個答案肯定是 V8 引擎或者其它 VM們,但是要知道的一點是,所有我們所見過的 Javascript 計時函數(shù),都沒有出現(xiàn)在 ECMAScript 標(biāo)準(zhǔn)中,也沒有被任何 Javascript 引擎實現(xiàn),計時函數(shù),其實都是由瀏覽器(或者其它運行時,比如 Node.js)實現(xiàn)的,并且,在不同的運行時下,其表現(xiàn)形式有可能都不一致。
在瀏覽器中,主計時器函數(shù)是 Window 接口的一部分,這保證了包括如 setTimeout、setInterval 等計時器函數(shù)以及其它函數(shù)和對象能被全局訪問,這才是你可以隨時隨地使用 setTimeout 的原因。同樣的,在 Node.js 中,setTimeout 是 global 對象的一部分,這拿得你也可以像在瀏覽器里面一樣,隨時隨地的使用它。
到現(xiàn)在可能會有一些人感覺這個問題其實并沒有實際的價值,但是作為一個 Javascript 開發(fā)者,如果不知道本質(zhì),那么就有可能不能完全的理解 V8 (或者其它VM)是到底是如何與瀏覽器或者 Node.js 相互作用的。
暫緩一個函數(shù)的執(zhí)行計時器函數(shù)都是更高階的函數(shù),它們可以用于暫緩一個函數(shù)的執(zhí)行,或者讓一個函數(shù)重復(fù)執(zhí)行(由他們的第一個參數(shù)執(zhí)行需要執(zhí)行的函數(shù))。
下面這是一個暫緩執(zhí)行的示例:
setTimeout(() => { console.log("距離函數(shù)的調(diào)用,已經(jīng)過去 4 秒了") }, 4 * 1000)
在上面的示例中, setTimeout 將 console.log 的執(zhí)行暫緩了 4 * 1000 毫秒,也就是 4 秒鐘, setTimeout 的第一個函數(shù),就是需要暫緩執(zhí)行的函數(shù),它是一個函數(shù)的引用,下面這個示例是我們更加常見到的寫法:
const fn = () => { console.log("距離函數(shù)的調(diào)用,已經(jīng)過去 4 秒了") } setTimeout(fn, 4 * 1000)傳遞參數(shù)
如果被 setTimeout 暫緩的函數(shù)需要接收參數(shù),我們可以從第三個參數(shù)開始添加需要傳遞給被暫緩函數(shù)的參數(shù):
const fn = (name, gender) => { console.log(`I"m ${name}, I"m a ${gender}`) } setTimeout(fn, 4 * 1000, "Tao Pan", "male")
上面的 setTimeout 調(diào)用,其結(jié)果與下面這樣調(diào)用類似:
setTimeout(() => { fn("Tao Pan", "male") }, 4 * 1000)
但是記住,只是結(jié)果類似,本質(zhì)上是不一樣的,我們可以用偽代碼來表示 setTimeout 的函數(shù)實現(xiàn):
const setTimeout = (fn, delay, ...args) => { wait(delay) // 這里表示等待 delay 指定的毫秒數(shù) fn(...args) }挑戰(zhàn)一下
編寫一個函數(shù):
當(dāng) delay 為 4 秒的時候,打印出:距離函數(shù)的調(diào)用,已經(jīng)過去 4 秒了
當(dāng) delay 為 8 秒的時候,打印出:距離函數(shù)的調(diào)用,已經(jīng)過去 8 秒了
當(dāng) delay 為 N 秒的時候,打印出:距離函數(shù)的調(diào)用,已經(jīng)過去 N 秒了
下面這個是我的一個實現(xiàn):
const delayLog = delay => { setTimeout(console.log, delay * 1000, `距離函數(shù)的調(diào)用,已經(jīng)過去 ${delay} 秒了`) } delayLog(4) // 輸出:距離函數(shù)的調(diào)用,已經(jīng)過去 4 秒了 delayLog(8) // 輸出:距離函數(shù)的調(diào)用,已經(jīng)過去 8 秒了
我們來理一下 delayLog(4) 的整個執(zhí)行過程:
delay = 4
setTimeout 執(zhí)行
4 * 1000 毫秒后, setTimeout 調(diào)用 console.log 方法
setTimeout 計算其第三個參數(shù) 距離函數(shù)的調(diào)用,已經(jīng)過去 ${delay} 秒了 得到 距離函數(shù)的調(diào)用,已經(jīng)過去 4 秒了
setTimeout 將計算得到的字符串當(dāng)作 console.log 的第一個參數(shù)
console.log("距離函數(shù)的調(diào)用,已經(jīng)過去 4 秒了") 執(zhí)行,輸出結(jié)果
規(guī)律性重復(fù)一個函數(shù)的執(zhí)行以及停止重復(fù)調(diào)用如果我們現(xiàn)在要每 4 秒第印一次呢?這里面就有很多種實現(xiàn)方式了,假如我們還是使用 setTimeout 來實現(xiàn),我們可以這樣做:
const loopMessage = delay => { setTimeout(() => { console.log("這里是由 loopMessage 打印出來的消息") loopMessage(delay) }, delay * 1000) } loopMessage(1) // 此時,每過 1 秒鐘,就會打印出一段消息:*這里是由 loopMessage 打印出來的消息*
但是這樣有一個問題,就是開始之后,我們就沒有辦法停止,怎么辦?可以稍稍改改實現(xiàn):
let loopMessageTimer const loopMessage = delay => { loopMessageTimer = setTimeout(() => { console.log("這里是由 loopMessage 打印出來的消息") loopMessage(delay) }, delay * 1000) } loopMessage(1) clearTimeout(loopMessageTimer) // 我們隨時都可以使用 `clearTimeout` 清除這個循環(huán)
但是這樣還是有問題的,如果 loopMessage 被調(diào)用多次,那么他們將共用一個 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() // 我們在任何時候都可以取消任何一個重復(fù)調(diào)用,而不影響其它的
這…… 實現(xiàn)是實現(xiàn)了,但是其它有更好的解決辦法:
const timer = setInterval(console.log, 1000, "每 1 秒鐘打印一次") clearInterval(timer) // 隨時可以 `clearInterval` 清除更加深入了認(rèn)識取消計時器(Cancel Timers)
上面的示例只是簡單的給我們展現(xiàn)了 setTimeout 以及 setInterval,也看到了,我們可以通過 clearTimeout 或者 clearInterval 取消計時器,但是關(guān)于計時器,遠(yuǎn)遠(yuǎn)不止這點知識,請看下面的代碼(請):
const cancelImmediate = () => { const timerId = setTimeout(console.log, 0, "暫緩了 0 秒執(zhí)行") clearTimeout(timerId) } cancelImmediate() // 這里并不會有任何輸出
或者看下面這樣的代碼:
const cancelImmediate2 = () => setTimeout(console.log, 0, "暫緩了 0 秒執(zhí)行") const timerId = cancelImmediate2() clearTimeout(timerId)
請將上面的的任一代碼片段同時復(fù)制到瀏覽器的控制臺中(有多行復(fù)制多行)執(zhí)行,你會發(fā)現(xiàn),兩個代碼片段都沒有任何輸出,這是為什么?
這是因為,Javascript 的運行機制導(dǎo)致,任何時刻都只能存在一個任務(wù)在進行,雖然我們調(diào)用的是暫緩 0 秒,但是,由于當(dāng)前的任務(wù)還沒有執(zhí)行完成,所以,setTimeout 中被暫緩的函數(shù)即使時間到了也不會被執(zhí)行,必須等到當(dāng)前的任務(wù)完全執(zhí)行完成,那么,再試著,上面的代碼分行復(fù)制到控制臺,看看結(jié)果是不是會打印出 暫緩了 0 秒執(zhí)行 了?答案是肯定的。
當(dāng)你一行一行復(fù)制執(zhí)行的時候, cancelImmediate2 執(zhí)行完成之后,當(dāng)前任務(wù)就已經(jīng)全部執(zhí)行完成了,所以開始執(zhí)行下一個任務(wù)(console.log 開始執(zhí)行)。
從上面的示例中,我們可以看出,setTimeout 其實是將一個任務(wù)安排進一個 Javascript 的任務(wù)隊列里面去,當(dāng)前面的所有任務(wù)都執(zhí)行完成之后,如果這個任務(wù)時間到了,那么就立即執(zhí)行,否則,繼續(xù)等待計時結(jié)束。
此時,你應(yīng)該發(fā)現(xiàn),只要是 setTimeout 所暫緩的函數(shù)沒有被執(zhí)行(任務(wù)還沒有完成),那么,我們就可以隨時使用 clearTimeout 清除掉這個暫緩(將這條任務(wù)從隊列里面移除)
計時器是沒有任何保證的通過前面的例子,我們知道了 setTimeout 的 delay 為 0 時,并不表示立馬就會執(zhí)行了,它必須等到所有的當(dāng)前任務(wù)(對于一個 JS 文件來講,就是需要執(zhí)行完當(dāng)前腳本中的所有調(diào)用)執(zhí)行完成之后都會執(zhí)行,而這里面就包括我們調(diào)用的 clearTimeout。
下面用一個示例來更清楚了說明這個問題:
setTimeout(console.log, 1000, "1 秒后執(zhí)行的") // 開始時間 const startTime = new Date() // 距離開始時間已經(jīng)過去幾秒 let secondsPassed = 0 while (true) { // 距離開始時間的毫秒數(shù) const duration = new Date() - startTime // 如果距離開始時間超過 5000 毫秒了, 則終止循環(huán) if (duration > 5000) { break } else { // 如果距離開始時間增長一秒,更新 secondsPassed if (Math.floor(duration / 1000) > secondsPassed) { secondsPassed = Math.floor(duration / 1000) console.log(`已經(jīng)過去 ${secondsPassed} 秒了。`) } } }
你們猜上面這段代碼會有什么樣的輸出?是下面這樣的嗎?
1 秒后執(zhí)行的 已經(jīng)過去 1 秒了。 已經(jīng)過去 2 秒了。 已經(jīng)過去 3 秒了。 已經(jīng)過去 4 秒了。 已經(jīng)過去 5 秒了。
并不是這樣的,而是下面這樣的:
已經(jīng)過去 1 秒了。 已經(jīng)過去 2 秒了。 已經(jīng)過去 3 秒了。 已經(jīng)過去 4 秒了。 已經(jīng)過去 5 秒了。 1 秒后執(zhí)行的
怎么會這樣?這是因為 while(true) 這個循環(huán)必須要執(zhí)行超過 5 秒鐘的時間之后,才算當(dāng)前所有任務(wù)完成,在它 break 之前,其它所有的操作都是沒有用的,當(dāng)然,我們不會在開發(fā)的過程中去寫這樣的代碼,但是并不表示就不存在這樣的情況,想象以下下面這樣的場景:
setTimeout(somethingMustDoAfter1Seconds, 1000) openFileSync("file more then 1gb")
這里面的 openFileSync 只是一個偽代碼,它表示我們需要同步進行一個特別費時的操作,這個操作很有可能會超過 1 秒,甚至更長的時間,但是上面那個 somethingMustDoAfter1Seconds 將一直處于掛起狀態(tài),只要這個操作完成,它才有可能執(zhí)行,為什么叫有可能?那是因為,有可能還有別的任務(wù)又會占用資源。所以,我們可以將 setTimeout 理解為:計時結(jié)束是執(zhí)行任務(wù)的必要條件,但是不是任務(wù)是否執(zhí)行的決定性因素。
setTimeout(somethingMustDoAfter1Seconds, 1000) 的意思是,必須超過 1000 毫秒后,somethingMustDoAfter1Seconds 才允許執(zhí)行。
再來一個小挑戰(zhàn)那如果我需要每一秒鐘都打印一句話怎么辦?從上面的示例中,已經(jīng)很明顯的看到了,setTimeout 是肯定解決不了這個問題了,不信我們可以試試下面這個代碼片段:
const log = (delay) => { timer = setTimeout(() => { console.log(`每 ${delay} 秒打印一次`) log(delay) }, delay * 1000) } log(1)
上面的代碼是沒有任何問題的,在瀏覽器的控制臺觀察,你會發(fā)現(xiàn)確實每一秒鐘都打印了一行,但是再試試下面這樣的代碼:
const log = (delay) => { timer = setTimeout(() => { console.log(`每 ${delay} 秒打印一次`) log(delay) }, delay * 1000) } const readLargeFileSync = () => { // 開始時間 const startTime = new Date() // 距離開始時間已經(jīng)過去幾秒 let secondsPassed = 0 while (true) { // 距離開始時間的毫秒數(shù) const duration = new Date() - startTime // 如果距離開始時間超過 5000 毫秒了, 則終止循環(huán) if (duration > 5000) { break } else { // 如果距離開始時間增長一秒,更新 secondsPassed if (Math.floor(duration / 1000) > secondsPassed) { secondsPassed = Math.floor(duration / 1000) console.log(`已經(jīng)過去 ${secondsPassed} 秒了。`) } } } } log(1) setTimeout(readLargeFileSync, 1300)
輸出結(jié)果是:
每 1 秒打印一次 已經(jīng)過去 1 秒了。 已經(jīng)過去 2 秒了。 已經(jīng)過去 3 秒了。 已經(jīng)過去 4 秒了。 已經(jīng)過去 5 秒了。 每 1 秒打印一次
第一秒的時候, log 執(zhí)行
第 1300 毫秒時,開始執(zhí)行 readLargeFileSync 這會需要整整 5 秒鐘的時間
第 2 秒的時候,log 執(zhí)行時間到了,但是當(dāng)前任務(wù)并沒有完成,所以,它不會打印
第 5 秒的時候, readLargeFileSync 執(zhí)行完成了,所以 log 繼續(xù)執(zhí)行
關(guān)于這個具體怎么實現(xiàn),就不在本文討論了最終,到底是誰在調(diào)用那個被暫緩的函數(shù)?
當(dāng)我們在一個 function 中調(diào)用 this 時,this 關(guān)鍵字會指向當(dāng)前函數(shù)的 caller:
function whoCallsMe() { console.log("My caller is: ", this) }
當(dāng)我們在瀏覽器的控制臺中調(diào)用 whoCallsMe 時,會打印出 Window,當(dāng)在 Node.js 的 REPL 中執(zhí)行時,會執(zhí)行出 global,如果我們將 whoCallsMe 設(shè)置為一個對象的屬性:
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)
這會打印出什么?這個很容易被忽視的問題,其實真的值得我們?nèi)ニ伎肌?/p>
請直接將上面這個代碼片段復(fù)制進瀏覽器的控制臺,看執(zhí)行的結(jié)果:
My caller is: Window https://pantao.parcmg.com/admin/write-post.php?cid=2952
再打開系統(tǒng)終端,進入 Node.js REPL 中,執(zhí)行同樣的代碼,看執(zhí)行結(jié)果:
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 }
回到這句話:當(dāng)我們在一個 function 中調(diào)用 this 時,this 關(guān)鍵字會指向當(dāng)前函數(shù)的 caller,當(dāng)我們使用 setTimeout 時,這個 caller 是跟當(dāng)前的運行時有關(guān)系的,如果我想 this 總是指向 person 對象呢?
function whoCallsMe() { console.log("My caller is: ", this) } const person = { name: "Tao Pan" } person.whoCallsMe = whoCallsMe.bind(person) setTimeout(person.whoCallsMe, 0)結(jié)語
標(biāo)題是寫上了 你需要知道的一切都在這里,但是如果有什么沒有考慮到了,歡迎大家指出。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/106832.html
摘要:瀏覽器是多進程的,而瀏覽器的內(nèi)核渲染進程是多線程的。如果已經(jīng)將回調(diào)函數(shù)放進任務(wù)隊列,但是主線程正在執(zhí)行一個非常耗時的任務(wù),當(dāng)這個任務(wù)執(zhí)行完畢后,主線程去任務(wù)隊列中取任務(wù),這個時候,就會出現(xiàn)連續(xù)執(zhí)行的情況,也就是說相當(dāng)于失效了。 前言 ??在刷筆試題的時候,經(jīng)常會碰到setTimeout的問題,只知道這個是設(shè)置定時器;但是考察的重點一般是在一個方法中包含了定時器,定時器中的打印和方法中打...
摘要:圖片轉(zhuǎn)引自的演講和兩個定時器中回調(diào)的執(zhí)行邏輯便是典型的機制。異步編程關(guān)于異步編程我的理解是,在執(zhí)行環(huán)境所提供的異步機制之上,在應(yīng)用編碼層面上實現(xiàn)整體流程控制的異步風(fēng)格。 問題背景 在一次開發(fā)任務(wù)中,需要實現(xiàn)如下一個餅狀圖動畫,基于canvas進行繪圖,但由于對于JS運行環(huán)境中異步機制的不了解,所以遇到了一個棘手的問題,始終無法解決,之后在與同事交流之后才恍然大悟。問題的根節(jié)在于經(jīng)典的J...
摘要:關(guān)于這部分有嚴(yán)格的文字定義,但本文的目的是用最小的學(xué)習(xí)成本徹底弄懂執(zhí)行機制,所以同步和異步任務(wù)分別進入不同的執(zhí)行場所,同步的進入主線程,異步的進入并注冊函數(shù)。宏任務(wù)微任務(wù)第三輪事件循環(huán)宏任務(wù)執(zhí)行結(jié)束,執(zhí)行兩個微任務(wù)和。 不論你是javascript新手還是老鳥,不論是面試求職,還是日常開發(fā)工作,我們經(jīng)常會遇到這樣的情況:給定的幾行代碼,我們需要知道其輸出內(nèi)容和順序。 因為javascr...
摘要:事件完成,回調(diào)函數(shù)進入。主線程從讀取回調(diào)函數(shù)并執(zhí)行。終于執(zhí)行完了,終于從進入了主線程執(zhí)行。遇到,立即執(zhí)行。宏任務(wù)微任務(wù)第三輪事件循環(huán)宏任務(wù)執(zhí)行結(jié)束,執(zhí)行兩個微任務(wù)和。事件循環(huán)事件循環(huán)是實現(xiàn)異步的一種方法,也是的執(zhí)行機制。 本文的目的就是要保證你徹底弄懂javascript的執(zhí)行機制,如果讀完本文還不懂,可以揍我。不論你是javascript新手還是老鳥,不論是面試求職,還是日常開發(fā)工作...
摘要:所以其實和所謂的異步調(diào)用事實上是通過將代碼段插入到代碼的執(zhí)行隊列中實現(xiàn)的。當(dāng)執(zhí)行和的時候,會根據(jù)你設(shè)定的時間準(zhǔn)確地找到代碼的插入點。綜上所述,其實終歸是單線程產(chǎn)物。無論如何異步都不可能突破單線程這個障礙。 發(fā)表過一片博客《跟著我用JavaScript寫計時器》,比較基礎(chǔ).....有網(wǎng)友說應(yīng)該寫一下setTimeout的原理和機制,嗯,今天就來寫一下吧: 直奔主題:setTimeout和...
閱讀 1253·2021-11-24 09:39
閱讀 391·2019-08-30 14:12
閱讀 2605·2019-08-30 13:10
閱讀 2449·2019-08-30 12:44
閱讀 974·2019-08-29 16:31
閱讀 860·2019-08-29 13:10
閱讀 2455·2019-08-27 10:57
閱讀 3169·2019-08-26 13:57