RxJS不完全指北(入門篇)

什麼是RxJS?

RxJS是一個JavaScript庫,用來編寫異步基於事件的程序。RxJS結合了觀察者模式迭代器模式使用集合的函數式編程,以滿足以一種理想方式來管理事件序列所需要的一切。

可以把RxJS當作用來處理事件的Lodash

爲什麼要學Rxjs?

在現在的Web開發中,異步(Async)操作隨處可見,比如使用ajax提交一個表單數據,我們需要等待服務端返回提交結果後執行後續操作,這就是一個典型的異步操作。雖然JavaScript爲了方便開發者進行異步操作,提出了很多解決方案(callback,Promise,Async/await等等),但是隨着需求愈加複雜,如何優雅的管理異步操作仍然是個難題。

此外,異步操作API千奇百怪,五花八門:

  1. DOM Events
  2. XMLHttpRequest
  3. fetch
  4. WebSockets
  5. Service Worker
  6. Timer
  7. ......

以上這些常用的API全部都是異步的,但是每個使用起來卻完全不同,無形中給開發者增加了很大的學習和記憶成本。

使用RxJS可以很好的幫助我們解決上面兩個問題,控制大量異步代碼的複雜度,保持代碼可讀性,並統一API。

舉個栗子:頁面上有一個搜索框,用戶可以輸入文本進行搜索,搜索時要向服務端發送異步請求,爲了減小服務端壓力,前端需要控制請求頻率,1秒最多發送5次請求,並且輸入爲空時不發送請求,最後將搜索的結果顯示在頁面上。

通常我們的做法是這樣的,先判斷輸入是否爲空,如果不爲空,則構造一個截流函數來控制請求頻率,這其中涉及到創建和銷燬定時器,此外,由於每個請求返回時間不確定,如何獲取最後一次搜索結果,需要構造一個棧來保存請求順序, 想完美實現需求並不簡單。

RxJS是如何解決這個問題的呢?請看下面的代碼:

// 1.獲取dom元素
const typingInput = document.querySelector("#typing-input"); // 輸入
const typingBack = document.querySelector("#typing-back"); // 輸出

// 2.模擬異步請求
const getData = value =>
  new Promise(resolve =>
    setTimeout(() => resolve(`async data ${value}`), 1000)
  );

// 3.RxJS操作
const source$ = fromEvent(typingInput, "input") // 創建事件數據流
.pipe( // 管道式操作
  map(e => e.target.value), // 獲取輸入的數據
  filter(i => i), // 過濾空數據
  debounceTime(200), // 控制頻率
  switchMap(getData) // 轉化數據爲請求
);
// 4.輸入結果
source$.subscribe(asyncData => (typingBack.innerHTML = asyncData));

這就是全部代碼,也許有些地方看不太懂 ,沒關係,先不要着急,我們分步解讀一下。

  1. 使用選擇器獲取了兩個dom元素,第一個是輸入框,第二個是搜索結果的容器;
  2. 使用Promise來模擬一個異步請求的函數,1秒後返回請求結果;
  3. 這部分是RxJS操作,這裏我們要先介紹一個概念,“數據流”(stream,簡稱“流”),“流”是RxJS中一種特殊的對象,我們可以想象數據流就像一條河流,而數據就是河裏的水,順流而下。代表“流”的變量一般用“$”結尾,這是RxJS編程的一種約定,被成爲“芬蘭式命名法”。
    代碼中的source$就是輸入框的輸入事件產生的數據流,我們可以使用pipe方法,像搭建“管道”一樣對流中的數據進行加工,先使用map函數將事件對象轉化成輸入值,然後使用fllter方法過濾掉無效的輸入,接着使用debounceTime控制數據向下流轉的頻率,最後使用switchMap把輸入值轉化成異步請求,整個數據流就構建完成了。
  4. 最後我們使用數據流的subscribe方法添加對數據的操作,也就是將請求的結果輸出到頁面上。

    注意,這段代碼我們使用的全部變量都是用const聲明的,全部是不可變的,也即是變量聲明時是什麼值,就永遠是什麼值,就像定義函數一樣。相對於傳統的指令式編程,RxJS的代碼就是由一個一個不可變的函數組成,每個函數只是對輸入參數作出相應,然後返回結果,這樣的代碼寫起來更加清爽,也更好維護

RxJS結合了函數式響應式這兩種編程思想,爲了更深入的瞭解RxJS,先來介紹一下什麼是函數式編程和響應式編程。

函數式編程

函數式編程(Functional Porgramming)是一種編程範式,就像“面向對象編程”一樣,是一種編寫代碼的“方法論”,告訴我們應該如何思考和解決問題。不同於面向對象編程,函數式編程強調使用函數來解決問題。

這裏有兩個問題:

  1. 任何語言都支持函數式編程麼?並不是,能夠支持函數式編程的語言至少要滿足“函數是一等公民(First Class)”這個要求,意思是函數可以被賦值給一個變量,並且可以作爲參數傳遞給另一個函數,也可以作爲另一個函數的返回值。顯然JavaScript滿足這個條件。
  2. 函數式編程裏的函數有什麼特別之處?函數式編程裏要求函數滿足以下幾個要求:聲明式、純函數、數據不可變

聲明式(Declarative)

與之對應的是命令式編程,也是最常見的編程模式。

舉個例子,我們希望寫個函數,把數組中的每個元素乘以2,使用命令式編程,大概是這個樣子的:

function double(arr) {
    const result = []
    for(let i=0,l=arr.length;i<l;i++) {
        result.push(arr[i] * 2)
    }
    return result
}

我們將整個邏輯過程完整描述了一遍,完美。

但如果又來了一個新需求,實現一個新函數,把數組中每個元素加1,簡單,再來一遍:

function addOne(arr) {
    const result = []
    for(let i=0,l=arr.length;i<l;i++) {
        result.push(arr[i] + 1)
    }
    return result
}

是不是感覺哪裏不對?double和addOne百分之九十的代碼完全一樣,“重複的代碼是萬惡之源。”我們應該想辦法改進一下。

這裏就體現了命令式編程的一個問題,程序按照邏輯過程來執行,但是很多問題都有相似的模式,比如上面的double和addOne。很自然我們想把這個模式抽象一下,減少重複代碼。

接下來我們使用JavaScript的map函數來重寫double和addOne:

function double(arr) {
    return arr.map(function(item) { return item * 2 })
}

function addOne(arr) {
    return arr.map(function(item) { return item + 1 })
}

重複代碼全部被封裝到map函數中。而我們需要做的只是告訴map函數應該如何映射數據,這就是聲明式編程。相比較之前的代碼,這樣的代碼更容易維護。

如果使用箭頭函數,代碼還可以進一步簡化:

const double = arr => arr.map(item => item * 2)

const addOne = arr => arr.map(item => item + 1)

注意以上兩個函數的返回結果都是一個新的數組,而並沒有對原數組進行修改,這符合函數式編程的另外一個要求:純函數

純函數(Pure Function)

純函數是指滿足以下兩個條件的函數:

相同的參數輸入,返回相同的輸出結果;
函數內不會修改任何外部狀態,比如全局變量或者傳入的參數對象;
舉個栗子:

const arr = [1, 2, 3, 4, 5]

arr.slice(0, 3) // [1, 2, 3]

arr.slice(0, 3) // [1, 2, 3]

arr.slice(0, 3) // [1, 2, 3]

JavaScript中數組的slice方法不管執行幾次,返回值都相同,並且沒有改變任何外部狀態,所以slice就是一個純函數。

const arr = [1, 2, 3, 4, 5]

arr.splice(0, 3) // [1, 2, 3]

arr.splice(0, 3) // [4, 5]

arr.slice(0, 3) // []

相反,splice方法每次調用的結果就不同,因爲splice方法改變了全局變量arr的值,所以splice就不是純函數。

不純的函數往往會產生一些副作用(Side Effect),比如以下這些:

改變全局變量;
改變輸入參數引用對象;
讀取用戶輸入,比如調用了alert或者confirm函數;
拋出一個異常;
網絡I/O,比如發送了一個AJAX請求;
操作DOM;
使用純函數可以大大增強代碼的可維護性,因爲固定輸入總是返回固定輸出,所以更容易寫單元測試,也就更不容易產生bug。

數據不可變(Immutability)

數據不可變是函數式編程中十分重要的一個概念,意思是如果我們想改變一個變量的值,不是直接對這個變量進行修改,而是通過調用函數,產生一個新的變量。

如果你是一個前端工程師,肯定已經對數據不可變的好處深有體會。在JavaScript中,字符串(String),數字(Number)這兩種類型就是不可變的,使用他們的時候往往不容易出錯,而數組(Array)類型就是可變的, 使用數組的pop、push等方法都會改變原數組對象,從而引發各種bug。

注意,雖然ES6已經提出了使用const聲明一個常量(不可變數據),但是這隻能保證聲明的對象的引用不可改變,而這個對象自身仍然可以變化。比如用const聲明一個數組,使用push方法仍然可以像數組中添加元素。

和麪向對象編程相比,面向對象編程更傾向把狀態的改變封裝到對象內部,以此讓代碼更清晰。而函數式編程傾向數據和函數分離,函數可以處理數據,但不改變原數據,而是通過產生新數據的方式作爲運算結果,以此來儘量減少變化的部分,讓我們的代碼更清晰。

響應式編程

和函數式編程類似,響應式編程(Reactive Programming)也是一種編程的範式。從設計模式的角度來說,響應式編程就是“觀察者模式”的一種有效實踐。簡單來說,響應式編程指當數據發生變化時,主動通知數據的使用者這個變化

很多同學都使用過vue框架開發,vue中很出名的數據雙向綁定就是基於響應式編程的設計思想實現的。當我們在通過v-bind綁定一個數據到組件上以後,不管這個數據何時發生變化,都會主動通知綁定過的組件,使我們開發時可以專注處理數據本身,而不用關心如何同步數據。

而在相應時編程裏最出名的框架就是微軟開發的Reactive Extension。這套框架旨在幫助開發者解決複雜的異步處理問題。我們的主角RxJS就是這個框架的JS版本。

怎麼使用RxJS

安裝

npm install rxjs

導入

import Rx from "rxjs";

請注意,這樣導入會將整個RxJS庫全部導入進來,而實際項目未必會用上Rxjs的全部功能,全部導入會讓項目打包後變得非常大,我們推薦使用深鏈(deep link)的方式導入Rxjs,只導入用的上的功能,比如我們要使用Observable類,就只導入它:

import { Observable } from "rxjs/Observable";

實際項目中,按需導入是一個好辦法,但是如果每個文件都寫一堆import語句,那就太麻煩了。所以,更好的實踐是用一個文件專門導入RxJS相關功能,其他文件再導入這個文件,把RxJS導入工作集中管理

篇幅有限,下一講將會講解RxJS中幾個核心概念,歡迎各位留言拍磚~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章