最近一直忙於工作,也沒倒開時間寫博客,組裏技術leader讓小編去做一次RxJS的技術分享,說時遲那時快果斷就打開博客準備先寫一遍中文版,之後譯成英文版發到公司confluence上,話不多少,開始吧~
一.基本概念
1.簡介
- Rx全名:Reactive Extension
- 源自於微軟,火於NetFlix
- 優勢:在思考的維度上加入時間考量
2.Js Bin運行例子
fromEvent案例
- HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<script src="https://unpkg.com/@reactivex/[email protected]/dist/global/Rx.js"></script>
</head>
<body>
<input id="height" type="number">
</body>
</html>
- ES6/Babel
const height = document.getElementById('height');
// 將Keyup事件變成Observable
// 約定俗成的寫法會將流狀的內容加一個$命名
const height$ = Rx.Observable.fromEvent(height, 'keyup');
height$.subscribe(val=> console.log(val.target.value + ' ' + new Date());
- Console
"1 Sun Jan 12 2020 14:24:06 GMT+0800 (中國標準時間)"
"12 Sun Jan 12 2020 14:24:07 GMT+0800 (中國標準時間)"
"123 Sun Jan 12 2020 14:24:07 GMT+0800 (中國標準時間)"
"1234 Sun Jan 12 2020 14:24:08 GMT+0800 (中國標準時間)"
combineLatest案例
- HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<script src="https://unpkg.com/@reactivex/[email protected]/dist/global/Rx.js"></script>
</head>
<body>
<div>
<input id="length" type="number">
</div>
<div>
<input id="width" type="number"/>
</div>
<div id="area"></div>
</body>
</html>
- ES6/Babel
const length = document.getElementById('length');
const width = document.getElementById('width');
const area = document.getElementById('area');
// 將Keyup事件變成Observable
// 約定俗成的寫法會將流狀的內容加一個$命名
const length$ = Rx.Observable.fromEvent(length, 'keyup').pluck('target', 'value');
const width$ = Rx.Observable.fromEvent(width, 'keyup').pluck('target', 'value');
// 爲了計算結果的話,需要進行合併
// combineLatest表示兩個流中只要有一個有最新的值,就重新計算一遍
// zip 表示兩個流都需要有新值出現,纔會重新計算一次
const area$ = Rx.Observable.combineLatest(length$, width$, (l, w) => {
return l*w;
});
area$.subscribe(val => {
area.innerHTML = val
});
- 思想
length: ----- 1 ------------- 3
width: ----------- 2 -------
area: ----------- (2,1) -- (2,3)
\ \
--------------2--------6------
3.總結
- Rx和傳統編程的區別在於要把任何變化想象成以時間爲維度的和事件流
- 通過監聽流的方法可以獲得流中對應的數據,而不是自己去再次獲取
二.常見操作符
1.常見創建類操作符
- from可以把數組、Promise、以及Iterator轉換爲Observable
- fromEvent可以把事件轉化爲Observable
- of接受一系列的數組,並把它們emit出去
- Interval
// interval每隔1s發射一個值,從0開始,發射的是位置數
// take只保留前三個數據,就觸發complete方法
const interval$ = Rx.Observable.interval(1000).take(3);
interval$.subscribe(next => {
console.log(next);
}, error => {
console.log(error);
}, complete => {
console.log('Complete');
});
- Timer
// timer的第一個參數是開始延遲多少毫秒發射,第二個參數是每次發射間隔是多少毫秒
// 如下事件會在開始後的2秒開始輸出0,之後間隔1秒輸出1 2 3 ...
// 如果只設置第一個參數則只會發射出一次
const timer$ = Rx.Observable.timer(2000, 1000)
timer$.subscribe(v=> console.log(v))
2.常見轉換操作符
- map:對原始流中的元素進行處理,映射成另一個元素
xxx.map(event=> event.target.value)
- mapTo:使用場景,不關心流中的值,只關心事件是否觸發(流是否發生改變)就行
const a$ = Rx.Observable.fromEvent(length, 'keyup').mapTo(1);
// 相當於.map(_=>1)
const b$ = Rx.Observable.fromEvent(width, 'keyup').mapTo(2);
Rx.Observable.combineLatest(a$, b$, (a, b) => a*b);
// 不管流內的lenth/width怎麼變化,a$的值都是1,b$的值的都是2
- pluck:從對應的流對象的值中獲取對應的屬性值
xxx.pluck('target', 'value')
3.常見工具類操作符
- do:可以作爲跟外部交互的橋樑,對相關內容進行處理
// interval每隔1s發射一個值,從0開始,發射的是位置數
// do作爲中間橋樑,可以做subscribe做的內容,因爲subscribe之後我們就無法使用操作符再進行處理了
// take只保留前三個數據,就觸發complete方法
const interval$ = Rx.Observable
.interval(1000)
.do( v => console.log(`當前值是:${v}`))
.take(3);
interval$.subscribe(next => {
console.log(next);
}, error => {
console.log(error);
}, complete => {
console.log('Complete');
})
輸出內容
"當前值是:0"
0
"當前值是:1"
1
"當前值是:2"
2
"Complete"
4.常見變化類操作符scan
- scan:第一個入參是上一個流元素處理後的內容,第二個入參是自定義操作內容參數,return的內容將作爲下一個scan的第一個入參
const interval$ = Rx.Observable
.interval(1000)
.filter( v => v%2===0) // 只保留是偶數的流數據
.scan((x,y) => { // x爲上一個return的值
return x+y;
})
.map(x => x+1)
.take(4);
interval$.subscribe(next => {
console.log(next);
}, error => {
console.log(error);
}, complete => {
console.log('Complete');
})
輸出內容
1
3
7
13
"Complete"
5.常見數學類操作符reduce
- reduce:可以通過reduce返回方法計算每一個流發射的元素值得到最終的值,即每一個流發射的元素需要有限個,否則又是一個Never類型的Observable
const interval$ = Rx.Observable
.interval(1000)
.filter( v => v%2===0) // 只保留是偶數的流數據
.take(4)
.reduce((x,y) => { // x爲上一個return的值
return x+y;
});
interval$.subscribe(next => {
console.log(next);
}, error => {
console.log(error);
}, complete => {
console.log('Complete');
})
輸出內容
12
"Complete"
6.過濾類操作符
- filter:設置根據某種條件過濾流的作用
// interval每隔1s發射一個值,從0開始,發射的是位置數
// filter過濾條件保留指定流內容
// do作爲中間橋樑,可以做subscribe做的內容,因爲subscribe之後我們就無法使用操作符再進行處理了
// take只保留前三個數據,就觸發complete方法
const interval$ = Rx.Observable
.interval(1000)
.filter( v => v%2===0) // 只保留是偶數的流數據
.do( v => console.log(`當前值是:${v}`))
.take(3);
interval$.subscribe(next => {
console.log(next);
}, error => {
console.log(error);
}, complete => {
console.log('Complete');
})
輸出內容
"當前值是:0"
0
"當前值是:2"
2
"當前值是:4"
4
"Complete"
- take:參數設置保留前幾個流的作用
- first/last:只保留流的第一個發射內容 / 最後一個發射內容
const interval$ = Rx.Observable
.interval(1000)
.filter( v => v%2===0) // 只保留是偶數的流數據
.do( v => console.log(`當前值是:${v}`))
.first();
interval$.subscribe(next => {
console.log(next);
}, error => {
console.log(error);
}, complete => {
console.log('Complete');
})
輸出內容
"當前值是:0"
0
"Complete"
- skip:過濾幾個元素再發射
const interval$ = Rx.Observable
.interval(1000)
.filter( v => v%2===0) // 只保留是偶數的流數據
.do( v => console.log(`當前值是:${v}`))
.skip(2);
interval$.subscribe(next => {
console.log(next);
}, error => {
console.log(error);
}, complete => {
console.log('Complete');
})
輸出內容[因爲有do所以會輸出 當前值是x 的內容,但沒發射對應元素]
"當前值是:0"
"當前值是:2"
"當前值是:4"
4
"當前值是:6"
6
"當前值是:8"
8
"當前值是:10"
10
- debounce:可以想象成一個濾波器(整流器),希望得到的流是符合時間間隔的,不符合的就拋棄掉,參數接的是Observable對象,比debounceTime更靈活,可參見debounceTime的樣例代碼
- debounceTime:可以想象成一個基於時間的濾波器,希望得到的流是符合時間間隔的,不符合的就拋棄掉[通常用於搜索的時候發送網絡請求]
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<script src="https://unpkg.com/@reactivex/[email protected]/dist/global/Rx.js"></script>
</head>
<body>
<div>
<input id="length" type="number">
</div>
</body>
</html>
const length = document.getElementById('length');
// 使用debounceTime將2秒內的流發射過濾掉,保留最後的發射內容
// 即只有停下來了2秒了,內容纔會輸出
const length$ = Rx.Observable
.fromEvent(length, 'keyup')
.pluck('target', 'value')
.debounceTime(2000);
// 相當於.debounce(()=> Rx.Observable.interval(300));
length$.subscribe(val => console.log(val));
輸出內容
當輸入停下2秒後,輸出輸入框中的內容
- distinct:只保留整個流中不一樣的內容,重複的內容拋棄掉
const length = document.getElementById('length');
const length$ = Rx.Observable
.fromEvent(length, 'keyup')
.pluck('target', 'value')
.distinct();
length$.subscribe(val => console.log(val));
輸出內容
在輸入框中分別輸入
1
12
123
12
1
1234
----------輸出結果爲----------
1
12
123
1234
-----即重複的內容不會輸出
- distinctUntilChanged:只跟前一個元素比,如果前一個元素是黃色,如下一個也是黃色就拋棄掉,如是其他顏色則發射
const length = document.getElementById('length');
const length$ = Rx.Observable
.fromEvent(length, 'keyup')
.pluck('target', 'value')
.distinctUntilChanged();
length$.subscribe(val => console.log(val));
輸出內容
在輸入框中分別輸入
1
12
123
123
12
123
----------輸出結果爲----------
1
12
123
12
123
-----即僅與流中上一個元素重複的內容不會輸出
7. 合併類操作符
- merge:將兩個流按各自時間順序給疊加成一個流
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<script src="https://unpkg.com/@reactivex/[email protected]/dist/global/Rx.js"></script>
</head>
<body>
<div>
<input id="length" type="number">
<input id="width" type="number">
</div>
</body>
</html>
const length = document.getElementById('length');
const width = document.getElementById('width');
const length$ = Rx.Observable
.fromEvent(length, 'keyup')
.pluck('target', 'value');
const width$ = Rx.Observable
.fromEvent(width, 'keyup')
.pluck('target', 'value');
const merged$ = Rx.Observable
.merge(length$, width$);
merged$.subscribe(val => console.log(val));
輸出內容
在輸入框中先後輸入
1 空
1 2
13 2
13 24
134 24
1344 24
--------------------輸出結果-----------------------
"1"
"2"
"13"
"24"
"134"
"1344"
- concat:先完成第一個流中所有發射內容,再進行第二個流的所有發射內容
const length = document.getElementById('length');
const width = document.getElementById('width');
const length$ = Rx.Observable
.fromEvent(length, 'keyup')
.pluck('target', 'value');
const width$ = Rx.Observable
.fromEvent(width, 'keyup')
.pluck('target', 'value');
const merged$ = Rx.Observable
.concat(length$, width$);
merged$.subscribe(val => console.log(val));
輸出內容
在第一個框中輸入的內容會一直輸出到console中,第二個輸入框中輸入的內容會一直無法輸出到console中
因爲第一個框的事件是無窮的序列,所以第二個流永遠無法執行到
- startWith:指定流發射的初始值,會先發射指定值。實際場合都是爲了流填一個初始值,以免流沒有初始值而報錯。
const startWith$ = Rx.Observable
.from([1,2,3,4])
.startWith(0)
startWith$.subscribe(val => console.log(val));
輸出內容
0
1
2
3
4
- combineLatest:簡單的說兩個流中有一個新元素生成的時候,就會在新流中生成一個新的元素
const length = document.getElementById('length');
const width = document.getElementById('width');
const length$ = Rx.Observable
.fromEvent(length, 'keyup')
.pluck('target', 'value');
const width$ = Rx.Observable
.fromEvent(width, 'keyup')
.pluck('target', 'value');
const combineLatest$ = Rx.Observable
.combineLatest(length$, width$, (l, w)=> {
return l*w;
})
combineLatest$.subscribe(val => console.log(val));
輸出結果
會根據兩個輸入框輸入的內容即是獲得相乘的結果
- withLatestFrom:區別-withLatestFrom是以源事件流爲基準,當基準流產生值的時候會去取另一個流的最新值放到新的流中【只有第一個流改變纔會有輸出,第二個流改變不會給新流增加內容】
- zip:區別-zip有對齊的特性,當兩個流都有一個新的內容的時候,會組成新流中的一個元素【看彈珠圖就可以理解,可以直接在jsbin中嘗試】
const length = document.getElementById('length');
const width = document.getElementById('width');
const length$ = Rx.Observable
.fromEvent(length, 'keyup')
.pluck('target', 'value');
const width$ = Rx.Observable
.fromEvent(width, 'keyup')
.pluck('target', 'value');
const zip$ = Rx.Observable
.zip(length$, width$, (l, w)=> {
return l*w;
})
zip$.subscribe(val => console.log(val));
三.Observable的性質
- Observable有三種狀態:next / error / complete 作爲subscribe的三個方法參數,但這三個狀態不一定都會達到,只是到對應狀態的時候調用對應的方法
const interval$ = Rx.Observable.interval(1000).take(3);
interval$.subscribe(next => {
console.log(next);
}, error => {
console.log(error);
}, complete => {
console.log('Complete');
})
- 特殊的狀態: Never永不結束(如:計時器每隔幾秒發送一些內容) / Empty(不發射內容但直接結束) / Throw(直接進入error狀態)
// Never狀態如下,會一直髮射,因爲不知道流什麼時候結束
const interval$ = Rx.Observable
.interval(1000)
.filter( v => v%2===0) // 只保留是偶數的流數據
.do( v => console.log(`當前值是:${v}`))
.last();
interval$.subscribe(next => {
console.log(next);
}, error => {
console.log(error);
}, complete => {
console.log('Complete');
})
// 手動拋出error,並在error狀態函數中捕獲並處理
const interval$ = Rx.Observable
.interval(1000)
.map(v => {
throw '拋出異常'; // 此時就會在error中輸出
})
.take(4)
.reduce((x,y) => { // x爲上一個return的值
return x+y;
});
interval$.subscribe(next => {
console.log(next);
}, error => {
console.log(error);
}, complete => {
console.log('Complete');
})
// 通常用於測試當中,使用官方的方式來創建never / error / empty 流
// 官方自帶的never
const never$ = Rx.Observable.never();
// 官方自帶的throw error
const throw$ = Rx.Observable.throw('出錯了');
// 官方自帶的empty()調用後悔直接調用complete方法中的內容
const empty$ = Rx.Observable.empty();