首頁 > web前端 > js教程 > 主體

JS運作機制:同步、非同步以及事件循環(Event Loop)的分析

不言
發布: 2018-08-07 15:41:28
原創
1495 人瀏覽過

這篇文章帶給大家的內容是關於JS運作機制:同步、非同步以及事件循環(Event Loop)的分析,有一定的參考價值,有需要的朋友可以參考一下,希望對你有所幫助。

一、JS為什麼是單執行緒的

Javascript語言的一大特色就是單線程,而同一時間只能做同一件事,那為什麼JS不能多線程呢?

作為瀏覽器腳本語言,Javascript的主要用途是與使用者互動,以及操作DOM,這決定了它只能是單線程,否則會帶來複雜的同步問題。例如:假定Javascript同時有兩個線程,一個線程在某個DOM節點上加入內容,另一個線程刪除了找個節點,這時瀏覽器應該以哪一個為準?

所以,為了避免複雜性,從一誕生,Javascript就是單線程,這已經成為了這門語言的核心特徵,將來也不會改變。

為了利用多核心CPU的運算能力,HTML5提出Web Woker標準,允許Javascript腳本建立多個線程,但是子執行緒完全受主執行緒控制,且不能操作DOM。所以,這個新標準並沒有改變JavaScript單執行緒的本質。

二、同步與非同步

#假設存在一個函數A:

A(args...){...}

同步:如果在呼叫函數A的時候,呼叫者立即能夠得到預期的結果,那麼這個函數就是同步的。

非同步:如果在呼叫函數A的時候,呼叫者無法立即得到預期的結果,而是需要在將來透過一定的手段(耗時,延時,事件觸發)得到,那麼這個函數就是異步的。

三、JS是如何實現非同步操作的

#雖然JS是單執行緒的但是瀏覽器的核心是多執行緒的,瀏覽器為一些耗時任務開闢了另外的線程,不同的非同步操作會由不同的瀏覽器內核模組調度執行,例如onlcik,setTimeout,ajax處理的方式都不同,分別是由瀏覽器內核中的DOM Bingding、network、timer模組執行,當執行的任務得到運行結果時,會將對應的回呼函數放到任務佇列中。所以說,JS是一直都是單線程的,實現非同步操作的其實就是瀏覽器。

在上圖中,呼叫堆疊中遇到DOM請求、ajax請求以及setTimeout等WebAPIs的時候就會交給瀏覽器核心的其他模組進行處理,webkit核心在Javascript執行引擎之外,有一個重要的模組是webcoew模組。圖中WebAPIs提到的三種API,webcore分別提供了DOM Binding、network、timer模組來處理底層實作。等到這些模組處理完這些操作的時候將回呼函數放入任務佇列中,之後等棧中的task執行完再去執行任務佇列中的回呼函數。

 小結:

#1、所有的程式碼都要透過函數呼叫堆疊中呼叫執行

2、當遇到前文提到的APIs的時候,會交給瀏覽器核心的其他模組進行處理

#3、任務佇列中存放的是回呼函數

#4 、等到呼叫堆疊中的task執行完之後再回去執行任務佇列中的task

JS的運作機制如下:

(1)所有的同步任務都在主執行緒上執行,形成一個執行棧。

(2)主執行緒之外,還存在一個「任務隊列」。只要非同步任務有了運算結果,就在「任務佇列「之中放置一個事件(回呼函數)。

(3)一旦“執行堆疊”中的所有同步任務執行完畢,系統就會讀取“任務佇列”,看看裡面有哪些事件。那些對應的非同步任務,於是結束等待狀態。進入執行棧,開始執行。

(4)主執行緒不斷重複上面的第三步

任務佇列

上文一直都有提到任務隊列,那麼任務隊列到底是個什麼東東呢?舉例

在ES6的標準中,任務佇列分為巨集任務(macro-task)和微任務(micro-task)

1.macro-task包括:script(整體程式碼), setTimeout, setInterval, setImmediate, I/O, UI rendering

2.micro-task包含:process.nextTick, Promises, Object.observe, MutationObserver

事件循環的順序是從script開始第一次循環,隨後全局上下文進入函數調用棧,碰到macro-task就將其交給處理它的模組處理完之後將回調函數放進macro -task的佇列之中,碰到micro-task也是將其回呼函數放進micro-task的佇列之中。直到函式呼叫堆疊清空只剩下全域執行上下文,然後開始執行所有的micro-task。當所有可執行的micro-task執行完畢之後。迴圈再次執行macro-task中的一個任務佇列,執行完之後再執行所有的micro-task,就這樣一直循環。

接下來我們分析執行過程:

(function test() {
    setTimeout(function() {console.log(4)}, 0);
    new Promise(function executor(resolve) {
        console.log(1);
        for( var i=0 ; i<10000 ; i++ ) {
            i == 9999 && resolve();
        }
        console.log(2);
    }).then(function() {
        console.log(5);
    });
    console.log(3);
})()
登入後複製

1、全域上下文入棧,開始執行其中的程式碼。

2、執行到setTimeout,作為一個macro-task交給timer模組處理完成後將其回調函數放入自己的佇列中。

3、執行到Promise實例,將Promise入棧,第一個參數是在目前任務直接執行輸出1。

4、執行循環體,遇到resolve函數,入棧執行後出棧,改變promise狀態為Fulfilled,隨後輸出2

5、遇到then方法,作為micro-task ,進入Promise的任務隊列。

6、繼續執行程式碼,輸出3。

7、輸出3之後第一個巨集任務程式碼執行完畢,開始執行所有在佇列之中的微任務。 then的回呼函數入棧執行後出棧,輸出5。

8、這時候所有的micao-task執行完畢,第一輪迴圈結束。第二輪迴圈從setTimeout的任務佇列開始,setTimeout的回呼函數入棧執行完畢之後出棧,此時輸出4。

小結:

1、不同的任務會放進不同的任務佇列之中。

2、先執行macro-task,等到函數呼叫堆疊清空之後再執行所有在佇列之中的micro-task。

3、等到所有micro-task執行完之後再從macro-task中的一個任務佇列開始執行,就這樣一直循環。

4、當有多個macro-task(micro-task)佇列時,事件循環的順序是按上文macro-task(micro-task)的分類中書寫的順序執行的。

總結:

JS引擎在解析JS程式碼的時候,會建立一個主執行緒(main thread)和一個調用堆疊(call-stack),在對一個呼叫堆疊中的task處理的時候,其他的都要等著。

當執行到一些非同步操作的時候,會交給瀏覽器核心的其他模組處理(以webkit為例,是webcore模組),處理完成將task(回呼函數)放入任務隊列中。

一般不同的非同步任務的回呼函數都會放入不同的任務佇列中,等到呼叫堆疊中所有的task執行完畢,接著去執行任務佇列中的task(回呼函數)

相關文章推薦:

javascript中this關鍵字的用法(附程式碼)

 js資料型別有哪些? js的資料型別總結

以上是JS運作機制:同步、非同步以及事件循環(Event Loop)的分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
最新問題
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板