摘要:?jiǎn)尉€程異步非阻塞然后,這又牽扯到了事件循環(huán)消息隊(duì)列,還有微任務(wù)宏任務(wù)這些。此步的位置不確定某個(gè)時(shí)刻后,定時(shí)器觸發(fā)線程通知事件觸發(fā)線程,事件觸發(fā)線程將回調(diào)函數(shù)加入消息隊(duì)列隊(duì)尾,等待引擎線程執(zhí)行。
前言
Philip Roberts 在演講 great talk at JSConf on the event loop 中說(shuō):要是用一句話來(lái)形容 JavaScript,我可能會(huì)這樣:
“JavaScript 是單線程、異步、非阻塞、解釋型腳本語(yǔ)言。”
單線程 ?
異步 ? ?
非阻塞 ? ? ?
然后,這又牽扯到了事件循環(huán)、消息隊(duì)列,還有微任務(wù)、宏任務(wù)這些。
作為一個(gè)初學(xué)者,對(duì)這些了解甚少。
這幾天翻閱了不少資料,似乎了解到了一二,是時(shí)候總結(jié)一下了,它們困擾了我好一段時(shí)間,就像學(xué)高數(shù)那會(huì)兒自己去理解一個(gè)概念一樣。
單線程與多線程單線程語(yǔ)言:JavaScript 的設(shè)計(jì)就是為了處理瀏覽器網(wǎng)頁(yè)的交互(DOM操作的處理、UI動(dòng)畫等),決定了它是一門單線程語(yǔ)言。
如果有多個(gè)線程,它們同時(shí)在操作 DOM,那網(wǎng)頁(yè)將會(huì)一團(tuán)糟。
JavaScript 是單線程的,那么處理任務(wù)是一件接著一件處理,從上往下順序執(zhí)行:
console.log("script start") console.log("do something...") console.log("script end") // script start // do something... // script end
上面的代碼會(huì)依次打印: "script start" >> "do something..." >> "script end"
那如果一個(gè)任務(wù)的處理耗時(shí)(或者是等待)很久的話,如:網(wǎng)絡(luò)請(qǐng)求、定時(shí)器、等待鼠標(biāo)點(diǎn)擊等,后面的任務(wù)也就會(huì)被阻塞,也就是說(shuō)會(huì)阻塞所有的用戶交互(按鈕、滾動(dòng)條等),會(huì)帶來(lái)極不友好的體驗(yàn)。
但是:
console.log("script start") console.log("do something...") setTimeout(() => { console.log("timer over") }, 1000) // 點(diǎn)擊頁(yè)面 console.log("click page") console.log("script end") // script start // do something... // click page // script end // timer over
"timer over" 在 "script end" 后再打印,也就是說(shuō)計(jì)時(shí)器并沒有阻塞后面的代碼。那,發(fā)生了什么?
其實(shí),JavaScript 單線程指的是瀏覽器中負(fù)責(zé)解釋和執(zhí)行 JavaScript 代碼的只有一個(gè)線程,即為JS引擎線程,但是瀏覽器的渲染進(jìn)程是提供多個(gè)線程的,如下:
JS引擎線程
事件觸發(fā)線程
定時(shí)觸發(fā)器線程
異步http請(qǐng)求線程
GUI渲染線程
瀏覽器渲染進(jìn)程參考這里
當(dāng)遇到計(jì)時(shí)器、DOM事件監(jiān)聽或者是網(wǎng)絡(luò)請(qǐng)求的任務(wù)時(shí),JS引擎會(huì)將它們直接交給 webapi,也就是瀏覽器提供的相應(yīng)線程(如定時(shí)器線程為setTimeout計(jì)時(shí)、異步http請(qǐng)求線程處理網(wǎng)絡(luò)請(qǐng)求)去處理,而JS引擎線程繼續(xù)后面的其他任務(wù),這樣便實(shí)現(xiàn)了 異步非阻塞。
定時(shí)器觸發(fā)線程也只是為 setTimeout(..., 1000) 定時(shí)而已,時(shí)間一到,還會(huì)把它對(duì)應(yīng)的回調(diào)函數(shù)(callback)交給 消息隊(duì)列 去維護(hù),JS引擎線程會(huì)在適當(dāng)?shù)臅r(shí)候去消息隊(duì)列取出消息并執(zhí)行。
JS引擎線程什么時(shí)候去處理呢?消息隊(duì)列又是什么?
這里,JavaScript 通過(guò) 事件循環(huán) event loop 的機(jī)制來(lái)解決這個(gè)問題。
這個(gè)放在后面再討論吧!
同步與異步上面說(shuō)到了異步,JavaScript 中有同步代碼與異步代碼。
下面便是同步:
console.log("hello 0") console.log("hello 1") console.log("hello 2") // hello 0 // hello 1 // hello 2
它們會(huì)依次執(zhí)行,執(zhí)行完了后便會(huì)返回結(jié)果(打印結(jié)果)。
setTimeout(() => { console.log("hello 0") }, 1000) console.log("hello 1") // hello 1 // hello 0
上面的 setTimeout 函數(shù)便不會(huì)立刻返回結(jié)果,而是發(fā)起了一個(gè)異步,setTimeout 便是異步的發(fā)起函數(shù)或者是注冊(cè)函數(shù),() => {...} 便是異步的回調(diào)函數(shù)。
這里,JS引擎線程只會(huì)關(guān)心異步的發(fā)起函數(shù)是誰(shuí)、回調(diào)函數(shù)是什么?并將異步交給 webapi 去處理,然后繼續(xù)執(zhí)行其他任務(wù)。
異步一般是以下:
網(wǎng)絡(luò)請(qǐng)求
計(jì)時(shí)器
DOM時(shí)間監(jiān)聽
...
事件循環(huán)與消息隊(duì)列回到事件循環(huán) event loop
其實(shí) 事件循環(huán) 機(jī)制和 消息隊(duì)列 的維護(hù)是由事件觸發(fā)線程控制的。
事件觸發(fā)線程 同樣是瀏覽器渲染引擎提供的,它會(huì)維護(hù)一個(gè) 消息隊(duì)列。
JS引擎線程遇到異步(DOM事件監(jiān)聽、網(wǎng)絡(luò)請(qǐng)求、setTimeout計(jì)時(shí)器等...),會(huì)交給相應(yīng)的線程多帶帶去維護(hù)異步任務(wù),等待某個(gè)時(shí)機(jī)(計(jì)時(shí)器結(jié)束、網(wǎng)絡(luò)請(qǐng)求成功、用戶點(diǎn)擊DOM),然后由 事件觸發(fā)線程 將異步對(duì)應(yīng)的 回調(diào)函數(shù) 加入到消息隊(duì)列中,消息隊(duì)列中的回調(diào)函數(shù)等待被執(zhí)行。
同時(shí),JS引擎線程會(huì)維護(hù)一個(gè) 執(zhí)行棧,同步代碼會(huì)依次加入執(zhí)行棧然后執(zhí)行,結(jié)束會(huì)退出執(zhí)行棧。
如果執(zhí)行棧里的任務(wù)執(zhí)行完成,即執(zhí)行棧為空的時(shí)候(即JS引擎線程空閑),事件觸發(fā)線程才會(huì)從消息隊(duì)列取出一個(gè)任務(wù)(即異步的回調(diào)函數(shù))放入執(zhí)行棧中執(zhí)行。
消息隊(duì)列是類似隊(duì)列的數(shù)據(jù)結(jié)構(gòu),遵循先入先出(FIFO)的規(guī)則。
執(zhí)行完了后,執(zhí)行棧再次為空,事件觸發(fā)線程會(huì)重復(fù)上一步操作,再取出一個(gè)消息隊(duì)列中的任務(wù),這種機(jī)制就被稱為事件循環(huán)(event loop)機(jī)制。
還是上面的代碼:
console.log("script start") setTimeout(() => { console.log("timer over") }, 1000) // 點(diǎn)擊頁(yè)面 console.log("click page") console.log("script end") // script start // click page // script end // timer over
執(zhí)行過(guò)程:
主代碼塊(script)依次加入執(zhí)行棧,依次執(zhí)行,主代碼塊為:
console.log("script start")
setTimeout()
console.log("click page")
console.log("script end")
console.log() 為同步代碼,JS引擎線程處理,打印 "script start",出棧;
遇到異步函數(shù) setTimeout,交給定時(shí)器觸發(fā)線程(異步觸發(fā)函數(shù)為:setTimeout,回調(diào)函數(shù)為:() => { ... }),JS引擎線程繼續(xù),出棧;
console.log() 為同步代碼,JS引擎線程處理,打印 "click page",出棧;
console.log() 為同步代碼,JS引擎線程處理,打印 "script end",出棧;
執(zhí)行棧為空,也就是JS引擎線程空閑,這時(shí)從消息隊(duì)列中取出(如果有的話)一條任務(wù)(callback)加入執(zhí)行棧,并執(zhí)行;
重復(fù)第6步。
(此步的位置不確定)某個(gè)時(shí)刻(1000ms后),定時(shí)器觸發(fā)線程通知事件觸發(fā)線程,事件觸發(fā)線程將回調(diào)函數(shù) () => { ... } 加入消息隊(duì)列隊(duì)尾,等待JS引擎線程執(zhí)行。
可以看出,setTimeout異步函數(shù)對(duì)應(yīng)的回調(diào)函數(shù)( () => {} )會(huì)在執(zhí)行棧為空,主代碼塊執(zhí)行完了后才會(huì)執(zhí)行。
零延時(shí):
console.log("script start") setTimeout(() => { console.log("timer 1 over") }, 1000) setTimeout(() => { console.log("timer 2 over") }, 0) console.log("script end") // script start // script end // timer 2 over // timer 1 over
這里會(huì)先打印 "timer 2 over",然后打印 "timer 1 over",盡管 timer 1 先被定時(shí)器觸發(fā)線程處理,但是 timer 2 的callback會(huì)先加入消息隊(duì)列。
上面,timer 2 的延時(shí)為 0ms,HTML5標(biāo)準(zhǔn)規(guī)定 setTimeout 第二個(gè)參數(shù)不得小于4(不同瀏覽器最小值會(huì)不一樣),不足會(huì)自動(dòng)增加,所以 "timer 2 over" 還是會(huì)在 "script end" 之后。
就算延時(shí)為 0ms,只是 timer 2 的回調(diào)函數(shù)會(huì)立即加入消息隊(duì)列而已,回調(diào)的執(zhí)行還是得等執(zhí)行棧為空(JS引擎線程空閑)時(shí)執(zhí)行。
其實(shí) setTimeout 的第二個(gè)參數(shù)并不能代表回調(diào)執(zhí)行的準(zhǔn)確的延時(shí)事件,它只能表示回調(diào)執(zhí)行的最小延時(shí)時(shí)間,因?yàn)榛卣{(diào)函數(shù)進(jìn)入消息隊(duì)列后需要等待執(zhí)行棧中的同步任務(wù)執(zhí)行完成,執(zhí)行棧為空時(shí)才會(huì)被執(zhí)行。宏任務(wù)與微任務(wù)
以上機(jī)制在ES5的情況下夠用了,但是ES6會(huì)有一些問題。
Promise同樣是用來(lái)處理異步的:
console.log("script start") setTimeout(function() { console.log("timer over") }, 0) Promise.resolve().then(function() { console.log("promise1") }).then(function() { console.log("promise2") }) console.log("script end") // script start // script end // promise1 // promise2 // timer over
WTF?? "promise 1" "promise 2" 在 "timer over" 之前打印了?
這里有一個(gè)新概念:macrotask(宏任務(wù)) 和 microtask(微任務(wù))。
所有任務(wù)分為 macrotask 和 microtask:
macrotask:主代碼塊、setTimeout、setInterval等(可以看到,事件隊(duì)列中的每一個(gè)事件都是一個(gè) macrotask,現(xiàn)在稱之為宏任務(wù)隊(duì)列)
microtask:Promise、process.nextTick等
JS引擎線程首先執(zhí)行主代碼塊。
每次執(zhí)行棧執(zhí)行的代碼就是一個(gè)宏任務(wù),包括任務(wù)隊(duì)列(宏任務(wù)隊(duì)列)中的,因?yàn)閳?zhí)行棧中的宏任務(wù)執(zhí)行完會(huì)去取任務(wù)隊(duì)列(宏任務(wù)隊(duì)列)中的任務(wù)加入執(zhí)行棧中,即同樣是事件循環(huán)的機(jī)制。
在執(zhí)行宏任務(wù)時(shí)遇到Promise等,會(huì)創(chuàng)建微任務(wù)(.then()里面的回調(diào)),并加入到微任務(wù)隊(duì)列隊(duì)尾。
microtask必然是在某個(gè)宏任務(wù)執(zhí)行的時(shí)候創(chuàng)建的,而在下一個(gè)宏任務(wù)開始之前,瀏覽器會(huì)對(duì)頁(yè)面重新渲染(task >> 渲染 >> 下一個(gè)task(從任務(wù)隊(duì)列中取一個(gè)))。同時(shí),在上一個(gè)宏任務(wù)執(zhí)行完成后,渲染頁(yè)面之前,會(huì)執(zhí)行當(dāng)前微任務(wù)隊(duì)列中的所有微任務(wù)。
也就是說(shuō),在某一個(gè)macrotask執(zhí)行完后,在重新渲染與開始下一個(gè)宏任務(wù)之前,就會(huì)將在它執(zhí)行期間產(chǎn)生的所有microtask都執(zhí)行完畢(在渲染前)。
這樣就可以解釋 "promise 1" "promise 2" 在 "timer over" 之前打印了。"promise 1" "promise 2" 做為微任務(wù)加入到微任務(wù)隊(duì)列中,而 "timer over" 做為宏任務(wù)加入到宏任務(wù)隊(duì)列中,它們同時(shí)在等待被執(zhí)行,但是微任務(wù)隊(duì)列中的所有微任務(wù)都會(huì)在開始下一個(gè)宏任務(wù)之前都被執(zhí)行完。
在node環(huán)境下,process.nextTick的優(yōu)先級(jí)高于Promise,也就是說(shuō):在宏任務(wù)結(jié)束后會(huì)先執(zhí)行微任務(wù)隊(duì)列中的nextTickQueue,然后才會(huì)執(zhí)行微任務(wù)中的Promise。
執(zhí)行機(jī)制:
執(zhí)行一個(gè)宏任務(wù)(棧中沒有就從事件隊(duì)列中獲取)
執(zhí)行過(guò)程中如果遇到微任務(wù),就將它添加到微任務(wù)的任務(wù)隊(duì)列中
宏任務(wù)執(zhí)行完畢后,立即執(zhí)行當(dāng)前微任務(wù)隊(duì)列中的所有微任務(wù)(依次執(zhí)行)
當(dāng)前宏任務(wù)執(zhí)行完畢,開始檢查渲染,然后GUI線程接管渲染
渲染完畢后,JS引擎線程繼續(xù),開始下一個(gè)宏任務(wù)(從宏任務(wù)隊(duì)列中獲取)
總結(jié)JavaScript 是單線程語(yǔ)言,決定于它的設(shè)計(jì)最初是用來(lái)處理瀏覽器網(wǎng)頁(yè)的交互。瀏覽器負(fù)責(zé)解釋和執(zhí)行 JavaScript 的線程只有一個(gè)(所有說(shuō)是單線程),即JS引擎線程,但是瀏覽器同樣提供其他線程,如:事件觸發(fā)線程、定時(shí)器觸發(fā)線程等。
異步一般是指:
網(wǎng)絡(luò)請(qǐng)求
計(jì)時(shí)器
DOM事件監(jiān)聽
事件循環(huán)機(jī)制:
JS引擎線程會(huì)維護(hù)一個(gè)執(zhí)行棧,同步代碼會(huì)依次加入到執(zhí)行棧中依次執(zhí)行并出棧。
JS引擎線程遇到異步函數(shù),會(huì)將異步函數(shù)交給相應(yīng)的Webapi,而繼續(xù)執(zhí)行后面的任務(wù)。
Webapi會(huì)在條件滿足的時(shí)候,將異步對(duì)應(yīng)的回調(diào)加入到消息隊(duì)列中,等待執(zhí)行。
執(zhí)行棧為空時(shí),JS引擎線程會(huì)去取消息隊(duì)列中的回調(diào)函數(shù)(如果有的話),并加入到執(zhí)行棧中執(zhí)行。
完成后出棧,執(zhí)行棧再次為空,重復(fù)上面的操作,這就是事件循環(huán)(event loop)機(jī)制。
原文鏈接
參考:
Tasks, microtasks, queues and schedules
great talk at JSConf on the event loop
并發(fā)模型與事件循環(huán) - JavaScript | MDN
從瀏覽器多進(jìn)程到JS單線程,JS運(yùn)行... - 掘金
瀏覽器篇-Event-Loop.md - PDKSophia/blog.io
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/99143.html
摘要:主線程從任務(wù)隊(duì)列中讀取事件,這個(gè)過(guò)程是循環(huán)不斷的,所以整個(gè)的這種運(yùn)行機(jī)制又稱為事件循環(huán)。上面也提到,在到達(dá)指定時(shí)間時(shí),定時(shí)器就會(huì)將相應(yīng)回調(diào)函數(shù)插入任務(wù)隊(duì)列尾部。這就是定時(shí)器功能。關(guān)于定時(shí)器的重要補(bǔ)充定時(shí)器包括與兩個(gè)方法。 一、引子 本文介紹JavaScript運(yùn)行機(jī)制,這一部分比較抽象,我們先從一道面試題入手: console.log(1); setTimeout(function()...
摘要:主線程從任務(wù)隊(duì)列中讀取事件,這個(gè)過(guò)程是循環(huán)不斷的,所以整個(gè)的這種運(yùn)行機(jī)制又稱為事件循環(huán)。上面也提到,在到達(dá)指定時(shí)間時(shí),定時(shí)器就會(huì)將相應(yīng)回調(diào)函數(shù)插入任務(wù)隊(duì)列尾部。這就是定時(shí)器功能。關(guān)于定時(shí)器的重要補(bǔ)充定時(shí)器包括與兩個(gè)方法。 一、引子 本文介紹JavaScript運(yùn)行機(jī)制,這一部分比較抽象,我們先從一道面試題入手: console.log(1); setTimeout(function()...
摘要:主線程從任務(wù)隊(duì)列中讀取事件,這個(gè)過(guò)程是循環(huán)不斷的,所以整個(gè)的這種運(yùn)行機(jī)制又稱為事件循環(huán)。上面也提到,在到達(dá)指定時(shí)間時(shí),定時(shí)器就會(huì)將相應(yīng)回調(diào)函數(shù)插入任務(wù)隊(duì)列尾部。這就是定時(shí)器功能。關(guān)于定時(shí)器的重要補(bǔ)充定時(shí)器包括與兩個(gè)方法。 一、引子 本文介紹JavaScript運(yùn)行機(jī)制,這一部分比較抽象,我們先從一道面試題入手: console.log(1); setTimeout(function()...
摘要:下面我將介紹的基本用法以及如何在異步編程中使用它們。在沒有發(fā)布之前,作為異步編程主力軍的回調(diào)函數(shù)一直被人詬病,其原因有太多比如回調(diào)地獄代碼執(zhí)行順序難以追蹤后期因代碼變得十分復(fù)雜導(dǎo)致無(wú)法維護(hù)和更新等,而的出現(xiàn)在很大程度上改變了之前的窘境。 前言 自己著手準(zhǔn)備寫這篇文章的初衷是覺得如果想要更深入的理解 JS,異步編程則是必須要跨過(guò)的一道坎。由于這里面涉及到的東西很多也很廣,在初學(xué) JS 的...
閱讀 4195·2021-11-22 13:52
閱讀 2096·2021-09-22 15:12
閱讀 1135·2019-08-30 15:53
閱讀 3467·2019-08-29 17:12
閱讀 2198·2019-08-29 16:23
閱讀 1664·2019-08-26 13:56
閱讀 1781·2019-08-26 13:44
閱讀 1898·2019-08-26 11:56