JavaScript在瀏覽器環境中的非同步

JohnKeatinghhh發表於2020-10-04


參考:

https://blog.csdn.net/qq_26222859/article/details/77622222
https://www.cnblogs.com/aaron—blog/p/10903118.html

單執行緒與多執行緒

JavaScript 預設情況下是單執行緒執行的(除了用H5 Web Workers),但執行js指令碼的環境可以是多執行緒的

眾所周知JavaScript是單執行緒執行的指令碼語言,即: 程逐行執行語句,上面的語句沒有執行完會阻塞下面語句的執行,但在不同的環境下,卻可以實現非同步的操作(如瀏覽器環境下,node環境下)。

瀏覽器的多執行緒

在JavaScript引擎中負責解析和執行JavaScript程式碼的執行緒只有一個。但是除了這個主程式以外,還有其他很多輔助執行緒。那麼諸如onclick回撥,setTimeoutAjax這些都是怎麼實現的呢?即瀏覽器搞了幾個其他執行緒去輔助JavaScript執行緒的執行。

瀏覽器有很多執行緒,例如:

  • GUI渲染執行緒 - 用於更新頁面
  • JavaScript引擎執行緒 - 用於解析JavaScript程式碼
  • 定時器觸發執行緒 - 瀏覽器定時計數器並不是 js引擎計數
  • 瀏覽器事件執行緒 - 用於解析BOM渲染等工作
  • http執行緒 - 主要負責資料請求
  • EventLoop輪詢處理執行緒 - 事件被觸發時該執行緒會把事件新增到待處理佇列的隊尾
  • 等等等

瀏覽器的核心是多執行緒的,它們在核心制控下相互配合以保持同步,一個瀏覽器至少實現三個常駐執行緒:javascript引擎執行緒GUI渲染執行緒瀏覽器事件觸發執行緒

瀏覽器的非同步邏輯

下面這張圖可以說明瀏覽器的非同步邏輯

JavaScript在瀏覽器環境中的非同步
  • 首先,js引擎解析js程式碼(稱為同步過程,即把整個頁面的js程式碼從頭到尾執行下來),當處理到與其他執行緒相關的程式碼,就會分發給其他執行緒(比如onclick, setTimeout,setInterval,ajax請求, 對DOM進行寫操作等等…此時的分發過程還是同步過程
  • 其他執行緒處理完之後(其他執行緒處理請求的過程就是非同步過程)會把需要js引擎執行的任務放入callback queue裡(也叫任務佇列、訊息佇列、事件佇列)。 這個過程中,js並不會阻塞當前執行緒,去等待其他執行緒執行完畢。
  • 且其他執行緒執行完畢後放進callback queue裡的事件,卻需要等待js引擎執行完畢當前stack裡的任務,空閒下來,才會被執行。

總結起來,JS執行緒不會等待其他執行緒執行,同樣地其他執行緒也不會等待JS執行緒(各司其職),而其他執行緒返回給JS執行緒的任務自然就需要在callback queue裡等待(其他執行緒交給JS引擎的任務,當然要等JS自己的事情先幹完)。

js引擎與GUI引擎是互斥的

雖然不同執行緒之間是互相獨立的,但當GUI引擎需要更新介面的時候,卻會受到JS執行緒的阻塞。

瞭解過 H5 Web Workers的朋友應該知道,在 H5 Web Workers出現之後,JS也可以“開掛” 實現多執行緒執行,當然這不在本文的討論範疇內。這裡只提一點, H5 Web Workers的一大用處就是讓js引擎中的耗時大的計算不影響介面的響應

例如,如果js引擎需要處理一段耗時很長的程式碼:遞迴計算斐波那契數列的第2000項…呵呵, 由於執行緒阻塞,此時頁面是無響應的,比如:

<input>標籤裡鍵入無響應,js實現的輪播圖不再播放等

所以此時可以把遞迴計算斐波那契數列的函式交給Workers分執行緒。

  • js引擎與GUI引擎是互斥的

這裡就引出了一個問題:js引擎與GUI引擎是互斥的,也就是說GUI引擎在渲染時會阻塞js引擎計算,反過來也一樣。原因很簡單,如果在GUI渲染的時候,js改變了dom,那頁面到底聽誰的?這就會造成渲染不同步。

這裡插句題外話,無論 H5 Web Workers如何神通廣大,都無法改變一個事實: 要實現對DOM的操作,還是隻能在js主執行緒上進行,而不能分到workers分執行緒上。原因也是不同執行緒的具體語句之間執行順序是不一定的,要避免不同執行緒對DOM修改造成衝突。

借用一下別人的例子:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
    <table border=1>
        <tr><td><button id='do'>Do long calc - bad status!</button></td>
            <td><div id='status'>Not Calculating yet.</div></td>
        </tr>
        <tr><td><button id='do_ok'>Do long calc - good status!</button></td>
            <td><div id='status_ok'>Not Calculating yet.</div></td>
        </tr>
    </table>    
<script>

function long_running(status_div) {

    var result = 0;
    for (var i = 0; i < 1000; i++) {
        for (var j = 0; j < 700; j++) {
            for (var k = 0; k < 300; k++) {
                result = result + i + j + k;
            }
        }
    }
    document.querySelector(status_div).innerHTML = 'calclation done' ;
}

document.querySelector('#do').onclick = function () {
    document.querySelector('#status').innerHTML = 'calculating....';
    long_running('#status');
};

document.querySelector('#do_ok').onclick = function () {
    document.querySelector('#status_ok').innerHTML = 'calculating....';
    window.setTimeout(function (){ long_running('#status_ok') }, 0);
};

</script>
</body>
</html>

我們希望能看到計算的每一個過程,我們在程式開始,計算,結束時,都執行了一個dom操作,插入了代表當前狀態的字串,Not Calculating yet.和calculating…和calclation done.計算中是一個耗時的3重for迴圈. 在沒有使用settimeout的時候,執行結果是由Not Calculating yet 直接跳到了calclation done.這顯然不是我們希望的.而造成這樣結果的原因正是js的事件迴圈單執行緒機制.dom操作是非同步的,for迴圈計算是同步的.非同步操作都會被延遲到同步計算之後執行.也就是程式碼的執行順序變了.calculating…和calclation done的dom操作都被放到事件佇列後面而且緊跟在一起,造成了丟幀.無法實時的反應.這個例子也告訴了我們,在需要實時反饋的操作,如渲染等,和其他相關同步的程式碼,要麼一起同步,要麼一起非同步才能保證程式碼的執行順序.在js中,就只能讓同步程式碼也非同步.即給for計算加上settimeout.

setTimeout(0): 手動非同步

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
    
    <script>
        alert(1); 
        setTimeout("alert(2)", 0); 
        alert(3); 
    </script>
</body>
</html>

上面這個程式碼的輸出順序是1->3->2,原因就是1和3是JS引擎中的同步過程,優先執行,而2是非同步過程,要等JS自己的同步過程執行完了,再被執行。

相關文章