1.非同步 程式中現在運行的部分和將來運行的部分之間的關係是非同步編程的核心。 多數JavaScript開發者從來沒有認真思考過自己程式中的非同步到底是如何出現的,以及為什麼會出現,也沒有探索過處理非同步的其他方法。一直以來,低調的回調函數就算足夠好的方法了。目前為止,還有很多人堅持認為回調函數完全夠用。 ...
1.非同步
程式中現在運行的部分和將來運行的部分之間的關係是非同步編程的核心。
多數JavaScript開發者從來沒有認真思考過自己程式中的非同步到底是如何出現的,以及為什麼會出現,也沒有探索過處理非同步的其他方法。一直以來,低調的回調函數就算足夠好的方法了。目前為止,還有很多人堅持認為回調函數完全夠用。
但是,作為在瀏覽器、伺服器以及其他能夠想到的任何設備上運行的一流編程語言,JavaScript面臨的需求日益擴大。為了滿足這些需求,JavaScript的規模和複雜性也在持續增長,對非同步的管理也越來越令人痛苦,這一切都迫切需要更強大、更合理的非同步方法。
1.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
setTimeout(...)
設置了一個事件(定時)在將來執行,所以函數later()
的內容會在之後的某個時間(從現在起1000毫秒之後)執行。
任何時候,只要把一段代碼包裝成一個函數,並指定它在響應某個事件(定時器、滑鼠點擊、Ajax響應等)時執行,你就是在代碼中創建了一個將來執行的塊,也由此在這個程式中引入了非同步機制。
1.2 事件迴圈
現在我們來澄清一件事情(可能令人震驚):儘管你顯然能夠編寫非同步JavaScript代碼,但直到最近(ES6),JavaScript才真正內建有直接的非同步概念。
JavaScript引擎並不是獨立運行的,它運行在宿主環境中,對多數開發者來說通常就是Web瀏覽器。經過最近幾年的發展,JavaScript已經超過了瀏覽器的範圍,進入了其他環境,比如通過像Node.js這樣的工具進入伺服器領域。實際上,JavaScript現如今已經嵌入到了從機器人到電燈泡等各種各樣的設備中。
所有這些環境都提供了一種機制來處理程式中多個塊的執行,且執行每個塊時調用JavaScript引擎,這種機制被稱為事件迴圈。
ES6中Promise對事件迴圈隊列的調度運行能夠直接進行精細控制。
1.3 並行
非同步是關於現在和將來的時間間隙,而並行是關於能夠同時發生的事情。
var a = 20;
function foo(){
a = a + 1;
}
function bar(){
a = a * 2;
}
ajax("...",foo);
ajax("...",bar);
由於JavaScript的單線程特性,foo()
和bar()
中的代碼具有原子性。也就是說,一旦foo()
開始運行,它的所有代碼都會在bar()
中的任意代碼運行之前完成,或者相反。這稱為完整運行特性。
1.4 併發
兩個或多個“進程”同時執行就出現了併發。這裡的“進程”之所以打上引號,是因為這並不是電腦科學意義上的真正操作系統級進程。這是虛擬進程,或者任務,表示一個邏輯上相關的運算序列。
1.5 任務
在ES6中,有一個新的概念建立在事件迴圈隊列之上,叫作任務隊列。這個概念給大家帶來的最大影響可能是Promise的非同步特性。
事件迴圈隊列類似於一個游樂園游戲:玩過了一個游戲之後,你需要重新到隊尾排隊才能再玩一次。而任務隊列類似於玩過了游戲之後,插隊接著繼續玩。
2.回調
回調是編寫和處理JavaScript程式非同步邏輯的最常用方式。
回調函數是JavaScript的非同步主力軍,並且它們不辱使命地完成了自己的任務。
2.1 continuation
//A
ajax("...",function(data){
//C
});
//B
//A
和//B
表示程式的前半部分,而//C
標識了程式的後半部分。前半部分立刻執行,然後是一段時間不確定的停頓。在未來的某個時刻,如果Ajax調用完成,程式就會從停下的位置繼續執行後半部分。
信任的問題
//C
會延遲到將來發生,並且在第三方的控制下。我們把這稱為控制反轉,也就是把自己程式一部分的執行控制交給某個第三方。在你的代碼和第三方工具之間有一份並沒有明確表達的契約。
//過分信任輸入
function addNumbers(x,y){
return x + y;
}
addNumbers(21,21);//42
addNumbers(21,"21");//"2121"
//針對不信任輸入的防禦性代碼
function addNumbers(x,y){
if(typeof x != "number" || y != "number"){
throw Error("Bad parameters");
}
return x + y;
}
addNumbers(21,21);//42
addNumbers(21,"21");//Error: "Bad parameters"
//依舊安全但更好一些
function addNumbers(x,y){
x = Number(x);
y = Number(y);
return x + y;
}
addNumbers(21,21);//42
addNumbers(21,"21");//42
3.Promise
通過回調表達程式非同步和管理併發的兩個主要缺陷:缺乏順序性和可信任性。
我們用回調函數來封裝程式中的continuation,然後把回調交給第三方,期待其能夠調用回調,實現正確的功能。通過這種形式,我們要表達的意思是:“這是將來要做的事情,要在當前的步驟完成之後發生”。
如果我們不把自己程式的continuation傳給第三方,而是希望第三方給我們提供瞭解其任務何時結束的能力,然後我們自己的代碼來決定下一步做什麼。這種範式就稱為Promise。
絕大多數JavaScript/DOM平臺新增的非同步API都是基於Promise構建的。
4.生成器
我們把註意力轉移到一種順序、看似同步的非同步流程式控制製表達風格。使這種風格成為可能的“魔法”就是ES6生成器(generator)。
4.1 打破完整運行
var x = 1;
//下麵是生成器函數
function *foo(){
x++;
yield;//暫停點
console.log("x: ",x);
}
function bar(){
x++;
}
var it = foo();//構造迭代器
it.next();//啟動foo()
x;//2
bar();
x;//3
it.next();//x: 3
註意:function* foo(){...}
、function *foo(){...}
是一樣的,唯一區別是*
位置的風格不同。function*foo(){...}
(沒有空格)也一樣,這隻是風格偏好問題。
上述代碼的運行過程:
it = foo()
運算並沒有執行生成器*foo()
,而只是構造了一個迭代器(iterator),這個迭代器會控制它的執行。- 第一個
it.next()
啟動了生成器*foo()
,並運行了*foo()
第一行的x++
。 *foo()
在yield
語句處暫停,在這一點上第一個it.next()
調用結束。- 我們查看x的值,此時為2。
- 我們調用
bar()
,它通過x++
再次遞增x。 - 我們再次查看x的值,此時為3.
- 最後的
it.next()
調用從暫停處恢復了生成器*foo()
的執行,並運行console.log(...)
語句,這條語句使用當前x的值3。
相關閱讀:知乎上關於生成器的解釋
4.2 生成器+Promise
ES6中最完美的世界就是生成器(看似同步的非同步代碼)和Promise(可信任可組合)的組合。
獲得Promise和生成器最大效用的最自然的方法就是yield出來一個Promise,然後通過這個Promise來控制生成器的迭代器。
推薦閱讀:ECMAScript 6 入門
參考資料:《你不知道的JavaScript》(中捲) 第二部分 非同步和性能