隨著前端框架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
被賦值為B
和C
的值胀莹。這時(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)用范圍
- 多線程揍堰,時(shí)間處理鹏浅,阻塞等場(chǎng)景
- ajax,websocket和數(shù)據(jù)加載
- 失敗處理
- 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)單地來說就是利用Observable
和LINQ
風(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ù)揭鳞。那么Observable
和Promise
相比炕贵,又有什么區(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:
其實(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è)需求吧馒吴。
-
先創(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>
?
-
寫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 視頻