RxJS 函數式與響應式編程
- 2019 年 11 月 5 日
- 筆記
什麼是函數式編程
簡單說,」函數式編程」是一種 「編程範式」(programming paradigm),也就是如何編寫程序的方法論。
函數式編程基本要素
所謂 「一等公民」(first class),指的是函數與其他數據類型一樣,處於平等地位,可以賦值給其他變量,也可以作為參數,傳入另一個函數,或者作為其它函數的返回值。
函數賦值給變量:
const greet = function(msg) { console.log(`Hello ${msg}`); } greet('Semlinker'); // Output: 'Hello Semlinker'
函數作為參數:
const logger = function(msg) { console.log(`Hello ${msg}`); }; const greet = function(msg, print) { print(msg); }; greet('Semlinker', logger);
函數作為返回值:
const a = function(a) { return function(b) { return a + b; }; }; const add5 = a(5); add5(10); // Output: 15
函數式編程重要特性
- 只用表達式,不用語句
「表達式」(expression)是一個單純的運算過程,總是有返回值;」語句」(statement)是執行某種操作,沒有返回值。函數式編程要求,只使用表達式,不使用語句。也就是說,每一步都是單純的運算,而且都有返回值。
- 純函數
純函數的特點:
- 給定相同的輸入參數,總是返回相同的結果。
- 沒有依賴外部變量的值。
- 沒有產生任何副作用。
純函數的示例:
const double = (number) => number * 2; double(5);
非純函數示例:
Math.random(); // => 0.3384159509502669 Math.random(); // => 0.9498302571942787 Math.random(); // => 0.9860841663478281
所謂 「副作用」(side effect),是指函數內做了與本身運算無關的事,比如修改某個全局變量的值,或發送 HTTP 請求,甚至函數體內執行 console.log
都算是副作用。函數式編程強調函數不能有副作用,也就是函數要保持純粹,只執行相關運算並返回值,沒有其他額外的行為。
函數式編程的優勢
- 代碼簡潔,開發快速
函數式編程大量使用函數,減少了代碼的重複,因此程序比較短,開發速度較快。
- 接近自然語言,易於理解,可讀性高
函數式編程的自由度很高,可以寫出很接近自然語言的代碼。我們可以通過一系列的函數,封裝數據的處理過程,代碼會變得非常簡潔且可讀性高,具體參考以下示例:
[1,2,3,4,5].map(x => x * 2).filter(x => x > 5).reduce((p,n) => p + n);
- 可維護性高、方便代碼管理
函數式編程不依賴、也不會改變外界的狀態,只要給定輸入參數,返回的結果必定相同。因此,每一個函數都可以被看做獨立單元,很有利於進行單元測試(unit testing)和除錯(debugging),以及模塊化組合。
- 易於 「並發編程」
函數式編程不需要考慮」死鎖」(deadlock),因為它不修改變量,所以根本不存在」鎖」線程的問題。不必擔心一個線程的數據,被另一個線程修改,所以可以很放心地把工作分攤到多個線程,部署」並發編程」(concurrency)。
JavaScript 函數式編程常用方法
- forEach
在 ES 5 版本之前,我們只能通過 for 循環遍曆數組:
var heroes = ['Windstorm', 'Bombasto', 'Magneta', 'Tornado']; for (var i =0, len = heroes.length; i < len; i++) { console.log(heroes[i]); }
在 ES 5 版本之後,我們可以使用 forEach 方法,實現上面的功能:
var heroes = ['Windstorm', 'Bombasto', 'Magneta', 'Tornado']; heroes.forEach(name => console.log(name));
- map
在 ES 5 版本之前,對於上面的示例,如果我們想給每個英雄的名字添加一個前綴,但不改變原來的數組,我們可以這樣實現:
var heroes = ['Windstorm', 'Bombasto', 'Magneta', 'Tornado']; var prefixedHeroes = []; for (var i =0, len = heroes.length; i < len; i++) { prefixedHeroes.push('Super_' + heroes[i]); }
在 ES 5 版本之後,我們可以使用 map 方法,方便地實現上面的功能:
var heroes = ['Windstorm', 'Bombasto', 'Magneta', 'Tornado']; var prefixedHeroes = heroes.map(name => 'Super_' + name);
- filter
在 ES 5 版本之前,對於 heroes 數組,我們想獲取名字中包含 m
字母的英雄,我們可以這樣實現:
var heroes = ['Windstorm', 'Bombasto', 'Magneta', 'Tornado']; var filterHeroes = []; for (var i =0, len = heroes.length; i < len; i++) { if(/m/i.test(heroes[i])) { filterHeroes.push(heroes[i]); } }
在 ES 5 版本之後,我們可以使用 filter 方法,方便地實現上面的功能:
var heroes = ['Windstorm', 'Bombasto', 'Magneta', 'Tornado']; var filterRe = /m/i; var filterHeroes = heroes.filter(name => filterRe.test(name));
響應式編程
什麼是響應式編程
響應式編程就是用異步數據流進行編程,這不是新理念。即使是最典型的點擊事件也是一個異步事件流,從而可以對其進行偵測(observe)並進行相應操作。
可以基於任何東西創建數據流。流非常輕便,並且無處不在,任何東西都可以是一個流:用戶輸入、緩存、數據結構等等。例如,想像一下微博推文也可以是一個數據流,和點擊事件一樣。你可以對其進行偵聽,並作相應反應。
Reactive Extension
Rx(Reactive Extension)的概念最初由微軟公司實現並開源,也就是 Rx.NET,因為 Rx 帶來的編程方式大大改進了異步編程模型,在 .NET 之後,眾多開發者在其他平台和語言上也實現了 Rx 的類庫。比如有 Java 實現的 RxJava,C++ 實現的 RxCpp,用 Python 實現的 RXPy,當然也包括我們後面要學習的 JavaScript 實現的 RxJS。
雖然 Rx 的主要目的是解決異步問題,按並不表示 Rx 不適合同步處理數據。實際上,在使用 Rx 後,我們開發者可以不用關心代碼是被同步執行還是異步執行,所以處理起來會更加簡單。
非響應式與響應式
說了那麼多響應式的概念,我們來看一下非響應式的一個例子:
let a1 = 6; let b1 = 6; let c1 = a1 + b1;
上面的示例很簡單,很明顯 c1 的值為 12。但當我改變 a1 的值,比如改為 3 時,我們會發現 c1 的值並不會更新。同理,單獨改變 b1 的值,c1 的值也不會更新。如果要獲取新的值的話,我們就需要重新計算。
其實,在生活中也有對應的場景。比如商城購物車,當我們改變購物車的商品數量或者刪除某個商品時,我們希望能自動更新訂單金額,而不需要用戶做任何其他操作。
而生活中響應式的另外一個常見例子就是 Excel 表格,以上面的例子為例,A1 單元格的值為 6,B1 單元格的值也為 6,C1 單元格的值為 a1 + b1
。 當我們改變 A1 單元格或 B1 單元格的值時,你會發現 C1 單元格內的值會自動更新,而不需要我們手動執行更新操作,我們可以簡單的理解,這就是響應式。
在前端領域,我們經常要跟異步場景打交道。比如 DOM 事件、AJAX、WebSocket、定時器等。通常情況下,異步的場景會比較複雜。不過值得慶幸地是,我們擁有 RxJS 這個利器。RxJS 擅長處理異步操作,因為它對數據採用 「Push」(相較於 「Pull」 方式),當一個數據產生的時候,會被主動地推給處理函數,這個處理函數不用關心數據是同步或者異步產生的,這樣就讓開發者從異步處理的境遇中解救出來。
參考資源
- 響應式編程入門
- 深入淺出 RxJS