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