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