這篇文章帶給大家的內容是關於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(回呼函數)
相關文章推薦:
以上是JS運作機制:同步、非同步以及事件循環(Event Loop)的分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!