【《你不知道的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引擎在編譯階段執行的優化都是安全的優化。