JS運行機制

參考

如何理解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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容