這篇文章帶給大家的內容是關於前端響應式程式設計的方案及其缺點的詳細介紹(附程式碼),有一定的參考價值,有需要的朋友可以參考一下,希望對你有所幫助。
現實世界有很多是以回應式的方式運作的,例如我們會在收到他人的提問,然後做出回應,給予相應的答案。在開發過程中我也應用了大量的響應式設計,累積了一些經驗,希望能拋磚引玉。
響應式程式設計(Reactive Programming)和普通的程式設計思路的主要區別在於,響應式以推(push)的方式運作,而非響應式的程式設計思路以拉(pull)的方式運作。例如,事件就是一個很常見的響應式編程,我們通常會這麼做:
button.on('click', () => { // ...})
而非響應式方式下,就會變成這樣:
while (true) { if (button.clicked) { // ... } }
顯然,無論在是程式碼的優雅度還是執行效率上,非響應式的方式都不如響應式的設計。
Event Emitter
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採用了一個事件流的方式實作響應式,在Redux中由於reducer必須是純函數,因此要實現響應式的方式只有訂閱中或者是在中間件中。
如果透過訂閱store的方式,由於Redux不能準確拿到哪一個資料放生了變化,因此只能透過髒檢查的方式。例如:
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的元件,會隨著元件的掛載和卸載被創建和銷毀,如果我們希望這個響應式和元件無關的話就有問題了。
另一種方式就是在中間件監聽資料變化。由於採用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秒再進行搜尋。
相關推薦:
拼圖響應式前端框架版響應式後台正式發布_html/css_WEB-ITnose
#使用很簡單的響應式前端開發框架_html/css_WEB-ITnose
以上是前端響應式程式設計的方案及其缺點的詳細介紹(附程式碼)的詳細內容。更多資訊請關注PHP中文網其他相關文章!