JavaScript執行順序分析

風靈使發表於2018-06-03

前言

被問到了事件執行順序的問題,想起來之前看《深入淺出Node.js》時看到這一章就忽略了,這次來分析一下JavaScript的事件執行順序。廢話少說,正題開始。

單執行緒JavaScript

首先我們要知道JavaScript是一門單執行緒解釋型語言。這就意味著在同一個時間下,我們只能執行一條命令。之所以它是一門單執行緒語言,和它的用途有關。
JavaScript設計出來的初衷是為了增強瀏覽器與使用者的互動,尤其是表單的互動,而之後的Ajax技術也是為了使表單的互動更加人性化而發明出來的。因為JavaScript是一門解釋型的語言,而直譯器內嵌於瀏覽器,這個直譯器是單執行緒的。
之所以不設計成多執行緒是因為渲染網頁的時候多執行緒容易引起死鎖或者資源衝突等問題。但是瀏覽器本身是多執行緒的,比如解釋執行JavaScript的同時還在載入網路資源。

Why doesn’t JavaScript support multithreading?

事件迴圈

單執行緒就意味著如果你要執行很多命令,那麼這些命令需要排序,一般情況下,這些命令是從上到下排序執行(因為直譯器是從檔案頂部開始)。比如以下程式碼是按照順序執行的。

console.log("1");
console.log("2");
console.log("3");
//1
//2
//3

但是我們還有知道在JavaScript裡有非同步程式設計的說法,比如AjaxsetTimeoutsetInterval或者ES6中的Promiseasyncawait

那麼什麼是同步和非同步呢?

一條命令的執行在計算機裡的意思就是它此時在使用CPU等資源,那麼因為想要獲得CPU資源的命令有很多,而CPU執行命令也需要時間去運算獲得結果,於是就有了同步非同步的概念。

同步就是在發出一個CPU請求時,在沒有得到結果之前,該CPU請求就不返回。但是一旦呼叫返回,就得到返回值了。

非同步表示CPU請求在發出之後,這個呼叫就直接返回了,所以沒有返回結果。在執行結束後,需要通過一系列手段來獲得返回值

這時候就要引入程式和執行緒的概念。

程式與執行緒

程式

概念:程式是一個具有一定獨立功能的程式在一個資料集上的一次動態執行的過程,是作業系統進行資源分配和排程的一個獨立單位,是應用程式執行的載體。

執行緒

由於程式對於CPU的使用是輪流的,那麼就存在程式的切換,但是由於現在的程式都比較大,切換的開銷很大會浪費CPU的資源,於是就發明了執行緒,把一個大的程式分解成多個執行緒共同執行。

區別

  • 程式是作業系統分配資源的最小單位,執行緒是程式執行的最小單位。
  • 一個程式由一個或多個執行緒組成,執行緒是一個程式中程式碼的不同執行路線;
  • 程式之間相互獨立,但同一程式下的各個執行緒之間共享程式的記憶體空間(包括程式碼段、資料集、堆等)及一些程式級的資源(如開啟檔案和訊號)。
  • 排程和切換:執行緒上下文切換比程式上下文切換要快得多。

舉個例子

假如我是鳴人,我想吃很多拉麵,如果我一個人吃10碗的話,那我就是一個程式一個執行緒完成吃拉麵這件事情。
但是如果我用9個分身和我一起吃10碗拉麵,那我就是一個程式用9個執行緒去完成吃拉麵這件事情。
而多程式這表示名人在一樂拉麵裡面吃拉麵的同時,好色仙人在偷看妹子洗澡~ ~。好色仙人是單程式單執行緒去偷看的哦!

瀏覽器的執行緒

瀏覽器的核心是多執行緒的,在核心控制下各執行緒相互配合以保持同步,一個瀏覽器通常由一下執行緒組成:

  • GUI 渲染執行緒
  • JavaScript引擎執行緒
  • 事件觸發執行緒
  • 非同步http請求執行緒
  • EventLoop輪詢的處理執行緒

這些執行緒的作用:

  • UI執行緒用於渲染頁面
  • js執行緒用於執行js任務
  • 瀏覽器事件觸發執行緒用於控制互動,響應使用者
  • http執行緒用於處理請求,ajax是委託給瀏覽器新開一個http執行緒
  • EventLoop處理執行緒用於輪詢訊息佇列

圖片

JavaScript事件迴圈和訊息佇列(瀏覽器環境)

因為JavaScript是單執行緒的,而瀏覽器是多執行緒的,所以為了執行不同的同步非同步的程式碼,JavaScript執行的環境採用裡事件迴圈和訊息佇列來達到目的。
每個執行緒的任務執行順序都是FIFO(先進先出)
JavaScript執行的環境中,有一個負責程式本身的執行,作為主執行緒;另一個負責主執行緒與其他執行緒的通訊,被稱為Event Loop 執行緒。
每當主執行緒遇到非同步的任務,把他們移入到Event Loop 執行緒,然後主執行緒繼續執行,等到主執行緒完全執行完之後,再去Event Loop 執行緒拿結果。
而每個非同步任務都包含著與它相關聯的資訊,比如執行狀態,回撥函式等。

事件迴圈

由此我們可以知道,同步任務和非同步任務會被分發到不同的執行緒去執行。
現在我們就可以分析一下一下程式碼的執行結果了。

setTimeout(()=>{console.log("我才是第一");},0);
console.log("我是第一");
  1. 因為setTimeout是非同步的事件,所以主執行緒把它調入Event Loop執行緒進行註冊。
  2. 主執行緒繼續執行console.log("我是第一");
  3. 主執行緒執行完畢,從Event Loop 執行緒讀取回撥函式。再執行console.log("我才是第一");;

setTimeoutsetInterval

setTimeout

這裡值得一提的是,setTimeout(callback,0)指的是主執行緒中的同步任務執行完了之後立刻由Event Loop 執行緒調入主執行緒。
而計時是在調入Event Loop執行緒註冊時開始的,此時setTimeout的回撥函式執行時間與主執行緒執行結束的時間相關。
關於setTimeout要補充的是,即便主執行緒為空,0毫秒實際上也是達不到的。根據HTML的標準,最低是4毫秒。

setInterval

需要注意的是,此函式是每隔一段時間將回撥函式放入Event Loop執行緒。
一旦setInterval的回撥函式fn執行時間超過了延遲時間ms,那麼就完全看不出來有時間間隔了
micro-task(微任務)macro-task(巨集任務)

Event Loop執行緒中包含任務佇列(用來對不同優先順序的非同步事件進行排序),而任務佇列又分為macro-task(巨集任務)micro-task(微任務),在最新標準中,它們被分別稱為taskjobs

  • macro-task大概包括:script(整體程式碼), setTimeout, setInterval, setImmediate, I/O, UI rendering。
  • micro-task大概包括: process.nextTick, Promise, Object.observe(已廢棄),
    MutationObserver(html5新特性)
  • setTimeout/Promise等我們稱之為任務源。而進入任務佇列的是他們指定的具體執行任務(回撥函式)。

來自不同的任務源的任務會進入到不同的任務佇列中,而不同的任務佇列執行過程如下:
執行過程如下:
JavaScript引擎首先從macro-task中取出第一個任務,
執行完畢後,將micro-task中的所有任務取出,按順序全部執行;
然後再從macro-task中取下一個,
執行完畢後,再次將micro-task中的全部取出;
迴圈往復,直到兩個佇列中的任務都取完。
舉個大例子

console.log("start");
var promise = new Promise((resolve) => {
    console.log("promise start..");
    resolve("promise");
}); //3
promise.then((val) => console.log(val));
setTimeout(()=>{console.log("setTime1")},0);
console.log("test end...")

這裡我們按順序來分析。
第一輪

  1. 整體script程式碼作為一個巨集任務進入主執行緒,執行console.log("start");
  2. 然後遇到Promises直接執行console.log("promise start..")
  3. 然後遇到promise.then,存入到micro-task佇列中。
  4. 然後遇到setTimeout,存入到macro-task佇列中。
  5. 於然後執行console.log("test end...");
  6. 在這一輪中,巨集任務執行結束,執行micro-task佇列中的 promise.then,輸出promise

第二輪

  1. 取出macro-task佇列中的setTimeout,執行console.log("setTime1");

結果

輸出的順序就是

// start
// promise start
// test end...
// promise
//setTime1

留一個案例你們去分析

async function testSometing() {
    console.log("執行testSometing");
    return "testSometing";
}

async function testAsync() {
    console.log("執行testAsync");
    return Promise.resolve("hello async");
}

async function test() {
    console.log("test start...");
    const v1 = await testSometing();
    console.log(v1);
    const v2 = await testAsync();
    console.log(v2);
    console.log(v1, v2);
}

test();

var promise = new Promise((resolve) => {
    console.log("promise start..");
    resolve("promise");
}); //3
promise.then((val) => console.log(val));
setTimeout(()=>{console.log("setTime1")},3000);
console.log("test end...")

相關文章