1. 程式人生 > 實用技巧 >【《你不知道的JS(中卷②)》】一、 非同步:現在與未來

【《你不知道的JS(中卷②)》】一、 非同步:現在與未來

一、非同步:現在與未來:

如何表達和控制持續一段時間的程式行為,是使用類似JS這樣的語言程式設計時,很重要但常常被誤解的一點。

持續一段時間,不是指類似於 for迴圈開始到結束的過程。而是指 程式的一部分現在執行,而另一部分則在未來執行。現在與將來之間有一段間隙,這段間隙在實際程式中,可以是等待使用者輸入、從資料庫或檔案系統中請求資料、通過網路傳送資料並等待響應,或者是在以固定時間間隔執行重複任務(比如動畫)。

​ 管理這段間隙,就是非同步程式設計的核心。本章將深入探討非同步的概念及其在JS中的運作模式。

一)、分塊的程式:

​ 無論JS程式是在多個JS檔案或一個JS檔案,JS中都是都是由一個個塊組成。同一時間只有一個塊可以在 現在

執行。最常見的塊就是 函式

​ 例如:

var data = ajax("http://some.url.1");
console.log(data);  // data通常並不會出現Ajax的結果

​ 因為標準Ajax請求不是同步完成的,當列印data時,ajax函式可能還沒有返回任何值給變數data。

​ 如何能夠確定得到ajax函式的返回值?1、將ajax(...)能夠阻塞到響應返回,即發出ajax請求後什麼事也不做,直到得到返回值。這與我們希望將一部分值在 未來執行是相違背的。

現在我們發出一個非同步Ajax請求,然後在將來才能得到返回的結果。實現“等待”的最簡單方法,是使用 回撥函式

ajax("http://some.url.1", function myCallbackFunction(data){
    console.log(data);  // 這裡得到資料
})

​ 另外,

function now() {
    return 21;
}

function later() {
    answer = answer * 2;
    console.log("Meaning of life:", answer);
}

var answer = now();

setTimeout(later, 1000);  // Meaning of life: 42

任何時候,只要把一段程式碼包裝稱一個函式,並指定它在響應某個時間(定時器、滑鼠點選、Ajax響應等)時執行,你就在程式碼中建立了一個將來執行的塊,也就在這個程式中引入了非同步機制*。

  • 不同的瀏覽器和JS環境可能有不同的控制檯非同步實現。

二)、事件迴圈:

​ JS引擎並不是獨立執行的,它執行在宿主環境中,大多數為Web瀏覽器,也有如Node.js等伺服器端環境。

​ 這些環境提供一種機制來處理程式中多個塊的執行,且執行每塊時呼叫JS引擎,這種機制被稱為 事件迴圈。 可以按照下面的虛擬碼來理解事件迴圈:

var eventLoop = [];
var event;

//“永遠”執行
while (true) {
    // 一次tick
    if (eventLoop.length > 0) {
        event = eventLoop.shift();
        try {
            event();
        }
        catch (err) {
            reportError(err);
        }
    }
}

setTimeout(..)無法將回調函式直接掛在事件迴圈佇列中,只能通過定時器,讓環境將回調函式放進去。因此setTimeout(..)方法精度不是很高。

三)、並行執行緒:

”非同步“與”並行“常常被混為一談,但實際上它們的意義完全不同。

  • 非同步:關於現在將來的時間間隙。

    事件迴圈把自身的工作分成一個個任務並順序執行,不允許對共享記憶體並行訪問和修改。通過分立執行緒中彼此合作的事件迴圈,並行和順序執行可以共存。

  • 並行:關於能夠同時發生的事情。

    並行最常見的工具有程序執行緒。程序與執行緒獨立執行,並可能同時執行,多個執行緒能夠共享單個程序的記憶體。

  • JS不支援跨執行緒共享資料,並且具有 完整執行特性,即如果有兩個函式執行,兩個函式不會交替執行,一定是先完整執行第一個函式,然後才是第二個函式。

    雖然JS不需要考慮 執行緒層次的不確定性,但是依然存在 競態條件,考慮下面的程式碼:

var a = 1;
var b = 2;

function foo() {
    a++;
    b = b * a;
    a = b + 3;
}

function bar() {
    b--;
    a = 8 + b;
    b = a * 2;
}

ajax("http;//some.url.1", foo);
ajax("http;//some.url.2", bar);

​ 這一段程式碼可能有很多的輸出,這種不確定性來自於 函式(事件)順序級別上,換句話說,這種不確定性低於多執行緒情況

四)、併發:

​ 設想一個狀態更新列表,隨著使用者向下滾動列表而住就按載入更多內容。這裡需要兩個”程序“,一個監聽頁面滾動觸發onscroll,併發起Ajax請求,另一個接受Ajax請求,並把內容展示到頁面。當用戶滾動頁面時,可能在等待第一個響應的同時,就會有第二、第三個請求發出。

​ 兩個或多個”程序“同時執行就出現了併發,不管組成它們的單個運算是否 並行執行(在獨立的處理器或處理器核心上同時執行)。可以把併發看作”程序級的並行,與運算級的並行(不同處理器上的執行緒)相對。

  • 如果併發的”程序“需要通過作用域DOM間接監護,就需要對互動進行協調,避免競態的出現。

五)、任務:

​ 在ES6中,有一個新的概念建立在事件迴圈佇列之上,叫做任務佇列(job queue),這個概念用於Promise的非同步特性。

​ 任務佇列是掛在前文提到的事件迴圈佇列的每個tick之後的一個佇列,但是不是被新增到佇列末尾,而可以直接插隊,優先處理,也即:”儘可能早的將來“

六)、語句順序:

​ 程式碼中語句的順序和JS引擎執行語句的順序並不一定要一致。JS引擎在編譯程式碼之後,可能會對語句的順序進行重新安排,以提高執行速度。可以保證的是,JS引擎在編譯階段執行的優化都是安全的優化。