RxJS 函式式與響應式程式設計
簡單說,”函數語言程式設計”是一種“程式設計正規化” (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、Socket/">WebSocket、定時器等。通常情況下,非同步的場景會比較複雜。不過值得慶幸地是,我們擁有 RxJS 這個利器。RxJS 擅長處理非同步操作,因為它對資料採用 “Push”(相較於 “Pull” 方式),當一個數據產生的時候,會被主動地推給處理函式,這個處理函式不用關心資料是同步或者非同步產生的,這樣就讓開發者從非同步處理的境遇中解救出來。