初探 BaconJS

93發表於2018-08-13

博文原地址,期待你的 Star 。

一、BaconJS 是什麼

BaconJS 是一個函式響應式程式設計(Functional Reactive Programming)的 JS 庫。雖說是函式響應式程式設計,但我用了會兒,感覺響應式程式設計更令人印象深刻,它與業界比較火的響應式程式設計庫 RxJS 有點類似。值得一提的是,Angular 內建的許多 API 就使用了 RxJS 。可見,響應式程式設計可能會是未來一個很重要的發展方向。

二、如何理解 FRP

函式響應式程式設計(Function Reactive Programmin),簡稱 FRP 。這是 BaconJS 最顯著的特點,要想入門 BaconJS ,只需理解這種程式設計思維即可,不必被官網提供的海量 API 所困擾。

FRP = FP(Function Programming) + RP(Reactive Programming)

顧名思義,函式響應式程式設計 = 函數語言程式設計(Functional Programming) + 響應式程式設計(Reactive Programming)

這兩種程式設計模式是兩個很大的話題,都可以聊很久。我在這裡就大致的羅列下兩者的一些特點,並簡述下我個人的理解。

2.1 函數語言程式設計(FP)

函數語言程式設計是業界很火的一個概念,我曾不止一次試圖去徹底的弄懂它,但至今也是一知半解的狀態~

不管怎麼樣, 以下幾個 FP 的特點還是需要了解的:

  • 純函式(Pure function),函式唯一的輸入只會有唯一的輸出。
  • 不可修改狀態(Immutable state)。

我們常常把 FP 和指令式程式設計(Imperative programming)進行比較,我個人認為 FP 更加強調程式碼的可預測性(Predictable),儘可能的避免程式碼的副作用(Side effect),更加關心資料的對映,而指令式程式設計關心的是解決問題的步驟

2.2 響應式程式設計(RP)

隨著 Google 前端框架 Angular(Angular 1.x 稱為 AngularJS,Angular v2+ 稱為 Angular) 的釋出,由於 Angular 幾乎所有的非同步介面都使用了響應式程式設計庫 RxJS ,響應式程式設計得到了越來越多的關注。

我算是半個 NG 粉,有用 Angular 開發過一個專案,用下來感覺 RxJS 的開發體驗特別好。總的來說,RP 有如下幾個特點:

  • 流(Stream)的概念,衍生的概念有:資料流(Data stream)、事件流(Event stream) 。
  • 鏈式呼叫(Chain invoke),使程式碼看起來清晰、簡潔。
  • 處理非同步資料流的神器。

響應式程式設計主要是用來處理非同步資料流(Asynchronous data stream),可以是 ajax 請求產生的資料流,也可以是 DOM 事件(或者其他時間)產生的事件流。

對於其核心的概念:流(Stream),我的理解是:流是按照時間順序排列的一系列正在進行的事件。在這個概念的基礎上,RP 框架提供了一系列(海量)的介面來處理流,使得我們開發出來的程式碼顯得非常清晰。

想要深入的理解響應式程式設計,推薦閱讀: The instroduction to Reactive Programming you've been missing

三、基本概念

3.1 Observable

觀察物件,是 EventStream 和 Property 物件共同的父類,大部分對流進行操作的介面都封裝在該物件中。

3.2 EventStream

EventStream,又稱事件流,是 BaconJS 中出現頻率最高的一個詞。其本質就是一個流物件,開發者可以通過鏈式的方式進行呼叫,並且可以通過一些 Stream API 來提高開發的效率。

3.3 Property

Property,可以理解為事件流對應的"值",通過 toProperty( ) 介面可以將事件流轉化成 Property 物件。

3.4 舉個例子

為了方便理解 EventStream 和 Property ,我們來看個具體的例子。

需求:某個表單頁面,需要在上面填寫手機號和驗證碼才可以點選註冊按鈕。

剖析下這個需求,我們需要做下面幾件事:

  • 需要顯示手機號和驗證碼的輸入框,另外還有一個註冊按鈕。
  • 驗證輸入手機號和驗證碼格式的有效性。
  • 如果手機號和驗證碼都填寫了,且都是有效的,將註冊按鈕的 disabled 屬性設定為 false 。

核心程式碼如下:

function isPhoneAvailable(n) {
    return /^[1][3,4,5,7,8][0-9]{9}$/.test(n);
}

function isVCAvailable(c) {
    return c.length === 4;
}

// 1. 生成手機號碼輸入框對應的 'keyup' 事件流。
// 2. 通過 map 方法校驗手機號碼的有效性。
// 3. 將事件流物件轉化成 Property 物件,方便後面使用 'and' 方法。
let phoneNumber$ = $('#phone_number')
    .asEventStream('keyup')
    .map((event) => {
        let value = $(event.target).val();
        return isPhoneAvailable(value);
    })
    .toProperty('');

// 同上
let verificationCode$ = $('#verification_code')
    .asEventStream('keyup')
    .map((event) => {
        let value = $(event.target).val();
        return isVCAvailable(value);
    })
    .toProperty('');

// 通過 'Property' 物件的 'and' 方法,生成一個新的 'Property' 物件
// Property.prototype.and 的含義:等同於 return A&B;
let registerButton$ = phoneNumber$.and(verificationCode$);
// 對 registerButton$ 取反,然後通過 'assign' 方法設定註冊按鈕的 disabled 屬性
registerButton$.not().assign($('#register_button'), 'attr', 'disabled');
複製程式碼

完整的程式碼在此。

四、常用的 API

4.1 Bacon.fromEvent( )

用途:通過 DOM 的 EventTarget 或是 NodeJS 的 EventEmitter 來建立事件流。

NodeJS 例子:

const Bacon = require('baconjs').Bacon;
const events = require('events');
const eventEmitter = new events.EventEmitter();

Bacon
    .fromEvent(eventEmitter, 'someEvent')
    .onValue((data) => { console.log(data) });

eventEmitter.emit('someEvent', 'some data');

// 輸出 =>
// 'some data'
複製程式碼

4.2 Obervable.property.map( )

用途:對映到一個新的值。

例子:

// 例子一: 可以給回撥函式傳參
Bacon
    .fromArray([1, 2, 3])
    .map(function (a, b) {
        console.log(`${a}, ${b}`)
        return a*b;
    }, 3)
    .log(); 
// 輸出 =>
// 3, 1
// 3
// 3, 2
// 6
// 3, 3
// 9
// <end>

// 例子二: 可以用 '.foo' 獲取物件的某個屬性值
Bacon
    .fromPoll(1000)
    .map(() => { return { foo: `foo${parseInt(Math.random()*100)}` } })
    .take(4)
    .map('.foo')
    .log();
// 輸出(foo後面的數字是隨機的) =>
// foo25
// foo35
// foo98
// foo76
// <end>
複製程式碼

4.3 EventStream.merge( )

用途:將一個 EventStream merge 到另一個 EventStream 中。

例子:

let eventStream1 = Bacon.fromArray([1, 2, 5]);
let eventSream2 = Bacon.fromArray([3, 4]);
eventStream1.merge(eventSream2).log(); 

// 輸出 =>
// 1
// 2
// 3
// 4
// 5
// <end>
複製程式碼

4.4 TIP

剛開始使用 RxJS 的時候容易被海量的 API 給嚇到了,而且並不是所有的介面都有 Demo 有些介面會顯然比較難理解。BaconJS 也是一樣,有許多的介面,光看官方的文件有時會比較難理解。

這裡,我們分享一個我快速理解某個介面的 TIP 。那就是:查閱具體介面單元測試(UT)中的所有 Case 。個人感覺,這比看官方文件高效很多。