上一篇部落格提到了幾種響應式的方案,以及它們的缺點。本文將介紹Observable
以及它的一個實現,以及它在處理響應式時相對於上篇部落格中的方案的巨大優勢(推薦兩篇部落格對比閱讀)。
Observable
是一個集合了觀察者模式、迭代器模式和函式式的庫,提供了基於事件流的強大的非同步處理能力,並且已在Stage 1
草案中。本文介紹的Rxjs
是Observable
的一個實現,它是ReactiveX眾多語言中的JavaScript
版本。
在JavaScript
中,我們可以使用T | null
去處理一個單值,使用Iterator
去處理多個值得情況,使用Promise
處理非同步的單個值,而Observable
則填補了缺失的“非同步多個值”。
單個值 | 多個值 | |
---|---|---|
同步 | T | null |
Iterator<T> |
非同步 | Promise<T> |
Observable<T> |
使用Rxjs
上文提到使用Event Emitter
做響應式處理,在Rxjs
中稍有些不同:
/*
const change$ = new Subject();
<Input change$={change$} />
<Search change$={change$} />
*/
class Input extends Component {
state = {
value: ''
};
onChange = e => {
this.props.change$.next(e.target.value);
};
componentDidMount() {
this.subscription = this.props.change$.subscribe(value => {
this.setState({
value
});
});
}
componentWillUnmount() {
this.subscription.ubsubscribe();
}
render() {
const { value } = this.state;
return <input value={value} onChange={this.onChange} />;
}
}
class Search extends Component {
// ...
componentDidMount() {
this.subscription = this.props.change$.subscribe(value => {
ajax(/* ... */).then(list =>
this.setState({
list
})
);
});
}
componentWillUnmount() {
this.subscription.ubsubscribe();
}
render() {
const { list } = this.state;
return <ul>{list.map(item => <li key={item.id}>{item.value}</li>)}</ul>;
}
}
複製程式碼
在這裡,我們雖然也需要手動釋放對事件的訂閱,但是得益於Rxjs
的設計,我們不需要像Event Emitter
那樣去存下回撥函式的例項,用於釋放訂閱,因此我們很容易就可以通過高階元件解決這個問題。例如:
const withObservables = observables => ChildComponent => {
return class extends Component {
constructor(props) {
super(props);
this.subscriptions = {};
this.state = {};
Object.keys(observables).forEach(key => {
this.subscriptions[key] = observables[key].subscribe(value => {
this.setState({
[key]: value
});
});
});
}
onNext = (key, value) => {
observables[key].next(value);
};
componentWillUnmount() {
Object.keys(this.subscriptions).forEach(key => {
this.subscriptions[key].unsubscribe();
});
}
render() {
return (
<ChildComponent {...this.props} {...this.state} onNext={this.onNext} />
);
}
};
};
複製程式碼
這樣在需要聚合多個資料來源時,也不會像Event Emitter
那樣手動釋放資源造成麻煩。同時,在Rxjs
中我們還有專用於聚合資料來源的方法:
Observable.combineLatest(foo$, bar$)
.pipe(
// ...
);
複製程式碼
顯然相對於Event Emitter
的方式十分高效,同時它相對於Mobx
也有巨大的優勢。在Mobx
中,我們提到需要聚合多個資料來源的時候,採用autoRun
的方式容易收集到不必要的依賴,使用observe
則不夠高效。在Rxjs
中,顯然不會有這些問題,combineLatest
可以以很簡練的方式宣告需要聚合的資料來源,同時,得益於Rxjs
設計,我們不需要像Mobx
一個一個去呼叫observe
返回的析構,只需要處理每一個subscribe
返回的subscription
:
class Foo extends Component {
constructor(props) {
super(props);
this.subscription = Observable.combineLatest(foo$, bar$)
.pipe(
// ...
)
.subscribe(() => {
// ...
});
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
}
複製程式碼
非同步處理
Rxjs
使用操作符去描述各種行為,每一個操作符會返回一個新的Observable
,我們可以對它進行後續的操作。例如,使用map
操作符就可以實現對資料的轉換:
foo$.map(event => event.target.value);
複製程式碼
Rxjs 5.5
之後所有的Observable
上都引入了一個pipe
方法,接收若干個操作符,pipe
方法會返回一個Observable
。因此,我們可以很容易配合tree shaking
實現對操作符的按需引入,而不是把整個Rxjs
引入進來:
import { map } from 'rxjs/operators';
foo$.pipe(map(event => event.target.value));
複製程式碼
推薦使用這種寫法。
在討論物件導向的響應式的響應式中,我們提到對於非同步的問題,物件導向的方式不好處理。在Observable
中我們可以通過switchMap
操作符處理非同步問題,一個非同步搜尋看起來會是這樣:
input$.pipe(switchMap(keyword => Observable.ajax(/* ... */)));
複製程式碼
在處理非同步單值時,我們可以使用Promise
,而Observable
用於處理非同步多個值,我們可以很容易把一個Promise
轉成一個Observable
,從而複用已有的非同步程式碼:
input$.pipe(switchMap(keyword => fromPromise(search(/* ... */))));
複製程式碼
switchMap
接受一個返回Observable
的函式作為引數,下游的流就會切到這個返回的Observable
。
而要聚合多個資料來源並做非同步處理時:
combineLatest(foo$, bar$).pipe(
switchMap(keyword => fromPromise(someAsyncOperation(/* ... */)))
);
複製程式碼
同時,由於標準制定的Promise
是沒有cancel
方法的,有時候我們要取消非同步方法的時候就有些麻煩(主要是為了解決一些併發安全問題)。switchMap
當上遊有新值到來時,會忽略結束已有未完成的Observable
然後呼叫函式返回一個新的Observable
,我們只使用一個函式就解決了併發安全問題。當然,我們可以根據實際需要選用switchMap
、mergeMap
、concatMap
、exhaustMap
等。
而對於時間軸的操作,Rxjs
也有巨大優勢。上篇部落格中提到當我們需要延時 5 秒做操作時,無論是Event Emitter
還是物件導向的方式都力不從心,而在Rxjs
中我們只需要一個delay
操作符即可解決問題:
input$.pipe(
delay(5000) // 下游會在input$值到來後5秒才接到資料
);
複製程式碼
用 Rxjs 處理資料
在實際開發過程中,事件不能解決所有問題,我們往往會需要儲存資料,而Observable
被設計成用於處理事件,因此它有很多符合事件直覺的設計。
Observable
被設計為懶(lazy
)的,噹噹沒有訂閱者時,一個流不會執行。對於事件而言,沒有事件的消費者那麼不執行也不會有問題。而在 GUI 中,訂閱者可能是View
:
class View extends Component {
state = {
input: ''
};
componentDidMount() {
this.subscription = input$.subscribe(input => {
this.setState({
input
});
});
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
render() {
// ...
}
}
複製程式碼
由於這個View
可能不存在,例如路由被切走了,那麼我們的事件源就沒有了訂閱者,他就不會執行。但是我們希望在路由被且走後,後臺的資料依然會繼續。
對於事件而言,在事件發生之後的訂閱者不會受到訂閱之前的邏輯。例如在EventEmitter
中:
eventEmitter.emit('hello', 1);
// ...
eventEmitter.on('hello', function listener() {});
複製程式碼
由於listener
是在hello
事件發生後在監聽的,不會收到值為1
的事件。但是這在處理資料的時候會造成麻煩,我們的資料在View
被解除安裝(例如路由切走)後丟失。
同時,由於Observable
沒有提供直接取到內部狀態的方法,當我們使用Observable
處理資料時,我們不方便隨時拿到資料。那有辦法解決這個問題,從而使Observable
強大抽象能力去賦能資料層呢?
回到Redux
。Redux
的事件(Action)其實是一個事件流,那麼我們就可以很自然地把Redux
的事件流融入到Rxjs
流中:
() => next => {
const action$ = new Subject();
return action => {
action$.next(action);
// ...
};
};
複製程式碼
通過這樣的封裝,redux-observable就能讓我們把Observable
強大的事件描述和處理能力和Redux
結合。我們可以非常方便地根據Action
去處理副作用:
action$.pipe(
ofType('ACTION_1'),
switchMap(() => {
// ...
}),
map(res => ({
type: 'ACTION_2',
payload: res
}))
);
action$.pipe(
ofType('ACTION_3'),
mergeMap(() => {
// ...
}),
map(res => ({
type: 'ACTION_4',
payload: res
}))
);
複製程式碼
Redux Observable
使我們可以結合Redux
和Observable
。在這裡,Action
被視作一個流,ofType
相當於filter(action => action.type === 'SOME_ACTION')
,從而得到需要監聽的Action
,得益於Redux
的設計,我們可以通過監聽Action
去完成副作用的處理或者監聽資料變化。最後這個流返回一個新的Action
流,Redux Observable
會把這個新的Action
流中的Action
dispatch
出去。由此,我們在使用Redux
儲存資料的基礎上獲得了Rxjs
對非同步事件的強大處理能力。
本文首發於(https://tech.youzan.com/reactive2/)[有贊技術部落格]。