淺析前端狀態管理

文章首發於:github.com/USTB-musion…html

寫在前面

前端技術的發展突飛猛進,vue,react,angular等的興起,爲咱們帶來了新的開發體驗。但隨着技術的革新,以及前端頁面複雜度的提高,對應有localStorage,eventBus,vuex,redux,mobx,rxjs等數據存儲和管理的方案,因此以爲研究狀態管理仍是頗有必要的。因此最近花了一些時間研究一下這方面的知識。在分析的過程中可能有本身理解出誤差或者你們有理解不同的地方,歡迎你們評論或私信我。前端

本文將從如下幾部分進行總結:vue

  1. 數據驅動視圖
  2. 組件間數據通訊和eventBus
  3. 單項數據流(vuex && redux)
  4. 更好用的mobx
  5. 實現一個超簡易版的redux和react-redux

數據驅動視圖

如今前端最火的react和vue,使用的設計思路都是數據驅動視圖,即UI = f(state),當頁面發生變化的時候,無須關心DOM的變化,只需關心state的變化便可。state映射到UI這個過程交給框架來處理。爲了解決性能上的問題,Virtual DOM產生了。有了Virtual DOM以後,數據驅動視圖能夠簡單地分爲四個步驟:react

  • 數據變化,生成新的Virtual DOM
  • 經過diff算法比對新的Virtual DOM和舊的Virtual DOM的異同
  • 生成新舊對象的差別(patch)
  • 遍歷差別對象並更新DOM

有了react和vue以後,state => UI這一過程有了很好的實踐,但反過來呢,如何在UI中合理地修改state中成爲了一個新的問題。爲此,Facebook提出了flux思想。具體能夠參考阮一峯這一篇文章Flux 架構入門教程。簡單地說,Flux 是一種架構思想,它認爲之前的MVC框架存在一些問題,因而打算用一個新的思惟來管理數據流轉。git

組件間數據通訊和eventBus

數據能夠簡單地分爲兩個部分,跨組件的數據和組件內的數據。組件內的數據大多數是和UI相關的,好比說單選框是否被勾選,按鈕是否被點擊。這些能夠稱爲組件內狀態數據。在react中,有一個概念叫作木偶組件,它裏邊存儲的數據就是組件內狀態數據。其實在市面上的不少UI組件庫如element,ant design提供的都是木偶組件。另一種數據就是跨組件的數據,好比父組件喚起子組件關閉,一旦面臨着跨組件的交互,咱們面臨的問題就開始變得複雜了。這時候就須要一個機制來處理父子和兄弟組件通訊。父組件對子組件就是props的傳遞,子組件對父組件react的處理方式就是父組件傳遞給子組件一個處理函數,由子組件調用,這樣數據就由函數參數傳給來父組件。vue的處理方式就是子組件經過$emit一個函數將數據由函數參數傳給父組件由父組件接收調用。github

eventBus則爲中央通訊,provide是一個對象或返回一個對象的函數。該對象包含可注入其子孫的屬性: 算法

inject 選項能夠是:一個字符串數組,或一個對象。而後經過inject注入的值做爲數據入口: vuex

但對於多個視圖須要依賴於統一狀態或者來自於不一樣視圖的行爲須要變動同一狀態。單單依賴於組件間的通訊就顯得有些雞肋了。編程

單項數據流(vuex && redux)

下面用一張圖來分別介紹如下redux和react的數據流是怎樣的: redux

Redux的數據具體是如何流動的,簡單來就是說每一個事件會發送一個action,action經過dispatch觸發reducer,直接依據舊的state生成一個新state替代最頂層的store裏面原有的state。

Redux強調三大基本原則:

  • 惟一數據源
  • 保持狀態只讀
  • 數據改變只能經過純函數完成

以todo-list爲例,代碼託管在github上:Github

惟一數據源: 惟一數據源指的是應用的狀態數據應該只存儲在惟一的一個Store上。todo-list應用的Store狀態樹大概是這樣子:

{
    todos: [
        {
            text: 'First todo',
            completed: false,
            id: 0
        },
        {
            text: 'Second todo',
            completed: false,
            id: 1
        }
    ],
    filter: 'all'
}
複製代碼

保持狀態可讀: 要修改Store的狀態,必需要經過派發一個action對象完成。根據UI=render(state),要驅動用戶界面渲染,就要改變應用的狀態,可是改變狀態的方法不是去修改狀態上的值,而是建立一個新的狀態對象返回給Redux,由Redux完成新的狀態的組裝。

數據改變只能經過純函數完成: reducer必需要是一個純函數,每一個reducer函數格式以下:reducer(state, action):

import {ADD_TODO, TOGGLE_TODO, REMOVE_TODO}from './actionTypes.js';

export default (state = [], action) => {
  switch(action.type) {
    case ADD_TODO: {
      return [
        {
          id: action.id,
          text: action.text,
          completed: false
        },
        ...state
      ]
    }
    case TOGGLE_TODO: {
      return state.map((todoItem) => {
        if (todoItem.id === action.id) {
           return {...todoItem, completed: !todoItem.completed};
        } else {
          return todoItem;
        }
      })
    }
    case REMOVE_TODO: {
      return state.filter((todoItem) => {
        return todoItem.id !== action.id;
      })
    }
    default: {
      return state;
    }
  }
}
複製代碼

下面用官網的一張圖來介紹如下vuex:

vuex能夠說是專門爲vue設計的狀態管理工具。和 Redux 中使用不可變數據來表示 state 不一樣,Vuex 中沒有 reducer 來生成全新的 state 來替換舊的 state,Vuex 中的 state 是能夠被修改的。這麼作的緣由和 Vue 的運行機制有關係,Vue 基於 ES5 中的 getter/setter 來實現視圖和數據的雙向綁定,所以 Vuex 中 state 的變動能夠經過 setter 通知到視圖中對應的指令來實現視圖更新。

Vuex 中的 state 是可修改的,而修改 state 的方式不是經過 actions,而是經過 mutations。更改 Vuex 的 store 中的狀態的惟一方法是提交 mutation。

vuex的數據流簡單地說爲:

  • 在視圖中觸發 action,並根據實際狀況傳入須要的參數
  • 在 action 中觸發所需的 mutation,在 mutation 函數中改變 state
  • 經過 getter/setter 實現的雙向綁定會自動更新對應的視圖

更好用的mobx

MobX 是經過透明的函數響應式編程(transparently applying functional reactive programming - TFRP)使得狀態管理變得簡單和可擴展。如下爲mobx的流程圖:

mobx和redux相對比,就有點差異了,若是說redux是體現函數式編程,mobx則更多體現面向對象的特色。 mobx由幾個要點:

  • Observable。它的 state 是可被觀察的,不管是基本數據類型仍是引用數據類型,均可以使用 MobX 的 (@)observable 來轉變爲 observable value。源
  • Reactions。它包含不一樣的概念,基於被觀察數據的更新致使某個計算值(computed values),或者是發送網絡請求以及更新視圖等,都屬於響應的範疇,這也是響應式編程(Reactive Programming)在 JavaScript 中的一個應用。
  • Actions。它至關於全部響應的源頭,例如用戶在視圖上的操做,或是某個網絡請求的響應致使的被觀察數據的變動。

實現一個簡易版的redux和react-redux

簡單實現redux的createStore,dispatch,subscribe, reducer, getState方法

function createStore (reducer) {
  let state = null
  const listeners = []
  const subscribe = (listener) => listeners.push(listener) // 觀察者模式實現監控數據變化
  const getState = () => state
  const dispatch = (action) => { //用於修改數據
    state = reducer(state, action) // reducer接受state和action
    listeners.forEach((listener) => listener())
  }
  dispatch({}) // 初始化 state
  return { getState, dispatch, subscribe } // 暴露出三個方法
}
複製代碼

簡單實現react-redux的Provider,connect,mapStateToProps, mapDispatchToProps方法 實現Provider方法:

export class Provider extends Component {
  static propTypes = {
    store: PropTypes.object,
    children: PropTypes.any
  }

  static childContextTypes = {
    store: PropTypes.object
  }

  getChildContext () {
    return {
      store: this.props.store
    }
  }

  render () {
    return (
      <div>{this.props.children}</div>
    )
  }
}
複製代碼

這樣就能用

<Provider store={store}>
    
</Provider>
複製代碼

包裹根組件了。

實現connect方法,約定傳入mapStateToProps和mapDispatchToprops:

export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
  class Connect extends Component {
    static contextTypes = {
      store: PropTypes.object
    }

    constructor () {
      super()
      this.state = {
        allProps: {}
      }
    }

    componentWillMount () {
      const { store } = this.context
      this._updateProps()
      store.subscribe(() => this._updateProps())
    }

    _updateProps () {
      const { store } = this.context
      let stateProps = mapStateToProps
        ? mapStateToProps(store.getState(), this.props)
        : {} // 防止 mapStateToProps 沒有傳入
      let dispatchProps = mapDispatchToProps
        ? mapDispatchToProps(store.dispatch, this.props)
        : {} // 防止 mapDispatchToProps 沒有傳入
      this.setState({
        allProps: {
          ...stateProps,
          ...dispatchProps,
          ...this.props
        }
      })
    }

    render () {
      return <WrappedComponent {...this.state.allProps} />
    }
  }
  return Connect
}
複製代碼

總結

若是項目技術棧是基於vue的話,狀態管理用vuex無疑是更好的選擇。但若是技術棧是基於react,在redux和mobx的選擇之間就仁者見仁,智者見智了。選擇mobx的緣由多是沒有redux那麼多的流程,改變一個狀態得去好幾個文件裏找代碼。還有就是學習成本少,可能看下文檔就能上手了。但缺點就是過於自由,提供的約定很是少,作大型項目就有點雞肋了。但redux給開發者添加了許多限制,但就是這些限制,作大型項目時就不容易寫亂。

參考文章

vuex中文文檔

redux中文文檔

淺談前端狀態管理(上)

前端狀態管理請三思

前端數據管理與前端框架選擇

相關文章
相關標籤/搜索