如何理解JS單線程?
JS是單線程的,也就是說同一時間只能做一件事,看下邊
console.log(1)
setTimeout(() => {
console.log(2)
}, 0);
console.log(3)
// 執(zhí)行結果
1,3,2
單線程優(yōu)先執(zhí)行同步任務,同步任務執(zhí)行完才去執(zhí)行異步任務,在執(zhí)行同步任務的過程中遇到異步任務會掛起,繼續(xù)執(zhí)行同步任務。
- 單線程:只有一個線程,代碼順序執(zhí)行,容易出現(xiàn)代碼阻塞(頁面假死)。
- 多線程:有多個線程,線程間獨立運行,能有效地避免代碼阻塞,并且提高程序的運行性能
可以看到多線程其實是比較占優(yōu)勢,事實上,大多數(shù)語言采用的也是多線程運行。那竟然如此,為何JavaScript卻選擇了單線程的方式運行呢?
這就跟js的用途有關了,因為在瀏覽器中,js主要是用于頁面交互以及操作頁面的DOM元素的。如果有兩個線程,一個線程要求刪除DOM元素,另一個線程卻要修改DOM元素的樣式,那瀏覽器就無法確定應該聽哪個線程的。雖然聰明的小伙伴可能知道可以加個”鎖“,但是這就會提高了復雜度,要知道,js可是用了十天就設計出來了呀,不可能搞得這么復雜滴。所以從誕生以來,js就一直是單線程執(zhí)行的
瀏覽器線程
既然js是單線程執(zhí)行的,那各種http請求和事件觸發(fā)以及邏輯運行怎么可能執(zhí)行的過來?其實不要混淆了,js線程一般只負責js的解析和執(zhí)行。而上面說的請求和事件觸發(fā)這些都是由瀏覽器處理的,而瀏覽器卻是多線程的。一般的瀏覽器有以下幾個線程:
- 事件觸發(fā)線程:處理常見的DOM操作
- 定時器線程:處理定時器任務,比如
setTimeOut,setInterval - http請求線程:處理http請求。
- 渲染引擎線程:負責頁面的渲染,當頁面發(fā)生重繪和回流時會執(zhí)行該線程
- js引擎線程:負責js的解析和邏輯執(zhí)行。
我們所說的“js是單線程”指的就是瀏覽器一般只開一個js引擎線程來執(zhí)行js。而在執(zhí)行過程中遇到定時器或者http請求等,會丟給上面相對應的線程執(zhí)行,而js則繼續(xù)運行自己代碼,這樣就不會阻塞了,等到http請求或定時器等返回回調函數(shù)的時候且js引擎沒有任務時(具體見下文),js再執(zhí)行這個回調函數(shù),
這就是異步和回調。
HTML5 Web Worker
當然js執(zhí)行過程中不可避免也有比如復雜運算或多重循環(huán)等耗時操作,針對這個問題HTML5提出了Web Worker,它會在當前js執(zhí)行主線程中利用Worker類新開辟一個額外的線程來加載和運行特定的JavaScript文件,這個新的線程和JavaScript的主線程互不干擾。同時HTML5也規(guī)定了 Web Worker中是不能操作DOM的,任何需要操作DOM的任務都需要委托給JavaScript主線程來執(zhí)行,所以雖然引入HTML5 Web Worker,但仍然沒有改變JavaScript單線程的本質。
EventLoop 事件循環(huán)機制
好了,前面鋪墊那么多,終于來到了標題所講的部分了。那么,什么是任務隊列和EventLoop呢?
其實,在js執(zhí)行過程中,分為一個主執(zhí)行棧和一個任務隊列。js的代碼執(zhí)行會在主執(zhí)行棧中進行,遇到http請求和定時器等異步操作的時候會丟給對應線程執(zhí)行。而每個異步任務都有一個回調函數(shù),等到請求操作完成或者定時器數(shù)秒完成之后,會把對應的回調函數(shù)放入到任務隊列中。而js主執(zhí)行棧里面的內容為空后,就會來任務隊列里面按隊列順序取一個函數(shù)到主執(zhí)行棧里執(zhí)行,等函數(shù)執(zhí)行完成之后棧又空了,再來任務隊列里取函數(shù)執(zhí)行,如此反復循環(huán)直到任務隊列和棧都為空,這就是所謂的事件循環(huán)機制EventLoop。
我們可以通過下邊例子來熟悉 事件循環(huán)機制
function a(){
console.log('this is a function');
}
function b(){
console.log('this is b function');
a();
}
setTimeout(function() {
console.log('this is setTimeout');
}, 0);
b();
1.這段代碼在執(zhí)行b函數(shù)之前,遇到setTimeout,所以將setTimeout丟到定時器線程里面去數(shù)秒,而主執(zhí)行棧繼續(xù)執(zhí)行,所以調用函數(shù)b,函數(shù)b入主執(zhí)行棧。
2.零秒很快數(shù)完,所以setTimeout后面的回調函數(shù)被放入到任務隊列里面,但是主執(zhí)行棧里面現(xiàn)在不為空,所以還沒有輪到任務隊列里的函數(shù)執(zhí)行。
3.調用函數(shù)b的時候就創(chuàng)建了主執(zhí)行棧的第一幀,里面包含了b函數(shù)的參數(shù)和局部變量等,執(zhí)行輸出this is b function。當b調用a時,創(chuàng)建第二幀,同樣該幀包含a函數(shù)的參數(shù)和局部變量,執(zhí)行輸出this is a function。a執(zhí)行結束,第二幀就出棧。同時b也執(zhí)行結束了,第一幀出棧,此時主執(zhí)行棧就為空了。
4.主執(zhí)行棧為空后,就開始調用任務隊列里面的任務。取出第一個回調函數(shù)執(zhí)行,創(chuàng)建新的第一幀,執(zhí)行輸出this is setTimeout。執(zhí)行完畢該幀出棧,棧為空,繼續(xù)去任務隊列取下一個任務到棧里執(zhí)行。如此循環(huán)反復。
再看一個例子
console.log('A')
while(true) {
}
console.log('B')
// 執(zhí)行結果
A
先執(zhí)行輸出A這個是沒有問題的,當執(zhí)行到while循環(huán)的時候,這是一個同步任務,又是一個循環(huán)體,會一直循環(huán)執(zhí)行,所以執(zhí)行不到B了
再看下邊的例子
for(var i=0;i<4;i++){
setTimeout(() => {
console.log(i)
}, 1000);
}
//輸出結果 4個4