現實世界有不少是以響應式的方式運做的,例如咱們會在收到他人的提問,而後作出響應,給出相應的回答。在開發過程當中我也應用了大量的響應式設計,積累了一些經驗,但願能拋磚引玉。react
響應式編程(Reactive Programming)和普通的編程思路的主要區別在於,響應式以推(push
)的方式運做,而非響應式的編程思路以拉(pull
)的方式運做。例如,事件就是一個很常見的響應式編程,咱們一般會這麼作:ajax
button.on('click', () => {
// ...
})
複製代碼
而非響應式方式下,就會變成這樣:編程
while (true) {
if (button.clicked) {
// ...
}
}
複製代碼
顯然,不管在是代碼的優雅度仍是執行效率上,非響應式的方式都不如響應式的設計。redux
Event Emitter
是大多數人都很熟悉的事件實現,它很簡單也很實用,咱們能夠利用Event Emitter
實現簡單的響應式設計,例以下面這個異步搜索:異步
class Input extends Component {
state = {
value: ''
}
onChange = e => {
this.props.events.emit('onChange', e.target.value)
}
afterChange = value => {
this.setState({
value
})
}
componentDidMount() {
this.props.events.on('onChange', this.afterChange)
}
componentWillUnmount() {
this.props.events.off('onChange', this.afterChange)
}
render() {
const { value } = this.state
return (
<input value={value} onChange={this.onChange} /> ) } } class Search extends Component { doSearch = (value) => { ajax(/* ... */).then(list => this.setState({ list })) } componentDidMount() { this.props.events.on('onChange', this.doSearch) } componentWillUnmount() { this.props.events.off('onChange', this.doSearch) } render() { const { list } = this.state return ( <ul> {list.map(item => <li key={item.id}>{item.value}</li>)} </ul> ) } } 複製代碼
這裏咱們會發現用Event Emitter
的實現有不少缺點,須要咱們手動在componentWillUnmount
裏進行資源的釋放。它的表達能力不足,例如咱們在搜索的時候須要聚合多個數據源的時候:函數
class Search extends Component {
foo = ''
bar = ''
doSearch = () => {
ajax({
foo,
bar
}).then(list => this.setState({
list
}))
}
fooChange = value => {
this.foo = value
this.doSearch()
}
barChange = value => {
this.bar = value
this.doSearch()
}
componentDidMount() {
this.props.events.on('fooChange', this.fooChange)
this.props.events.on('barChange', this.barChange)
}
componentWillUnmount() {
this.props.events.off('fooChange', this.fooChange)
this.props.events.off('barChange', this.barChange)
}
render() {
// ...
}
}
複製代碼
顯然開發效率很低。性能
Redux
採用了一個事件流的方式實現響應式,在Redux
中因爲reducer
必須是純函數,所以要實現響應式的方式只有訂閱中或者是在中間件中。fetch
若是經過訂閱store
的方式,因爲Redux
不能準確拿到哪個數據放生了變化,所以只能經過髒檢查的方式。例如:this
function createWatcher(mapState, callback) {
let previousValue = null
return (store) => {
store.subscribe(() => {
const value = mapState(store.getState())
if (value !== previousValue) {
callback(value)
}
previousValue = value
})
}
}
const watcher = createWatcher(state => {
// ...
}, () => {
// ...
})
watcher(store)
複製代碼
這個方法有兩個缺點,一是在數據很複雜且數據量比較大的時候會有效率上的問題;二是,若是mapState
函數依賴上下文的話,就很難辦了。在react-redux
中,connect
函數中mapStateToProps
的第二個參數是props
,能夠經過上層組件傳入props
來得到須要的上下文,可是這樣監聽者就變成了React
的組件,會隨着組件的掛載和卸載被建立和銷燬,若是咱們但願這個響應式和組件無關的話就有問題了。spa
另外一種方式就是在中間件中監聽數據變化。得益於Redux
的設計,咱們經過監聽特定的事件(Action)就能夠獲得對應的數據變化。
const search = () => (dispatch, getState) => {
// ...
}
const middleware = ({ dispatch }) => next => action => {
switch action.type {
case 'FOO_CHANGE':
case 'BAR_CHANGE': {
const nextState = next(action)
// 在本次dispatch完成之後再去進行新的dispatch
setTimeout(() => dispatch(search()), 0)
return nextState
}
default:
return next(action)
}
}
複製代碼
這個方法能解決大多數的問題,可是在Redux
中,中間件和reducer
實際上隱式訂閱了全部的事件(Action),這顯然是有些不合理的,雖然在沒有性能問題的前提下是徹底能夠接受的。
ECMASCRIPT 5.1
引入了getter
和setter
,咱們能夠經過getter
和setter
實現一種響應式。
class Model {
_foo = ''
get foo() {
return this._foo
}
set foo(value) {
this._foo = value
this.search()
}
search() {
// ...
}
}
// 固然若是沒有getter和setter的話也能夠經過這種方式實現
class Model {
foo = ''
getFoo() {
return this.foo
}
setFoo(value) {
this.foo = value
this.search()
}
search() {
// ...
}
}
複製代碼
Mobx
和Vue
就使用了這樣的方式實現響應式。固然,若是不考慮兼容性的話咱們還能夠使用Proxy
。
當咱們須要響應若干個值而後獲得一個新值的話,在Mobx
中咱們能夠這麼作:
class Model {
@observable hour = '00'
@observable minute = '00'
@computed get time() {
return `${this.hour}:${this.minute}`
}
}
複製代碼
Mobx
會在運行時收集time
依賴了哪些值,並在這些值發生改變(觸發setter
)的時候從新計算time
的值,顯然要比EventEmitter
的作法方便高效得多,相對Redux
的middleware
更直觀。
可是這裏也有一個缺點,基於getter
的computed
屬性只能描述y = f(x)
的情形,可是現實中不少狀況f
是一個異步函數,那麼就會變成y = await f(x)
,對於這種情形getter
就沒法描述了。
對於這種情形,咱們能夠經過Mobx
提供的autorun
來實現:
class Model {
@observable keyword = ''
@observable searchResult = []
constructor() {
autorun(() => {
// ajax ...
})
}
}
複製代碼
因爲運行時的依賴收集過程徹底是隱式的,這裏常常會遇到一個問題就是收集到意外的依賴:
class Model {
@observable loading = false
@observable keyword = ''
@observable searchResult = []
constructor() {
autorun(() => {
if (this.loading) {
return
}
// ajax ...
})
}
}
複製代碼
顯然這裏loading
不該該被搜索的autorun
收集到,爲了處理這個問題就會多出一些額外的代碼,而多餘的代碼容易帶來犯錯的機會。 或者,咱們也能夠手動指定須要的字段,可是這種方式就不得很少出一些額外的操做:
class Model {
@observable loading = false
@observable keyword = ''
@observable searchResult = []
disposers = []
fetch = () => {
// ...
}
dispose() {
this.disposers.forEach(disposer => disposer())
}
constructor() {
this.disposers.push(
observe(this, 'loading', this.fetch),
observe(this, 'keyword', this.fetch)
)
}
}
class FooComponent extends Component {
this.mode = new Model()
componentWillUnmount() {
this.state.model.dispose()
}
// ...
}
複製代碼
而當咱們須要對時間軸作一些描述時,Mobx
就有些力不從心了,例如須要延遲5秒再進行搜索。
在下一篇博客中,將介紹Observable
處理異步事件的實踐。