翻譯連載 | 第 10 章:異步的函數式(上)-《JavaScript輕量級函數式編程》 |《你不知道的JS》姊妹篇

關於譯者:這是一個流淌着滬江血液的純粹工程:認真,是 HTML 最堅實的樑柱;分享,是 CSS 裏最閃耀的一瞥;總結,是 JavaScript 中最嚴謹的邏輯。通過捶打磨練,成就了本書的中文版。本書包含了函數式編程之精髓,但願能夠幫助你們在學習函數式編程的道路上走的更順暢。比心。前端

譯者團隊(排名不分前後):阿希bluekenbrucechamcfanlifedailkyoko-dfl3velilinsLittlePineappleMatildaJin冬青pobusamaCherry蘿蔔vavd317vivaxy萌萌zhouyaogit

第 10 章:異步的函數式(上)

閱讀到這裏,你已經學習了我所說的全部輕量級函數式編程的基礎概念,在本章節中,咱們將把這些概念應有到不一樣的情景當中,但絕對不會有新的知識點。github

到目前爲止,咱們所說的一切都是同步的,意味着咱們調用函數,傳入參數後立刻就會獲得返回值。大部分的狀況下是沒問題的,但這幾乎知足不了現有的 JS 應用。爲了能在當前的 JS 環境裏使用上函數式編程,咱們須要去了解異步的函數式編程。編程

本章的目的是拓展咱們對用函數式編程管理數據的思惟,以便以後咱們在更多的業務上應用。數組

時間狀態

在你全部的應用裏,最複雜的狀態就是時間。當你操做的數據狀態改變過程比較直觀的時候,是很容易管理的。可是,若是狀態隨着時間由於響應事件而隱晦的變化,管理這些狀態的難度將會成幾何級增加。promise

咱們在本文中介紹的函數式編程可讓代碼變得更可讀,從而加強了可靠性和可預見性。可是當你添加異步操做到你的項目裏的時候,這些優點將會大打折扣。app

必須明確的一點是:並非說一些操做不能用同步來完成,或者觸發異步行爲很容易。協調那些可能會改變應用程序的狀態的響應,這須要大量額外的工做。異步

因此,做爲做者的你最好付出一些努力,或者只是留給閱讀你代碼的人一個難題,去弄清楚若是 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]
複製代碼

這裏 ab 的映射就是積極的,由於它在執行的那一刻映射了數組 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(..) 的實現沒有被寫出來,是由於它是虛構的方法,是不存在的。若是要實現 ab 之間的惰性的操做,那麼簡單的數組就須要變得更加聰明。

考慮下把 ab 關聯到一塊兒的好處,不管什麼時候何地,你添加一個值進 a 裏,它都將改變且映射到 b 裏。它比同爲聲明式函數式編程的 map(..) 更強大,但如今它能夠隨時地變化,進行映射時你不用知道 a 裏面全部的值。

** 【上一章】翻譯連載 | 第 9 章:遞歸(下)-《JavaScript輕量級函數式編程》 |《你不知道的JS》姊妹篇 **

iKcamp原創新書《移動Web前端高效開發實戰》已在亞馬遜、京東、噹噹開售。

iKcamp官網:www.ikcamp.com


2019年,iKcamp原創新書《Koa與Node.js開發實戰》已在京東、天貓、亞馬遜、噹噹開售啦!

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息