摘要:?jiǎn)尉€(xiàn)程的話(huà),如果我們做一些的操作比如說(shuō)這是一個(gè)耗時(shí)的操所那么在這將近一秒內(nèi),線(xiàn)程就會(huì)被阻塞,無(wú)法繼續(xù)執(zhí)行下面的任務(wù)。事件循環(huán)的主要機(jī)制就是任務(wù)隊(duì)列機(jī)制一個(gè)事件循環(huán)有一個(gè)或者多個(gè)任務(wù)隊(duì)列。
瀏覽器中的事件循環(huán)機(jī)制
網(wǎng)上一搜事件循環(huán), 很多文章標(biāo)題的前面會(huì)加上 JavaScript, 但是我覺(jué)得事件循環(huán)機(jī)制跟 JavaScript 沒(méi)什么關(guān)系, JavaScript 只是一門(mén)解釋型語(yǔ)言, 方便開(kāi)發(fā)和理解的, 由V8 JIT將 JavaScript 編譯成機(jī)器語(yǔ)言來(lái)調(diào)用底層, 至于瀏覽器怎么執(zhí)行 JavaScript 代碼, JavaScript 管不著也不關(guān)心. 因此, “JavaScript事件循環(huán)機(jī)制”這種說(shuō)法是不合理的. 事件循環(huán)機(jī)制是由運(yùn)行時(shí)環(huán)境實(shí)現(xiàn)的, 具體來(lái)說(shuō)有瀏覽器、Node等. 這篇文章就先來(lái)說(shuō)說(shuō)瀏覽器中實(shí)現(xiàn)的事件循環(huán)機(jī)制.
正文首先,javascript 在瀏覽器端運(yùn)行是單線(xiàn)程的,這是由瀏覽器決定的,這是為了避免多線(xiàn)程執(zhí)行不同任務(wù)會(huì)發(fā)生沖突的情況。也就是說(shuō)我們寫(xiě)的javascript 代碼只在一個(gè)線(xiàn)程上運(yùn)行,稱(chēng)之為主線(xiàn)程(HTML5提供了web worker API可以讓瀏覽器開(kāi)一個(gè)線(xiàn)程運(yùn)行比較復(fù)雜耗時(shí)的 javascript任務(wù),但是這個(gè)線(xiàn)程仍受主線(xiàn)程的控制)。單線(xiàn)程的話(huà),如果我們做一些“sleep”的操作比如說(shuō):
var now = + new Date() while (+new Date() <= now + 1000){ //這是一個(gè)耗時(shí)的操所 }
那么在這將近一秒內(nèi),線(xiàn)程就會(huì)被阻塞,無(wú)法繼續(xù)執(zhí)行下面的任務(wù)。
還有些操作比如說(shuō)獲取遠(yuǎn)程數(shù)據(jù)、I/O操作等,他們都很耗時(shí),如果采用同步的方式,那么進(jìn)程在執(zhí)行這些操作時(shí)就會(huì)因?yàn)楹臅r(shí)而等待,就像上面那樣,下面的任務(wù)也只能等待,這樣效率并不高。
那瀏覽器是怎么做的呢?
我們找到WHATWG規(guī)范對(duì)Event loop的介紹:
為了協(xié)調(diào)事件,用戶(hù)交互,腳本,渲染,網(wǎng)絡(luò)等,用戶(hù)代理必須使用事件循環(huán)。
事件循環(huán)的主要機(jī)制就是任務(wù)隊(duì)列機(jī)制:
一個(gè)事件循環(huán)有一個(gè)或者多個(gè)任務(wù)隊(duì)列(task queues)。任務(wù)隊(duì)列是task的有序列表,task是調(diào)度Events,Parsing,Callbacks,Using a resource,Reacting to DOM manipulation這些任務(wù)的算法;
每個(gè)任務(wù)都來(lái)自一個(gè)特定的任務(wù)源(task source)(比如鼠標(biāo)鍵盤(pán)事件)。來(lái)自同一個(gè)特定任務(wù)源且屬于特定事件循環(huán)的任務(wù)必須被加入到同一個(gè)任務(wù)隊(duì)列中,來(lái)自不同任務(wù)源的任務(wù)可以放在不同的任務(wù)隊(duì)列中;
瀏覽器調(diào)用這些隊(duì)列中的任務(wù)時(shí)采取這樣的做法: 相同隊(duì)列中的任務(wù)按照先進(jìn)先出的順序, 不同的隊(duì)列按照提前設(shè)置的隊(duì)列優(yōu)先級(jí)來(lái)調(diào)用. 例如,用戶(hù)代理可以有一個(gè)用于鼠標(biāo)和鍵盤(pán)事件的任務(wù)隊(duì)列(用戶(hù)交互任務(wù)源),另一個(gè)用于其他任務(wù)。然后,用戶(hù)代理75%概率調(diào)用鍵盤(pán)和鼠標(biāo)事件任務(wù)隊(duì)列,25%調(diào)用其他隊(duì)列, 這樣的話(huà)就保持界面響應(yīng)而且不會(huì)餓死其他任務(wù)隊(duì)列. 但是相同隊(duì)列中的任務(wù)要按照先進(jìn)先出的順序。也就是說(shuō)多帶帶的任務(wù)隊(duì)列中的任務(wù)總是按先進(jìn)先出的順序執(zhí)行,但是不保證多個(gè)任務(wù)隊(duì)列中的任務(wù)優(yōu)先級(jí),具體實(shí)現(xiàn)可能會(huì)交叉執(zhí)行
在調(diào)用任務(wù)的過(guò)程中, 會(huì)產(chǎn)生新的任務(wù), 瀏覽器就會(huì)不斷執(zhí)行任務(wù), 因此稱(chēng)為事件循環(huán).
microtask queue 微任務(wù)隊(duì)列
還有一些特殊任務(wù), 它們不會(huì)被放在task queues中, 會(huì)放在一個(gè)叫做microtask(微任務(wù)) queue中, 繼續(xù)看標(biāo)準(zhǔn):
Each event loop has a microtask queue. A microtask is a task that is originally to be queued on the microtask queue rather than a task queue.
任務(wù)隊(duì)列可以有多個(gè), 但是微任務(wù)隊(duì)列只有一個(gè).
那么哪些任務(wù)是放在task queue, 哪些放在microtask queue呢? 通常對(duì)瀏覽器和Node.js來(lái)說(shuō):
macrotask(宏任務(wù)): script(整體代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering等
microtask(微任務(wù)): process.nextTick, Promises(這里指瀏覽器實(shí)現(xiàn)的原生 Promise), Object.observe, MutationObserver等
請(qǐng)尤其注意macrotask中執(zhí)行整體代碼也是一個(gè)宏任務(wù)
事件循環(huán)處理過(guò)程
總體來(lái)說(shuō), 瀏覽器端事件循環(huán)的一個(gè)回合(go-around或者叫cycle)就是:
從macrotask隊(duì)列中(task queue)取一個(gè)宏任務(wù)執(zhí)行, 執(zhí)行完后, 取出所有的microtask執(zhí)行.
重復(fù)回合
無(wú)論在執(zhí)行macrotask還是microtask, 都有可能產(chǎn)生新的macrotask或者microtask, 就這樣繼續(xù)執(zhí)行.
用任務(wù)隊(duì)列機(jī)制解釋異步操作順序
這里有一些常見(jiàn)異步操作:
const interval = setInterval(() => { console.log("setInterval") }, 0) setTimeout(() => { console.log("setTimeout 1") Promise.resolve().then(() => { console.log("promise 3") }).then(() => { console.log("promise 4") }).then(() => { setTimeout(() => { console.log("setTimeout 2") Promise.resolve().then(() => { console.log("promise 5") }).then(() => { console.log("promise 6") }).then(() => { clearInterval(interval) }) }, 0) }) }, 0) Promise.resolve().then(() => { console.log("promise 1") }).then(() => { console.log("promise 2") })
結(jié)果(Chrome 63.0.3239.84; Mac OS):
promise 1 promise 2 setInterval setTimeout 1 promise 3 promise 4 setInterval // 大部分情況下2次, 少數(shù)情況下一次 setTimeout 2 promise 5 promise 6
這個(gè)順序是如何得來(lái)的?
我們先講promise 4后面只出現(xiàn)一次setInterval的情況, 畫(huà)個(gè)圖簡(jiǎn)單表示一下這個(gè)過(guò)程:
注意本圖為了方便把各時(shí)間段(Cycle)隊(duì)列的任務(wù)都畫(huà)在隊(duì)列中去了, 實(shí)際上執(zhí)行一個(gè)task 和 microtask 后就會(huì)把這個(gè)任務(wù)從相應(yīng)隊(duì)列中刪除
首先, 主任務(wù)就是執(zhí)行腳本, 也就是執(zhí)行上述代碼, 這也是一個(gè)task. 在執(zhí)行代碼過(guò)程中, 遇到setTimeout、setInterval 就會(huì)將回調(diào)函數(shù)添加到task queue中, 遇到 promise 就會(huì)將then回調(diào)添加到 microtask 中去.
Task執(zhí)行完, 接著取所有 microtask 執(zhí)行, 所有microtask 執(zhí)行完了, microtask queue也就空了, 接著再取task執(zhí)行, 如果microtask queue為空, 沒(méi)有任務(wù), 則繼續(xù)取下一個(gè)task執(zhí)行, 就這樣循環(huán)執(zhí)行. 圖中箭頭就表示執(zhí)行的順序.
那么為什么promise 4后面大部分情況下出現(xiàn)2次setInterval, 少數(shù)情況出現(xiàn)1次呢?
我猜測(cè)這是因?yàn)閟etInterval是有最短間隔時(shí)間的(chrome下4ms左右), 這個(gè)時(shí)間不同機(jī)子、不同瀏覽器都有可能不一樣. 代碼中的參數(shù)是0, 意味著盡可能短的時(shí)間內(nèi)就會(huì)產(chǎn)生一個(gè)task加入到 task queue中. 瀏覽器在執(zhí)行setInterval后到執(zhí)行下一個(gè)task前, 時(shí)間間隔就可能超過(guò)這個(gè)最短時(shí)間, 因此會(huì)產(chǎn)生一個(gè)setInterval task.
我是這樣論證的:
我把含有promise5、promise6回調(diào)函數(shù)的setTimeout的時(shí)間設(shè)置大一點(diǎn), 讓它推遲插入task queue中:
... setTimeout(() => { console.log("setTimeout 2") Promise.resolve().then(() => { console.log("promise 5") }).then(() => { console.log("promise 6") }).then(() => { clearInterval(interval) }) }, 10) //這里加上10ms ...
結(jié)果是promise 4后面的setInterval出現(xiàn)了5次, 因此我覺(jué)得promise 4后面大部分情況下出現(xiàn)2次setInterval、少數(shù)情況出現(xiàn)一次的原因就是瀏覽器在執(zhí)行setInterval回調(diào)函數(shù)后、執(zhí)行setTimeout回調(diào)函數(shù)前, 時(shí)間間隔大部分情況超過(guò)了這個(gè)最短時(shí)間.
另外, 我試著再依次加上1ms, 直到14ms——也就是加上4ms時(shí), promise 4后面的setInterval變成了6次, 可以認(rèn)為setInterval最短間隔時(shí)間在Chrome下約為4ms(不考慮機(jī)子性能、設(shè)置).
Node中的奇怪結(jié)果
首先說(shuō)明一下, 在Node中也體現(xiàn)了任務(wù)隊(duì)列的機(jī)制, 但是這不是Node實(shí)現(xiàn)的, 這是V8實(shí)現(xiàn)的, 由Node調(diào)用了V8任務(wù)隊(duì)列機(jī)制的API. 至于為什么是V8實(shí)現(xiàn)的, 我們翻翻ECMA 262 標(biāo)準(zhǔn)對(duì) Job 和 Job queue 的介紹就可以得知
但是讓人摸不著頭腦的是, 這段代碼在node v8.5.0下有時(shí)會(huì)出現(xiàn)這樣的結(jié)果:
promise 1 promise 2 setInterval setTimeout 1 promise 3 promise 4 setInterval setTimeout 2 setInterval // 為什么會(huì)出現(xiàn)setInterval??? promise 5 promise 6
按理說(shuō)應(yīng)該是setTimeout 2 => promise 5 => promise 6, 因?yàn)檩敵鰏etTimeout 2的回調(diào)函數(shù)是task, 執(zhí)行完這個(gè)task后應(yīng)該調(diào)用microtask 輸出promise 5 => promise 6啊? 很奇怪! Node對(duì)V8確實(shí)有些改動(dòng), 不知道是不是這方面原因...
還請(qǐng)大神解惑!
你竟然讀到這了總結(jié)一下:
學(xué)習(xí)技術(shù)還是有捷徑的, 那就是讀標(biāo)準(zhǔn) ;)
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/90751.html
摘要:主線(xiàn)程不斷重復(fù)上面的三步,此過(guò)程也就是常說(shuō)的事件循環(huán)。所以主線(xiàn)程代碼執(zhí)行時(shí)間過(guò)長(zhǎng),會(huì)阻塞事件循環(huán)的執(zhí)行。參考資料這一次,徹底弄懂執(zhí)行機(jī)制任務(wù)隊(duì)列的順序機(jī)制事件循環(huán)搞懂異步事件輪詢(xún)與中的事件循環(huán) 1. 說(shuō)明 讀過(guò)本文章后,您能知道: JavaScript代碼在瀏覽器中的執(zhí)行機(jī)制和事件循環(huán) 面試中經(jīng)常遇到的代碼輸出順序問(wèn)題 首先通過(guò)一段代碼來(lái)驗(yàn)證你是否了解代碼輸出順序,如果你不知道輸出...
摘要:了解事件循環(huán)機(jī)制有助于理解的執(zhí)行過(guò)程,同時(shí)這也是面試常見(jiàn)題。那么這個(gè)回調(diào)函數(shù)將在何時(shí)由誰(shuí)執(zhí)行呢已知是瀏覽器環(huán)境提供的,因此瀏覽器將對(duì)它進(jìn)行處理,瀏覽器會(huì)在本次事件完成,即計(jì)時(shí)結(jié)束后,將回調(diào)函數(shù)加入循環(huán)隊(duì)列中,然后等待被加入執(zhí)行棧執(zhí)行。 如果有人問(wèn)JavaScript是什么,也許你會(huì)說(shuō)它是一個(gè)單線(xiàn)程、非阻塞、異步、解釋型的腳本語(yǔ)言。那么作為一個(gè)單線(xiàn)程語(yǔ)言,它是怎么實(shí)現(xiàn)非阻塞、異步的?這就...
摘要:主線(xiàn)程要明確的一點(diǎn)是,主線(xiàn)程跟執(zhí)行棧是不同概念,主線(xiàn)程規(guī)定現(xiàn)在執(zhí)行執(zhí)行棧中的哪個(gè)事件。主線(xiàn)程循環(huán)即主線(xiàn)程會(huì)不停的從執(zhí)行棧中讀取事件,會(huì)執(zhí)行完所有棧中的同步代碼。以上參考資料詳解中的事件循環(huán)機(jī)制中的事件循環(huán)運(yùn)行機(jī)制詳解再談 showImg(https://segmentfault.com/img/remote/1460000015317437?w=1920&h=1080); 前言 大家都...
摘要:事件循環(huán)機(jī)制事件循環(huán)機(jī)制分為瀏覽器和事件循環(huán)機(jī)制,兩者的實(shí)現(xiàn)技術(shù)不一樣,瀏覽器是中定義的規(guī)范,是由庫(kù)實(shí)現(xiàn)。整個(gè)事件循環(huán)完成之后,會(huì)去檢測(cè)微任務(wù)的任務(wù)隊(duì)列中是否存在任務(wù),存在就執(zhí)行。 文章來(lái)自我的 github 博客,包括技術(shù)輸出和學(xué)習(xí)筆記,歡迎star。 先來(lái)明白些概念性?xún)?nèi)容。 進(jìn)程、線(xiàn)程 進(jìn)程是系統(tǒng)分配的獨(dú)立資源,是 CPU 資源分配的基本單位,進(jìn)程是由一個(gè)或者多個(gè)線(xiàn)程組成的。 線(xiàn)...
摘要:事件循環(huán)機(jī)制首先區(qū)分進(jìn)程和線(xiàn)程進(jìn)程是資源分配的最小單位系統(tǒng)會(huì)給它分配內(nèi)存不同的進(jìn)程之間是可以同學(xué)的,如管道命名管道消息隊(duì)列一個(gè)進(jìn)程里有單個(gè)或多個(gè)線(xiàn)程瀏覽器是多進(jìn)程的,因?yàn)橄到y(tǒng)給它的進(jìn)程分配了資源內(nèi)存打開(kāi)會(huì)有一個(gè)主進(jìn)程,每打開(kāi)一個(gè)頁(yè)就有一個(gè)獨(dú) JS JavaScript事件循環(huán)機(jī)制 首先區(qū)分進(jìn)程和線(xiàn)程 進(jìn)程是cpu資源分配的最小單位(系統(tǒng)會(huì)給它分配內(nèi)存) 不同的進(jìn)程之間是可以同學(xué)的,如...
閱讀 1900·2021-11-22 09:34
閱讀 3038·2021-09-28 09:35
閱讀 13474·2021-09-09 11:34
閱讀 3602·2019-08-29 16:25
閱讀 2833·2019-08-29 15:23
閱讀 2047·2019-08-28 17:55
閱讀 2437·2019-08-26 17:04
閱讀 3052·2019-08-26 12:21