關於譯者:這是一個流淌著滬江血液的純粹工程:認真,是 HTML 最堅實的樑柱;分享,是 CSS 裡最閃耀的一瞥;總結,是 JavaScript 中最嚴謹的邏輯。經過捶打磨練,成就了本書的中文版。本書包含了函數語言程式設計之精髓,希望可以幫助大家在學習函數語言程式設計的道路上走的更順暢。比心。
譯者團隊(排名不分先後):阿希、blueken、brucecham、cfanlife、dail、kyoko-df、l3ve、lilins、LittlePineapple、MatildaJin、冬青、pobusama、Cherry、蘿蔔、vavd317、vivaxy、萌萌、zhouyao
第 10 章:非同步的函式式(上)
閱讀到這裡,你已經學習了我所說的所有輕量級函數語言程式設計的基礎概念,在本章節中,我們將把這些概念應有到不同的情景當中,但絕對不會有新的知識點。
到目前為止,我們所說的一切都是同步的,意味著我們呼叫函式,傳入引數後馬上就會得到返回值。大部分的情況下是沒問題的,但這幾乎滿足不了現有的 JS 應用。為了能在當前的 JS 環境裡使用上函數語言程式設計,我們需要去了解非同步的函數語言程式設計。
本章的目的是擴充我們對用函數語言程式設計管理資料的思維,以便之後我們在更多的業務上應用。
時間狀態
在你所有的應用裡,最複雜的狀態就是時間。當你操作的資料狀態改變過程比較直觀的時候,是很容易管理的。但是,如果狀態隨著時間因為響應事件而隱晦的變化,管理這些狀態的難度將會成幾何級增長。
我們在本文中介紹的函數語言程式設計可以讓程式碼變得更可讀,從而增強了可靠性和可預見性。但是當你新增非同步操作到你的專案裡的時候,這些優勢將會大打折扣。
必須明確的一點是:並不是說一些操作不能用同步來完成,或者觸發非同步行為很容易。協調那些可能會改變應用程式的狀態的響應,這需要大量額外的工作。
所以,作為作者的你最好付出一些努力,或者只是留給閱讀你程式碼的人一個難題,去弄清楚如果 A 在 B 之前完成,專案中狀態是什麼,還有相反的情況是什麼?這是一個浮誇的問題,但以我的觀點來看,這有一個確切的答案:如果可以把複雜的程式碼變得更容易理解,作者就必須花費更多心思。
減少時間狀態
非同步程式設計最為重要的一點是通過抽象時間來簡化狀態變化的管理。
為說明這一點,讓我們先來看下一種有競爭狀態(又稱,時間複雜度)的糟糕情況,且必須手動去管理裡面的狀態:
var customerId = 42;
var customer;
lookupCustomer( customerId, function onCustomer(customerRecord){
var orders = customer ? customer.orders : null;
customer = customerRecord;
if (orders) {
customer.orders = orders;
}
} );
lookupOrders( customerId, function onOrders(customerOrders){
if (!customer) {
customer = {};
}
customer.orders = customerOrders;
} );
複製程式碼
回撥函式 onCustomer(..)
和 onOrders(..)
之間是互為競爭關係。假設他們都在執行,兩者都有可能先執行,那將無法預測到會發生什麼。
如果我們可以把 lookupOrders(..)
寫到 onCustomer(..)
裡面,那我們就可以確認 onOrders(..)
會在 onCustomer(..)
之後執行,但我們不能這麼做,因為我們需要讓 2 個查詢同時執行。
所以,為了讓這個基於時間的複雜狀態正常化,我們用相應的 if
-宣告在各自的回撥函式裡來檢查外部作用域的變數 customer
。當各自的回撥函式被執行,將會去檢測 customer
的狀態,從而確定各自的執行順序,如果 customer
在回撥函式裡還沒被定義,那他就是先執行的,否則則是第二個執行的。
這些程式碼可以執行,但是他違背了可讀性的原則。時間複雜度讓這個程式碼變得難以閱讀。
讓我們改用 JS promise 來把時間因素抽離出來:
var customerId = 42;
var customerPromise = lookupCustomer( customerId );
var ordersPromise = lookupOrders( customerId );
customerPromise.then( function onCustomer(customer){
ordersPromise.then( function onOrders(orders){
customer.orders = orders;
} );
} );
複製程式碼
現在 onOrders(..)
回撥函式存在 onCustomer(..)
回撥函式裡,所以他們各自的執行順序是可以保證的。在各自的 then(..)
執行之前 lookupCustomer(..)
和 lookupOrders(..)
被分別的呼叫,兩個查詢就已經並行的執行完了。
這可能不太明顯,但是這個程式碼裡還有其他內在的競爭狀態,那就是 promise 的定義沒有被體現出來。如果 orders
的查詢在把 onOrders(..)
回撥函式被 ordersPromise.then(..)
呼叫前完成,那麼就需要一些比較智慧的 東西 來儲存 orders
直到 onOrders(..)
能被呼叫。 同理,record
(或者說customer
)物件是否能在 onCustomer(..)
執行時被接收到。
這裡的 東西 和我們之前討論過的時間複雜度類似。但我們不必去擔心這些複雜性,無論是編碼或者是讀(更為重要)這些程式碼的時候,因為對我們來說,promise 所處理的就是時間複雜度上的問題。
promise 以時間無關的方式來作為一個單一的值。此外,獲取 promise 的返回值是非同步的,但卻是通過同步的方法來賦值。或者說, promise 給 =
操作符擴充套件隨時間動態賦值的功能,通過可靠的(時間無關)方式。
接下來我們將探索如何以相同的方式,在時間上非同步地擴充本書之前同步的函數語言程式設計操作。
積極的 vs 惰性的
積極的和惰性的在電腦科學的領域並不是表揚或者批評的意思,而是描述一個操作是否立即執行或者是延時執行。
我們在本例子中看到的函數語言程式設計操作可以被稱為積極的,因為它們同步(即時)地操作著離散的即時值或值的列表/結構上的值。
回憶下:
var a = [1,2,3]
var b = a.map( v => v * 2 );
b; // [2,4,6]
複製程式碼
這裡 a
到 b
的對映就是積極的,因為它在執行的那一刻對映了陣列 a
裡的所有的值,然後生成了一個新的陣列 b
。即使之後你去修改 a
,比如說新增一個新的值到陣列的最後一位,也不會影響到 b
的內容。這就是積極的函數語言程式設計。
但是如果是一個惰性的函數語言程式設計操作呢?思考如下情況:
var a = [];
var b = mapLazy( a, v => v * 2 );
a.push( 1 );
a[0]; // 1
b[0]; // 2
a.push( 2 );
a[1]; // 2
b[1]; // 4
複製程式碼
我們可以想象下 mapLazy(..)
本質上 “監聽” 了陣列 a
,只要一個新的值新增到陣列的末端(使用 push(..)
),它都會執行對映函式 v => v * 2
並把改變後的值新增到陣列 b
裡。
注意: mapLazy(..)
的實現沒有被寫出來,是因為它是虛構的方法,是不存在的。如果要實現 a
和 b
之間的惰性的操作,那麼簡單的陣列就需要變得更加聰明。
考慮下把 a
和 b
關聯到一起的好處,無論何時何地,你新增一個值進 a
裡,它都將改變且對映到 b
裡。它比同為宣告式函數語言程式設計的 map(..)
更強大,但現在它可以隨時地變化,進行對映時你不用知道 a
裡面所有的值。
** 【上一章】翻譯連載 | 第 9 章:遞迴(下)-《JavaScript輕量級函數語言程式設計》 |《你不知道的JS》姊妹篇 **
iKcamp原創新書《移動Web前端高效開發實戰》已在亞馬遜、京東、噹噹開售。
iKcamp官網:www.ikcamp.com
2019年,iKcamp原創新書《Koa與Node.js開發實戰》已在京東、天貓、亞馬遜、噹噹開售啦!