剖析瀏覽器執行JavaScript

JavaScript 單線程

單線程,即只有一個主線程。同一時刻只能運行一行代碼、同一時刻不能處理多個任務(不支持並行)。

假設JS同時有2個線程,線程A在某個DOM節點添加內容,線程B刪除該節點,瀏覽器應該以哪個線程爲準?爲避免複雜性,JavaScript設計之初就確定爲“單線程”。

瀏覽器是事件驅動(Event Driven),JS運行在瀏覽器中是單線程的JS也必須遵循事件驅動的規則)。

JavaScript引擎

JavaScript是專門處理JS腳本的虛擬機,通常會附在瀏覽器裏面,例如Chrome的V8引擎。

任務隊列

單線程意味着所有任務執行前需要排隊,前一個任務執行結束,後一個任務纔會執行。如果前一個任務耗時很長,則後一個任務就必須等待,從而無法執行。

所有任務分爲同步任務和異步任務:

  • 同步任務(synchronous)

主線程中排隊執行的任務,只有前一個任務執行完成,才能執行後一個任務。

JS中常見的同步行爲:if-else、for、while等

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=<device-width>, initial-scale=1.0">
    <title>JS Promise Study</title>
</head>
<body>
    <script>
        console.log("start...");
        let str = "hello world";
        console.log(str);
        if (str.length > 0) {
            console.log(str.length)
        } else {
            console.log(-1);
        }
        var cur= Date.now();
        for (var i = 0; i < 10000; i++) {}
        // 每次輸出結果都不一樣,每次for循環時間不一樣
        console.log(Date.now()-cur);
        console.log("Today is a good day.");
        console.log("end...");
    </script>
</body>
</html>

瀏覽器輸入index.html之後,打開調試界面運行結果如下圖所示:

index.hml代碼解析:

  • 異步任務(asynchronous)
不進入主線程,而進入“任務隊列(task queue)”,只有等主線程任務全部執行完成之後,“任務隊列”開始通知主線程,請求執行任務,“任務隊列”中的任務纔會進入主線程執行。
 
JS中常見的異步行爲:定時器(setTimeout函數,setInterval函數)、ajax(有異步,也有同步)、事件(onload onclick)、promise、async、await。
 

執行棧

函數執行時會生成新的執行上下文(execution context),執行上下文包含當前函數的參數、局部變量之類的信息,這些信息被推入棧中。正在執行的上下文(running execution context)始終處於棧頂,函數執行結束後,它的執行上下文從棧中彈出。

 
function bar() {
console.log('bar');
}

function foo() {
console.log('foo');
bar();
}

foo();

執行過程中棧的變化:

 

異步運行機制

異步運行機制一般由4步組成:
注:同步執行可被視爲沒有異步任務的異步執行,是特殊的異步運行。
 
第一步:所有同步任務都在主線程上執行,形成一個執行棧概念如上)。
第二步:主線程之外,存在一個“任務隊列(task queue)”。只要異步任務有了運行結果,就在“任務隊列”中放置一個事件。
第三步:一旦“執行棧”中所有任務執行完畢,系統會讀取“任務隊列”。開始執行任務。
第四步:主線程不斷重複第三步。
 
只要主線程空了,就會讀取“任務隊列”。這個過程會不斷重複,這就是JavaScript的運行機制。
 
任務隊列也可以理解爲消息隊列
 
“回調函數”:被主線程掛起來的代碼。異步任務必須指定回調函數,主線程開始執行異步任務,就是執行對應的回調函數。例如,ajax的success, complete, error都是指定各自的回調函數。這些函數加入“任務隊列”,等待執行。
 

事件循環機制

上圖解釋:

  • 同步和異步任務分別進入不同的執行"場所",同步的進入主線程,異步的進入Event Table並註冊函數。
  • 當指定的事情完成時,Event Table會將這個函數移入Event Queue。
  • 當棧中的代碼執行完畢,執行棧(call stack中的任務爲空時,就會讀取任務隊列(Event quene)中的事件,去執行對應的回調
  • 如此循環,形成js的事件循環機制(Event Loop

Event Table是個註冊站:調用棧讓Event Table註冊一個函數。例如,註冊某函數A(該函數5s之後被調用)。

Event Queue:任務隊列,實質是個緩衝區域。例如,5s之後函數A會被移至Event Queue,A等着被調用並移到調用棧。

Event Loop:事件循環。JavaScript引擎有monitoring process(監督器)會持續不斷檢查執行棧是否爲空,一旦爲空,就會檢查Event Queue中是否有被等待調用的函數。如果存在,監督器就會調用此函數並移至執行棧中。如果Event Queue爲空,監督器會繼續不定期檢查。整個過程就是Event Loop。

另外一種解釋

事件循環描述如下:

1. 函數入棧,當Stack中執行到異步任務的時候,就將他丟給WebAPIs,接着執行同步任務,直到Stack爲空;

2. 在此期間WebAPIs完成這個事件,把回調函數放入CallbackQueue中等待;

3. 當執行棧爲空時,Event Loop把Callback Queue中的一個任務放入Stack中,回到第1步。

Call Stack (棧):存儲同步任務(立即執行、不耗時的任務,例如:初始化變量、綁定事件等。)

Memory Heap(堆):存儲聲明的變量、對象。

Callback Queue(消息隊列):一旦某個異步任務有了響應就會被添加至隊列中,即存放異步任務的回調函數。例如,點擊事件、瀏覽器收到服務請求響應等。

Event Loop(事件循環):由JavaScript宿主環境(例如瀏覽器)實現。

Web APIs:Web API的統稱,主要用於瀏覽器交互效果。Web API是瀏覽器提供的一套操作瀏覽器功能和頁面元素的API(BOM和DOM)。

兩種解釋不矛盾,二者關係如下圖所示:

解釋1爲從JS引擎角度出發,解釋2從瀏覽器角度出發。可以理解爲一個是內部,一個是外部。

實例說明1

var start=new Date();
setTimeout(function cb(){
    console.log("時間間隔:",new Date()-start+'ms');
},500);
while(new Date()-start<1000){};
  1. main(Script) 函數入棧,start變量開始初始化。
  2. setTimeout入棧,出棧,丟給WebAPIs,開始定時500ms;
  3. while循環入棧,開始阻塞1000ms;
  4. 500ms過後,WebAPIs把cb()放入任務隊列,此時while循環還在棧中,cb()等待;
  5. 又過了500ms,while循環執行完畢從棧中彈出,main()彈出,此時棧爲空。Event Loop,cb()進入棧,log()進棧,輸出'時間間隔:1003ms',出棧,cb()出棧。

實例說明2

爲了能更好理解上面的內容,不解釋,直接上代碼
 
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=<device-width>, initial-scale=1.0">
    <title>JS Promise Study</title>
</head>
<body>
    <script>
        console.log("start...");
        setTimeout(function() {
            console.log("第一次異步調用。。。");
        }, 1000);
        let str = "hello world";
        console.log(str);
        if (str.length > 0) {
            console.log(str.length)
        } else {
            console.log(-1);
        }
        var cur= Date.now();
        for (var i = 0; i < 10000; i++) {}
        // 每次輸出結果都不一樣,每次for循環時間不一樣
        console.log(Date.now()-cur);
        setTimeout(function() {
            console.log("第二次異步調用。。。");
        }, 1000);
        console.log("Today is a good day.");
        setTimeout(function() {
            console.log("第三次異步調用。。。");
        }, 500);
        setTimeout(function() {
            console.log("第四次異步調用。。。");
        }, 200);
        console.log("end...");
        setTimeout(function() {
            console.log("第五次異步調用。。。");
        }, 100);
        setTimeout(function() {
            console.log("第六次異步調用。。。");
        }, 1600);
        setTimeout(function() {
            console.log("第七次異步調用。。。");
        }, 2500);
        setTimeout(function() {
            console.log("第八次異步調用。。。");
        });
    </script>
</body>
</html>

運行結果:

等待時間越短,CPU響應優先級越高。

消息1-消息8,

時間響應內容:消息1(1000),消息2(1000), 消息3(500),消息4 (200),消息5( 100),消息6( 1600), 消息7(2500),消息8( 默認爲0)

響應優先級(從大到小):消息8( 默認爲0),消息5( 100),消息4 (200),消息3(500),消息1(1000),消息2(1000),消息6( 1600), 消息7(2500)

消息1:爲函數設定響應時間1000ms

消息2:爲函數設定響應時間1000ms

。。。。

注:響應時間相同時,越在上面越早等待,優先級越高。

儘管我們設置了setTimeout(function,time)中的等待時間爲0,結果其中的function還是後執行。

火狐瀏覽器的api文檔有這樣一句話:Because even though setTimeout was called with a delay of zero, it's placed on a queue and scheduled to run at the next opportunity, not immediately. Currently executing code must complete before functions on the queue are executed, the resulting execution order may not be as expected.

意思就是:儘管setTimeout的time延遲時間爲0,其中的function也會被放入一個隊列中,等待下一個機會執行,當前的代碼(指不需要加入隊列中的程序)必須在該隊列的程序完成之前完成,因此結果可能不與預期結果相同。

宏任務(Macrotasks)

每次執行棧執行的代碼就是一個宏任務[macro-task(Task) ]。執行棧代碼:同步任務和異步任務(event loop)。

異步任務來源:(事件隊列中的每一個事件都是一個macrotask)

  1. setTimeout
  2. setInterval
  3. setImmediate
  4. I/O
  5. UI rendering

宏任務執行期間不會執行其他任務(JS單線程本質)。

瀏覽器爲了使JS內部的task與DOM任務能夠有序執行,在一個task執行結束後,在下一個task執行開始前,重新渲染頁面。

(task1 -> 渲染 -> task2 ...)

微任務(Microtasks)

當前任務執行結束後立即執行的任務[micro-task(Job) 微任務],在渲染之前。宏任務無需等待渲染,因此宏任務執行完成之後,在其執行期間產生的微任務都會在渲染前執行完畢。一個event loop中只有一個microtask隊列。

微任務的任務源

  • process.nextTick
  • Promise
  • Object.observe
  • MutationObserver
宏任務執行結束之後,繼續執行相關的微任務(process.nextTick 、 Promise、Object.observe、MutationObserver)部分,微任務執行結束之後,繼續執行下一個宏任務。
 
流程圖如下所示:
 
 
  • 執行一個宏任務(棧中沒有就從事件隊列中獲取)
  • 執行過程中如果遇到微任務,就將它添加到微任務的任務隊列中
  • 宏任務執行完畢後,立即執行當前微任務隊列中的所有微任務(依次執行)
  • 當前宏任務執行完畢,開始檢查渲染,然後GUI線程接管渲染
  • 渲染完畢後,JS線程繼續接管,開始下一個宏任務(從事件隊列中獲取)

實例說明

<script>
    console.log("start...");

    console.log("test 01");

    Promise.resolve().then(function promise1 () {
        console.log('promise1');
    });

    console.log("test 02");

    Promise.resolve().then(function promise1 () {
        console.log('promise2');
    });

    setTimeout(function setTimeout1 () {
        console.log('setTimeout1');
        Promise.resolve().then(function promise2 () {
            console.log('promise5');
        });
        console.log('setTimeout1-1');
    }, 0);

    Promise.resolve().then(function promise1 () {
        console.log('promise3');
    });

    console.log("test 03");

    setTimeout(function setTimeout2 () {
        console.log('setTimeout2')
    }, 0);

    Promise.resolve().then(function promise1 () {
        console.log('promise4');
    });

    console.log("end...");
</script>

運行結果:

參考鏈接:

 
 
http://www.ruanyifeng.com/blog/2014/10/event-loop.html (JavaScript 運行機制詳解:再談Event Loop)
 
https://www.cnblogs.com/wxcbg/p/11040362.html(瀏覽器事件循環機制(event loop))
 
https://www.jianshu.com/p/667a20d008cf (JS JavaScript事件循環機制)
 
https://www.bilibili.com/video/av82229058/ (瀏覽器多進程與JS單線程-深入淺出)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章