談?wù)勴憫?yīng)式編程

隨著前端框架react,angular以及vue的流行嫌佑,響應(yīng)式編程也開始在前端領(lǐng)域得以廣泛應(yīng)用。因此宦焦,了解并且理解響應(yīng)式編程有助于更好地學(xué)習(xí)這些框架须妻,同時(shí)利用好響應(yīng)式編程的相關(guān)工具,可以讓編程更加輕松煞额。

什么是響應(yīng)式編程

和平常經(jīng)常聽說的面向?qū)ο缶幊毯秃瘮?shù)式編程一樣思恐,響應(yīng)式編程(Reactive Programming)就是一個(gè)編程范式,但是與其他編程范式不同的是它是基于數(shù)據(jù)流和變化傳播的膊毁。我們經(jīng)常在程序中這樣寫

A = B + C

A被賦值為BC的值胀莹。這時(shí),如果我們改變B的值婚温,A的值并不會(huì)隨之改變描焰。而如果我們運(yùn)用一種機(jī)制,當(dāng)B或者C的值發(fā)現(xiàn)變化的時(shí)候栅螟,A的值也隨之改變荆秦,這樣就實(shí)現(xiàn)了”響應(yīng)式“篱竭。

而響應(yīng)式編程的提出,其目的就是簡(jiǎn)化類似的操作步绸,因此它在用戶界面編程領(lǐng)域以及基于實(shí)時(shí)系統(tǒng)的動(dòng)畫方面都有廣泛的應(yīng)用掺逼。另一方面,在處理嵌套回調(diào)的異步事件瓤介,復(fù)雜的列表過濾和變換的時(shí)候也都有良好的表現(xiàn)坪圾。

函數(shù)響應(yīng)式編程

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

FPR 將輸入分為兩個(gè)基礎(chǔ)的部分:行為(behavior)和事件(events) 。這兩個(gè)基本元素在函數(shù)響應(yīng)式編程中都是第一類(first-class)值漾月。 其中行為是隨時(shí)間連續(xù)變化的數(shù)據(jù)病梢,而事件則是基于離散的時(shí)間序列 。例如:在我們操作網(wǎng)頁的時(shí)候梁肿,會(huì)觸發(fā)很多的事件蜓陌,包括點(diǎn)擊,拖動(dòng)吩蔑,按鍵事件等钮热。這些事件都是不連續(xù)的。對(duì)事件求值是沒有意義的烛芬,所有我們一般要通過fromEvent隧期,buffer等將其變成連續(xù)的行為來做進(jìn)一步處理。與RP相比赘娄,F(xiàn)RP更偏重于底層仆潮。由于采用了函數(shù)式編程范式,F(xiàn)RP也自然而然帶有其特點(diǎn)遣臼。這其中包括了不可變性性置,沒有副作用以及通過組合函數(shù)來構(gòu)建程序等特點(diǎn)。

應(yīng)用范圍

  1. 多線程揍堰,時(shí)間處理鹏浅,阻塞等場(chǎng)景
  2. ajax,websocket和數(shù)據(jù)加載
  3. 失敗處理
  4. DOM事件和動(dòng)畫

觀察者模式和迭代器模式

在這里簡(jiǎn)單介紹一下觀察者模式和迭代器模式屏歹,便于對(duì)后續(xù)介紹的概念有所了解隐砸。

觀察者模式

觀察者模式(Observer Pattern):定義了對(duì)象間的一種一對(duì)多的依賴關(guān)系,當(dāng)一個(gè)對(duì)象狀態(tài)發(fā)生改變時(shí)西采,其相關(guān)依賴對(duì)象皆得到通知并被自動(dòng)更新凰萨。觀察者模式又叫做發(fā)布-訂閱(Publish/Subscribe)模式、模型-視圖(Model/View)模式、源-監(jiān)聽器(Source/Listener)模式或從屬者(Dependents)模式胖眷。

觀察者模式在事件處理中應(yīng)用特別廣泛武通,也是MVC架構(gòu)模式的核心。我們來寫一個(gè)簡(jiǎn)單的應(yīng)用珊搀,試一試:

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

執(zhí)行結(jié)果如下:

"Observable : 開始通知所有監(jiān)聽者"
"observer : 我開始說話了"
"我是第1個(gè)用戶??"
"observer : 我開始說話了"
"我是第2個(gè)用戶??"

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

1.push“推模式“囚枪,就是被監(jiān)聽者將消息推送出去,進(jìn)而觸發(fā)監(jiān)聽者的相應(yīng)事件劳淆。如上面的事例代碼就是采用這種方式链沼,響應(yīng)式編程一般采用這種模式。

2.pull"拉模式”沛鸵,就是監(jiān)聽者主動(dòng)從被監(jiān)聽者處獲取數(shù)據(jù)括勺。

迭代器模式

提供一種方法順序訪問一個(gè)聚合對(duì)象中的各個(gè)元素,而又不暴露內(nèi)部的表示曲掰。

迭代器模式常用的場(chǎng)合是在遍歷集合疾捍,增刪改查的時(shí)候。如今栏妖,很多的高級(jí)語言都已經(jīng)將其作為自身的語言特性乱豆,如python,java,es6等都有其實(shí)現(xiàn)吊趾。我們可以使用es5的語法簡(jiǎn)單實(shí)現(xiàn)一下:

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語法的閉包特性宛裕,返回了一個(gè)帶狀態(tài)的對(duì)象,通過調(diào)用next方法來獲取集合中的下一個(gè)值趾徽。這和函數(shù)式編程中部分求值的特點(diǎn)是一樣的续滋。這個(gè)模式很簡(jiǎn)單,我們利用它就可以不再需要手動(dòng)遍歷獲取集合中的數(shù)據(jù)了孵奶。

RX(Reactive Extension)

?Reactive Extension 這個(gè)概念最早是出現(xiàn)在微軟的.NET社區(qū)中的,而目前也越來越多的語言實(shí)現(xiàn)了自己的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.

簡(jiǎn)單地來說就是利用ObservableLINQ風(fēng)格的基于事件驅(qū)動(dòng)的編程擴(kuò)展庫。它是響應(yīng)式編程的一種實(shí)現(xiàn)湿颅,用于解決異步事件流的一種解決方案载绿。通俗點(diǎn)解釋,就是利用它可以很好地控制事件流的異步操作油航,將事件的發(fā)生和對(duì)事件的響應(yīng)進(jìn)行解耦崭庸。可以讓開發(fā)者不再關(guān)心復(fù)雜的線程處理,鎖等并發(fā)相關(guān)問題怕享。

RxJs

RxJS是用javascript實(shí)現(xiàn)的一個(gè)RX類庫执赡, 官方說明里指出RxJS = Observables + Operators + Schedulers。其中Observables用于生產(chǎn)消息函筋,而Subscriber則用于消費(fèi)消息沙合,這和生產(chǎn)者和消費(fèi)者的概念有點(diǎn)類似。

Obversables其實(shí)是一組事件流跌帐,比如你在鍵盤上輸入"hello"這五個(gè)字母首懈,你就有了"h - e - l - l -o"十個(gè)keydown + keyup事件組成的一組序列,這就稱為Obversables谨敛,但是它是不可更改的究履。如果你只想要過濾出keydown事件怎么做呢?這時(shí)候你就需要利用Operators脸狸,在響應(yīng)式編程概念里可以利用其組合出新的行為和事件挎袜。在這里,你可以用它用來操作這個(gè)不可變的Obversable從而生成你想要的結(jié)果肥惭,同時(shí)盯仪,可以采用多個(gè)鏈?zhǔn)秸{(diào)用來進(jìn)行更加復(fù)雜的操作,如:

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

Observable

Observable 實(shí)際上是應(yīng)用了觀察者模式和迭代器模式的事件或者說消息序列蜜葱。在RxJs中提供了多個(gè)API來將生成Observable對(duì)象全景,如基本的create,of, fromEvent等。

Observable可以被訂閱(subscribe)牵囤,隨后會(huì)將數(shù)據(jù)push給所有訂閱者(subscribers)爸黄。

你可能在處理異步操作的時(shí)候,會(huì)應(yīng)用到Promise這個(gè)技術(shù)揭鳞。那么ObservablePromise相比炕贵,又有什么區(qū)別呢?

首先野崇,Observable是不可變的称开,這也是函數(shù)式編程的思想。你每次需要獲取新的序列的時(shí)候乓梨,都需要利用函數(shù)操作對(duì)其做變換鳖轰,這也避免了無意中修改數(shù)據(jù)造成的Bug。其次扶镀,我們知道Promise對(duì)象一旦生成并觸發(fā)后蕴侣,是不可以取消的,而Observable是可以臭觉,這也提供了一些靈活性昆雀。同時(shí)辱志,當(dāng)你需要共享變量的時(shí)候,Observable是可以組合使用的狞膘。最后揩懒,還有一個(gè)特性是Promise每次只能返回一個(gè)值,而Observable可以返回多值客冈。

Observer

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

Observer就是一組回調(diào)函數(shù)的集合旭从,包括next, error, complete三個(gè),它的值是Observable傳進(jìn)來的场仲,然后在監(jiān)聽的時(shí)候來觸發(fā)這些函數(shù)和悦。

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是不可變的,因此要根據(jù)原生的數(shù)據(jù)結(jié)構(gòu)生成新的數(shù)據(jù)結(jié)構(gòu)渠缕,必須要借助強(qiáng)大的函數(shù)組合來達(dá)到效果鸽素。Operator就是這樣的一個(gè)工具箱。它不僅僅提供了我們常見的map,filter,reduce操作亦鳞,也提供了如連接馍忽,條件判斷,轉(zhuǎn)換燕差,聚合遭笋,操作時(shí)間等方法。

在Javascript原生的數(shù)組操作中徒探,也經(jīng)惩吆簦可以看到map,filter,reduce等函數(shù)的身影,如:

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

而RxJs提供的Operator和對(duì)數(shù)組操作的又有什么區(qū)別呢测暗?Operator工作和數(shù)組相比較而言央串,數(shù)組每次操作會(huì)直接處理整個(gè)數(shù)組,但是Operator是一個(gè)迭代器碗啄,它會(huì)在處理完一個(gè)值后才轉(zhuǎn)去處理下一個(gè)值质和。

安裝和使用

現(xiàn)在官方提供的RxJs有兩個(gè)倉庫,RxJs5以及RxJS,你可以自己選擇

安裝rxjs用

npm install rx

而安裝rxjs5就用

npm install rxjs

然后導(dǎo)入項(xiàng)目中應(yīng)用就可以了稚字。

如果你嫌麻煩的話饲宿,我在github上創(chuàng)建了新的初始項(xiàng)目,可以直接上手應(yīng)用RxJS,倉庫地址https://github.com/scq000/rxjs-quick-starter尉共。用來練手寫個(gè)小Demo還是很方便的褒傅。

動(dòng)手寫一寫

下面我們來寫一個(gè)小的Demo。任務(wù)是通過查詢GitHub的API袄友, 獲取用戶列表,然后當(dāng)點(diǎn)擊特定用戶名的時(shí)候,獲取這個(gè)用戶的詳細(xì)信息霹菊【珧迹基于Github官方的提供的兩個(gè)API:

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

其實(shí)支竹,利用原生的Javascript我們也能很好地實(shí)現(xiàn)這樣的需求。不過我們通常會(huì)依賴前一次的回調(diào)狀態(tài)鸠按,因此它不適用于模塊化或者修改要傳遞給下一個(gè)回調(diào)的數(shù)據(jù))礼搁。當(dāng)我們要回調(diào)的層數(shù)比較多的時(shí)候,我們就陷入了“回調(diào)地獄”中去了∧考猓現(xiàn)在就讓我們看看RxJs怎么實(shí)現(xiàn)這樣一個(gè)需求吧馒吴。

  1. 先創(chuàng)建HTML頁面結(jié)構(gòu):

      <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代碼:

    //導(dǎo)入依賴
    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');
    
    //將用戶觸發(fā)的事件流轉(zhuǎn)換成API請(qǐng)求流
    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));
    });
    
    //當(dāng)響應(yīng)到達(dá)時(shí)觸發(fā)
    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函數(shù)避免錯(cuò)誤被中斷
    }).subscribe(users => {
      console.log(users);
      renderUserLists(users)
    });
    
    //將數(shù)據(jù)渲染到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>`);
      }
    }
    

代碼已經(jīng)放在Github,如果你感興趣的話瑟曲,可以clone下來跑跑看饮戳。

總結(jié)

響應(yīng)式編程的思想比較不好理解,我在學(xué)習(xí)過程中洞拨,也查閱了很多的資料扯罐,只能算是剛剛?cè)腴T。所以烦衣,這篇文章也算是對(duì)整個(gè)學(xué)習(xí)過程的一次總結(jié)吧歹河。

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
響應(yīng)式編程一覽
C#設(shè)計(jì)模式系列(16)-迭代器模式
學(xué)習(xí)教程
RxJs GitBook
RxJs官方文檔
Introduction to reactive programming 視頻

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市花吟,隨后出現(xiàn)的幾起案子秸歧,更是在濱河造成了極大的恐慌,老刑警劉巖衅澈,帶你破解...
    沈念sama閱讀 216,470評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件键菱,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡矾麻,警方通過查閱死者的電腦和手機(jī)纱耻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來险耀,“玉大人弄喘,你說我怎么就攤上這事∷ξ” “怎么了蘑志?”我有些...
    開封第一講書人閱讀 162,577評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長贬派。 經(jīng)常有香客問我急但,道長,這世上最難降的妖魔是什么搞乏? 我笑而不...
    開封第一講書人閱讀 58,176評(píng)論 1 292
  • 正文 為了忘掉前任波桩,我火速辦了婚禮,結(jié)果婚禮上请敦,老公的妹妹穿的比我還像新娘镐躲。我一直安慰自己储玫,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評(píng)論 6 388
  • 文/花漫 我一把揭開白布萤皂。 她就那樣靜靜地躺著撒穷,像睡著了一般。 火紅的嫁衣襯著肌膚如雪裆熙。 梳的紋絲不亂的頭發(fā)上端礼,一...
    開封第一講書人閱讀 51,155評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音入录,去河邊找鬼蛤奥。 笑死,一個(gè)胖子當(dāng)著我的面吹牛纷跛,可吹牛的內(nèi)容都是我干的喻括。 我是一名探鬼主播,決...
    沈念sama閱讀 40,041評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼贫奠,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼唬血!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起唤崭,我...
    開封第一講書人閱讀 38,903評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤拷恨,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后谢肾,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體腕侄,經(jīng)...
    沈念sama閱讀 45,319評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評(píng)論 2 332
  • 正文 我和宋清朗相戀三年芦疏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了冕杠。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,703評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡酸茴,死狀恐怖分预,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情薪捍,我是刑警寧澤笼痹,帶...
    沈念sama閱讀 35,417評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站酪穿,受9級(jí)特大地震影響凳干,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜被济,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評(píng)論 3 325
  • 文/蒙蒙 一救赐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧只磷,春花似錦净响、人聲如沸少欺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至畏陕,卻和暖如春配乓,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背惠毁。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評(píng)論 1 269
  • 我被黑心中介騙來泰國打工犹芹, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鞠绰。 一個(gè)月前我還...
    沈念sama閱讀 47,711評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親酝掩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子等舔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)翁巍,斷路器驴一,智...
    卡卡羅2017閱讀 134,651評(píng)論 18 139
  • 關(guān)于Rxjs的現(xiàn)狀 鑒于響應(yīng)式編程近幾年才開始真正流行,而且響應(yīng)式的理念也并不是在所有領(lǐng)域都深得人心灶壶,對(duì)于不是特別...
    吧啦啦小湯圓閱讀 2,920評(píng)論 0 7
  • 一.背景介紹 Rx(Reactive Extension -- 響應(yīng)式擴(kuò)展 http://reactivex.io...
    愛上Shu的小刺猬閱讀 2,043評(píng)論 1 3
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,074評(píng)論 25 707
  • SONNET 20 造物主親手描繪一張女性的臉龐 [英] 莎士比亞 造物主親手描繪一張女性的臉龐 你這男主婦肝断,我愛...
    袁彼石閱讀 138評(píng)論 0 0