談談響應式編程 什麼是響應式編程 函數響應式編程 應用範圍 觀察者模式和迭代器模式 RX(Reactive Extension) 動手寫一寫 總結 Read More

隨着前端框架react,angular以及vue的流行,響應式編程也開始在前端領域得以廣泛應用。因此,瞭解並且理解響應式編程有助於更好地學習這些框架,同時利用好響應式編程的相關工具,可以讓編程更加輕鬆。

什麼是響應式編程

和平常經常聽說的面向對象編程和函數式編程一樣,響應式編程(Reactive Programming)就是一個編程範式,但是與其他編程範式不同的是它是基於數據流和變化傳播的。我們經常在程序中這樣寫

A = B + C

A被賦值爲BC的值。這時,如果我們改變B的值,A的值並不會隨之改變。而如果我們運用一種機制,當B或者C的值發現變化的時候,A的值也隨之改變,這樣就實現了”響應式“。

而響應式編程的提出,其目的就是簡化類似的操作,因此它在用戶界面編程領域以及基於實時系統的動畫方面都有廣泛的應用。另一方面,在處理嵌套回調的異步事件,複雜的列表過濾和變換的時候也都有良好的表現。

函數響應式編程

而主要利用函數式編程(Functional Programming)的思想和方法(函數、高階函數)來支持Reactive Programming就是所謂的Functional Reactive Programming,簡稱FRP。

FPR 將輸入分爲兩個基礎的部分:行爲(behavior)和事件(events) 。這兩個基本元素在函數響應式編程中都是第一類(first-class)值。 其中行爲是隨時間連續變化的數據,而事件則是基於離散的時間序列 。例如:在我們操作網頁的時候,會觸發很多的事件,包括點擊,拖動,按鍵事件等。這些事件都是不連續的。對事件求值是沒有意義的,所有我們一般要通過fromEventbuffer等將其變成連續的行爲來做進一步處理。與RP相比,FRP更偏重於底層。由於採用了函數式編程範式,FRP也自然而然帶有其特點。這其中包括了不可變性,沒有副作用以及通過組合函數來構建程序等特點。

應用範圍

  1. 多線程,時間處理,阻塞等場景
  2. ajax,websocket和數據加載
  3. 失敗處理
  4. DOM事件和動畫

觀察者模式和迭代器模式

在這裏簡單介紹一下觀察者模式和迭代器模式,便於對後續介紹的概念有所瞭解。

觀察者模式

觀察者模式(Observer Pattern):定義了對象間的一種一對多的依賴關係,當一個對象狀態發生改變時,其相關依賴對象皆得到通知並被自動更新。觀察者模式又叫做發佈-訂閱(Publish/Subscribe)模式、模型-視圖(Model/View)模式、源-監聽器(Source/Listener)模式或從屬者(Dependents)模式。

觀察者模式在事件處理中應用特別廣泛,也是MVC架構模式的核心。我們來寫一個簡單的應用,試一試:

// 監聽者
class Observer {
  constructor(index) {
    this.index = index;
  }
  
  say() {
    console.log(`我是第${this.index}個用戶😁`);
  }
  
  update() {
    console.log('observer : 我開始說話了');
    this.say();
  }
}
// 被監聽者
class Observable {
  constructor() {
    this.observers = [];
  }
  
  addObserver(observer) {
    this.observers.push(observer);
  }
  
  removeObserverByIndex(index) {
    this.observers.splice(index,1);
  }
  
  notify() {
    console.log('Observable : 開始通知所有監聽者');
    this.observers.forEach(x => x.update());
  }
}
//客戶端代碼,註冊監聽
const observer1 = new Observer(1);
const observer2 = new Observer(2);
const observable = new Observable();
observable.addObserver(observer1);
observable.addObserver(observer2);
//通知所有監聽者
observable.notify();

執行結果如下:

"Observable : 開始通知所有監聽者"
"observer : 我開始說話了"
"我是第1個用戶😁"
"observer : 我開始說話了"
"我是第2個用戶😁"

在觀察者模式中,可以分爲兩種模式,push和pull:

1.push“推模式“,就是被監聽者將消息推送出去,進而觸發監聽者的相應事件。如上面的事例代碼就是採用這種方式,響應式編程一般採用這種模式。

2.pull"拉模式”,就是監聽者主動從被監聽者處獲取數據。

迭代器模式

提供一種方法順序訪問一個聚合對象中的各個元素,而又不暴露內部的表示。

迭代器模式常用的場合是在遍歷集合,增刪改查的時候。如今,很多的高級語言都已經將其作爲自身的語言特性,如python,java,es6等都有其實現。我們可以使用es5的語法簡單實現一下:

function getIterator(array){
    var nextIndex = 0;
    
    return {
       next: function(){
           return nextIndex < array.length ?
               {value: array[nextIndex++], done: false} :
               {done: true};
       }
    }
}

var iterator = getIterator([1,2,3]);
iterator.next().value; // 1
iterator.next().value; // 2

以上的代碼利用了javscript語法的閉包特性,返回了一個帶狀態的對象,通過調用next方法來獲取集合中的下一個值。這和函數式編程中部分求值的特點是一樣的。這個模式很簡單,我們利用它就可以不再需要手動遍歷獲取集合中的數據了。

RX(Reactive Extension)

�Reactive Extension 這個概念最早是出現在微軟的.NET社區中的,而目前也越來越多的語言實現了自己的RX,如java,javascript,ruby等。

微軟官方的解釋是這樣的:

Reactive Extensions (Rx) is a library for composing asynchronous and event-based programs using observable sequences and LINQ-style query operators.

簡單地來說就是利用ObservableLINQ風格的基於事件驅動的編程擴展庫。它是響應式編程的一種實現,用於解決異步事件流的一種解決方案。通俗點解釋,就是利用它可以很好地控制事件流的異步操作,將事件的發生和對事件的響應進行解耦。可以讓開發者不再關心複雜的線程處理,鎖等併發相關問題。

RxJs

RxJS是用javascript實現的一個RX類庫, 官方說明裏指出RxJS = Observables + Operators + Schedulers。其中Observables用於生產消息,而Subscriber則用於消費消息,這和生產者和消費者的概念有點類似。

Obversables其實是一組事件流,比如你在鍵盤上輸入"hello"這五個字母,你就有了"h - e - l - l -o"十個keydown + keyup事件組成的一組序列,這就稱爲Obversables,但是它是不可更改的。如果你只想要過濾出keydown事件怎麼做呢?這時候你就需要利用Operators,在響應式編程概念裏可以利用其組合出新的行爲和事件。在這裏,你可以用它用來操作這個不可變的Obversable從而生成你想要的結果,同時,可以採用多個鏈式調用來進行更加複雜的操作,如:

obversables.filter((event) => {return event === 'keydown' });

Observable

Observable 實際上是應用了觀察者模式和迭代器模式的事件或者說消息序列。在RxJs中提供了多個API來將生成Observable對象,如基本的create,of, fromEvent等。

Observable可以被訂閱(subscribe),隨後會將數據push給所有訂閱者(subscribers)。

你可能在處理異步操作的時候,會應用到Promise這個技術。那麼ObservablePromise相比,又有什麼區別呢?

首先,Observable是不可變的,這也是函數式編程的思想。你每次需要獲取新的序列的時候,都需要利用函數操作對其做變換,這也避免了無意中修改數據造成的Bug。其次,我們知道Promise對象一旦生成並觸發後,是不可以取消的,而Observable是可以,這也提供了一些靈活性。同時,當你需要共享變量的時候,Observable是可以組合使用的。最後,還有一個特性是Promise每次只能返回一個值,而Observable可以返回多值。

Observer

Observer: is a collection of callbacks that knows how to listen to values delivered by the Observable.

Observer就是一組回調函數的集合,包括next, error, complete三個,它的值是Observable傳進來的,然後在監聽的時候來觸發這些函數。

var foo = Rx.Observable.create(function (observer) {
  console.log('Hello');
  observer.next(42);
  observer.next(100);
  observer.next(200);
  setTimeout(() => {
    observer.next(300); // happens asynchronously
  }, 1000);
});

console.log('before');
foo.subscribe(function (x) {
  console.log(x);
});
console.log('after');

輸出:
"before"
"Hello"
42
100
200
"after"
300

Operator

由於Observables是不可變的,因此要根據原生的數據結構生成新的數據結構,必須要藉助強大的函數組合來達到效果。Operator就是這樣的一個工具箱。它不僅僅提供了我們常見的map,filter,reduce操作,也提供瞭如連接,條件判斷,轉換,聚合,操作時間等方法。

在Javascript原生的數組操作中,也經常可以看到map,filter,reduce等函數的身影,如:

var source = ['1', '2', 'hello', 'world'];
var result = source.map(x => parseInt(x)).filter(x => !isNaN(x));

而RxJs提供的Operator和對數組操作的又有什麼區別呢?Operator工作和數組相比較而言,數組每次操作會直接處理整個數組,但是Operator是一個迭代器,它會在處理完一個值後才轉去處理下一個值。

安裝和使用

現在官方提供的RxJs有兩個倉庫,RxJs5以及RxJS,你可以自己選擇

安裝rxjs用

npm install rx

而安裝rxjs5就用

npm install rxjs

然後導入項目中應用就可以了。

如果你嫌麻煩的話,我在github上創建了新的初始項目,可以直接上手應用RxJS,倉庫地址https://github.com/scq000/rxjs-quick-starter用來練手寫個小Demo還是很方便的。

動手寫一寫

下面我們來寫一個小的Demo。任務是通過查詢GitHub的API, 獲取用戶列表,然後當點擊特定用戶名的時候,獲取這個用戶的詳細信息。基於Github官方的提供的兩個API:

  1. https://api.github.com/users
  2. https://api.github.com/users/username

其實,利用原生的Javascript我們也能很好地實現這樣的需求。不過我們通常會依賴前一次的回調狀態,因此它不適用於模塊化或者修改要傳遞給下一個回調的數據)。當我們要回調的層數比較多的時候,我們就陷入了“回調地獄”中去了。現在就讓我們看看RxJs怎麼實現這樣一個需求吧。

  1. 先創建HTML頁面結構:

      <button id="getAllBtn">Get All Users</button>
    
      <form onsubmit="return false;">
        <input id="search-input" type="text" placeholder="search">
      </form>
    
      <div>
        <ul id="user-lists">
        </ul>
      </div>
    
      <div id="user-info">
      </div>
    

  2. 寫JS代碼:

    //導入依賴
    const $ = require('jquery');
    const Rx = require('rxjs/Rx');
    
    //獲取頁面元素
    const getAllBtn = $('#getAllBtn');
    const searchInput = $('#search-input');
    let keyword = '';
    
    //定義事件流
    const clickEventStream = Rx.Observable.fromEvent(getAllBtn, 'click');
    const inputEventStream = Rx.Observable.fromEvent(searchInput, 'keyup').filter(event => event.keyCode !== 13);
    const clickUserItemStream = Rx.Observable.fromEvent($('#user-lists'), 'click');
    
    //將用戶觸發的事件流轉換成API請求流
    const getUserListStream = clickEventStream.flatMap(() => {
      return Rx.Observable.fromPromise($.getJSON('https://api.github.com/users'));
    });
    
    const filterUserStream = inputEventStream.flatMap(event => {
      return Rx.Observable.fromPromise($.getJSON('https://api.github.com/users'));
    });
    
    const getUserInformation = clickUserItemStream.flatMap(event => {
      console.log(event.target.innerText);
      return Rx.Observable.fromPromise($.getJSON('https://api.github.com/users/' + event.target.innerText));
    });
    
    //當響應到達時觸發
    getUserInformation.subscribe(user => {
      console.log(user);
      renderUserInfo(user);
    });
    
    filterUserStream.subscribe(users => {
      console.log(users);
      renderUserLists(users.filter(user => user.login.includes(keyword)));
    });
    
    clickEventStream.subscribe(
      value => console.log('GetUsers btn click!')
    );
    
    inputEventStream.subscribe(event => {
      console.log(searchInput.val());
      keyword = searchInput.val();
    });
    
    clickUserItemStream.subscribe(event => {
      console.log(event.target);
    });
    
    getUserListStream.catch(err => {
      Rx.Observable.of(err); //使用catch函數避免錯誤被中斷
    }).subscribe(users => {
      console.log(users);
      renderUserLists(users)
    });
    
    //將數據渲染到DOM元素上
    function renderUserLists(users) {
      $('#user-lists').html('');
      users.forEach((user) => {
        $('#user-lists').append(`<li>${user.login}</li>`);
      });
    }
    
    function renderUserInfo(user) {
      $('#user-info').html('');
      for (var key in user) {
        $('#user-info').append(`<div>${key} ---> ${user[key]}</div>`);
      }
    }
    

代碼已經放在Github,如果你感興趣的話,可以clone下來跑跑看。

總結

響應式編程的思想比較不好理解,我在學習過程中,也查閱了很多的資料,只能算是剛剛入門。所以,這篇文章也算是對整個學習過程的一次總結吧。

Read More

React Programming
An introduction to reactive programming
Slidershare: introduction to functional reactive programming
Functional Reactive Programming from First Principles
A survey of functional reactive programming
響應式編程一覽
C#設計模式系列(16)-迭代器模式
學習教程
RxJs GitBook
RxJs官方文檔
Introduction to reactive programming 視頻

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