JavaScript 是單線程砾跃、異步、非阻塞节吮、解釋型腳本語言抽高。
單線程與多線程
單線程語言:JavaScript 的設(shè)計就是為了處理瀏覽器網(wǎng)頁的交互(DOM操作的處理、UI動畫等)透绩,決定了它是一門單線程語言翘骂。
如果有多個線程,它們同時在操作 DOM帚豪,那網(wǎng)頁將會一團糟碳竟。
JavaScript 是單線程的,那么處理任務(wù)是一件接著一件處理狸臣,從上往下順序執(zhí)行:
console.log('script start')
console.log('do something...')
console.log('script end')
// script start
// do something...
// script end
那如果一個任務(wù)的處理耗時(或者是等待)很久的話莹桅,如:網(wǎng)絡(luò)請求、定時器烛亦、等待鼠標點擊等诈泼,后面的任務(wù)也就會被阻塞,也就是說會阻塞所有的用戶交互(按鈕煤禽、滾動條等)铐达,會帶來極不友好的體驗。
console.log('script start')
console.log('do something...')
setTimeout(() => {
console.log('timer over')
}, 1000)
// 點擊頁面
console.log('click page')
console.log('script end')
// script start
// do something...
// click page
// script end
// timer over
timer over
在 script end
后再打印呜师,也就是說計時器并沒有阻塞后面的代碼娶桦。那贾节,發(fā)生了什么汁汗?
其實,JavaScript 單線程指的是瀏覽器中負責解釋和執(zhí)行 JavaScript 代碼的只有一個線程栗涂,即為 JS引擎線程知牌,但是瀏覽器的渲染進程是提供多個線程的,如下:
- JS引擎線程
- 事件觸發(fā)線程
- 定時觸發(fā)器線程
- 異步http請求線程
- GUI渲染線程
瀏覽器渲染進程參考此處
當遇到計時器斤程、DOM事件監(jiān)聽或者是網(wǎng)絡(luò)請求的任務(wù)時角寸,JS引擎會將它們直接交給 webapi菩混,也就是瀏覽器提供的相應(yīng)線程(如定時器線程為setTimeout計時、異步http請求線程處理網(wǎng)絡(luò)請求)去處理扁藕,而JS引擎線程繼續(xù)后面的其他任務(wù)沮峡,這樣便實現(xiàn)了 異步非阻塞。
定時器觸發(fā)線程也只是為 setTimeout(..., 1000) 定時而已亿柑,時間一到邢疙,還會把它對應(yīng)的回調(diào)函數(shù)(callback)交給 消息隊列 去維護,JS引擎線程會在適當?shù)臅r候去消息隊列取出消息并執(zhí)行望薄。
這里疟游,JavaScript 通過 事件循環(huán) event loop 的機制
來解決這個問題
同步與異步
setTimeout(() => {
console.log('hello 0')
}, 1000)
console.log('hello 1')
// hello 1
// hello 0
上面的 setTimeout 函數(shù)便不會立刻返回結(jié)果,而是發(fā)起了一個異步痕支,setTimeout 便是異步的發(fā)起函數(shù)或者是注冊函數(shù)颁虐,() => {…} 便是異步的回調(diào)函數(shù)。
這里卧须,JS引擎線程只會關(guān)心異步的發(fā)起函數(shù)是誰另绩、回調(diào)函數(shù)是什么?并將異步交給 webapi 去處理花嘶,然后繼續(xù)執(zhí)行其他任務(wù)板熊。
異步一般是以下:
- 網(wǎng)絡(luò)請求
- 計時器
- DOM時間監(jiān)聽
- ....
事件循環(huán)與消息隊列
回到事件循環(huán) event loop
其實 事件循環(huán) 機制和 消息隊列 的維護是由事件觸發(fā)線程控制的。
事件觸發(fā)線程 同樣是瀏覽器渲染引擎提供的察绷,它會維護一個 消息隊列干签。
JS引擎線程遇到異步(DOM事件監(jiān)聽、網(wǎng)絡(luò)請求拆撼、setTimeout計時器等…)容劳,會交給相應(yīng)的線程單獨去維護異步任務(wù),等待某個時機(計時器結(jié)束闸度、網(wǎng)絡(luò)請求成功竭贩、用戶點擊DOM),然后由 事件觸發(fā)線程 將異步對應(yīng)的 回調(diào)函數(shù) 加入到消息隊列中莺禁,消息隊列中的回調(diào)函數(shù)等待被執(zhí)行留量。
同時,JS引擎線程會維護一個 執(zhí)行棧哟冬,同步代碼會依次加入執(zhí)行棧然后執(zhí)行楼熄,結(jié)束會退出執(zhí)行棧。
如果執(zhí)行棧里的任務(wù)執(zhí)行完成浩峡,即執(zhí)行棧為空的時候(即JS引擎線程空閑)可岂,事件觸發(fā)線程才會從消息隊列取出一個任務(wù)(即異步的回調(diào)函數(shù))放入執(zhí)行棧中執(zhí)行。
消息隊列是類似隊列的數(shù)據(jù)結(jié)構(gòu)翰灾,遵循先入先出(FIFO)的規(guī)則缕粹。
執(zhí)行完了后稚茅,執(zhí)行棧再次為空,事件觸發(fā)線程會重復(fù)上一步操作平斩,再取出一個消息隊列中的任務(wù)亚享,這種機制就被稱為事件循環(huán)(event loop)機制。
宏任務(wù)與微任務(wù)
以上機制在ES5的情況下夠用了绘面,但是ES6會有一些問題虹蒋。
Promise同樣是用來處理異步的:
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
這里有一個新概念:macrotask(宏任務(wù))
和 microtask(微任務(wù))
。
所有任務(wù)分為 macrotask
和 microtask
:
macrotask:主代碼塊飒货、setTimeout魄衅、setInterval等(可以看到,事件隊列中的每一個事件都是一個 macrotask塘辅,現(xiàn)在稱之為宏任務(wù)隊列).
microtask:Promise晃虫、process.nextTick等
JS引擎線程首先執(zhí)行主代碼塊。
每次執(zhí)行棧執(zhí)行的代碼就是一個宏任務(wù)扣墩,包括任務(wù)隊列(宏任務(wù)隊列)中的哲银,因為執(zhí)行棧中的宏任務(wù)執(zhí)行完會去取任務(wù)隊列(宏任務(wù)隊列)中的任務(wù)加入執(zhí)行棧中,即同樣是事件循環(huán)的機制呻惕。
在執(zhí)行宏任務(wù)時遇到Promise等荆责,會創(chuàng)建微任務(wù)(.then()里面的回調(diào)),并加入到微任務(wù)隊列隊尾亚脆。
microtask必然是在某個宏任務(wù)執(zhí)行的時候創(chuàng)建的做院,而在下一個宏任務(wù)開始之前,瀏覽器會對頁面重新渲染(task >> 渲染 >> 下一個task(從任務(wù)隊列中取一個))濒持。同時键耕,在上一個宏任務(wù)執(zhí)行完成后,渲染頁面之前柑营,會執(zhí)行當前微任務(wù)隊列中的所有微任務(wù)屈雄。
也就是說,在某一個macrotask執(zhí)行完后官套,在重新渲染與開始下一個宏任務(wù)之前酒奶,就會將在它執(zhí)行期間產(chǎn)生的所有microtask都執(zhí)行完畢(在渲染前)。
這樣就可以解釋 “promise 1” “promise 2” 在 “timer over” 之前打印了奶赔⊥锖浚”promise 1” “promise 2” 做為微任務(wù)加入到微任務(wù)隊列中,而 “timer over” 做為宏任務(wù)加入到宏任務(wù)隊列中纺阔,它們同時在等待被執(zhí)行瘸彤,但是微任務(wù)隊列中的所有微任務(wù)都會在開始下一個宏任務(wù)之前都被執(zhí)行完。
在node環(huán)境下笛钝,process.nextTick的優(yōu)先級高于Promise质况,也就是說:在宏任務(wù)結(jié)束后會先執(zhí)行微任務(wù)隊列中的nextTickQueue,然后才會執(zhí)行微任務(wù)中的Promise玻靡。
執(zhí)行機制:
- 執(zhí)行一個宏任務(wù)(棧中沒有就從事件隊列中獲冉衢)
- 執(zhí)行過程中如果遇到微任務(wù),就將它添加到微任務(wù)的任務(wù)隊列中
- 宏任務(wù)執(zhí)行完畢后囤捻,立即執(zhí)行當前微任務(wù)隊列中的所有微任務(wù)(依次執(zhí)行)
- 當前宏任務(wù)執(zhí)行完畢臼朗,開始檢查渲染,然后GUI線程接管渲染
- 渲染完畢后蝎土,JS引擎線程繼續(xù)视哑,開始下一個宏任務(wù)(從宏任務(wù)隊列中獲取)
總結(jié)
- JavaScript 是單線程語言誊涯,決定于它的設(shè)計最初是用來處理瀏覽器網(wǎng)頁的交互挡毅。瀏覽器負責解釋和執(zhí)行 JavaScript 的線程只有一個(所有說是單線程)暴构,即JS引擎線程,但是瀏覽器同樣提供其他線程取逾,如:事件觸發(fā)線程、定時器觸發(fā)線程等砾隅。
異步一般是指:
- 網(wǎng)絡(luò)請求
- 計時器
- DOM事件監(jiān)聽
事件循環(huán)機制:
- JS引擎線程會維護一個執(zhí)行棧误阻,同步代碼會依次加入到執(zhí)行棧中依次執(zhí)行并出棧。
- JS引擎線程遇到異步函數(shù)堕绩,會將異步函數(shù)交給相應(yīng)的Webapi,而繼續(xù)執(zhí)行后面的任務(wù)奴紧。
- Webapi會在條件滿足的時候,將異步對應(yīng)的回調(diào)加入到消息隊列中晶丘,等待執(zhí)行黍氮。
- 執(zhí)行棧為空時,JS引擎線程會去取消息隊列中的回調(diào)函數(shù)(如果有的話)浅浮,并加入到執(zhí)行棧中執(zhí)行。
- 完成后出棧滚秩,執(zhí)行棧再次為空,重復(fù)上面的操作本股,這就是事件循環(huán)(event loop)機制。