淺析前端狀態管理

慕晨同學發表於2018-10-27

寫在前面

前端技術的發展日新月異,vue,react,angular等的興起,為我們帶來了新的開發體驗。但隨著技術的革新,以及前端頁面複雜度的提升,對應有localStorage,eventBus,vuex,redux,mobx,rxjs等資料儲存和管理的方案,所以覺得研究狀態管理還是很有必要的。所以最近花了一些時間研究一下這方面的知識。在分析的過程當中可能有自己理解出偏差或者大家有理解不一樣的地方,歡迎大家評論或私信我。

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

  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之後,資料驅動檢視可以簡單地分為四個步驟:

  • 資料變化,生成新的Virtual DOM
  • 通過diff演算法比對新的Virtual DOM和舊的Virtual DOM的異同
  • 生成新舊物件的差異(patch)
  • 遍歷差異物件並更新DOM

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

元件間資料通訊和eventBus

資料可以簡單地分為兩個部分,跨元件的資料和元件內的資料。元件內的資料大多數是和UI相關的,比如說單選框是否被勾選,按鈕是否被點選。這些可以稱為元件內狀態資料。在react中,有一個概念叫做木偶元件,它裡邊儲存的資料就是元件內狀態資料。其實在市面上的很多UI元件庫如element,ant design提供的都是木偶元件。另外一種資料就是跨元件的資料,比如父元件喚起子元件關閉,一旦面臨著跨元件的互動,我們面臨的問題就開始變得複雜了。這時候就需要一個機制來處理父子和兄弟元件通訊。父元件對子元件就是props的傳遞,子元件對父元件react的處理方式就是父元件傳遞給子元件一個處理函式,由子元件呼叫,這樣資料就由函式引數傳給來父元件。vue的處理方式就是子元件通過$emit一個函式將資料由函式引數傳給父元件由父元件接收呼叫。

eventBus則為中央通訊,provide是一個物件或返回一個物件的函式。該物件包含可注入其子孫的屬性: 淺析前端狀態管理

inject 選項可以是:一個字串陣列,或一個物件。然後通過inject注入的值作為資料入口: 淺析前端狀態管理

但對於多個檢視需要依賴於統一狀態或者來自於不同檢視的行為需要變更同一狀態。單單依賴於元件間的通訊就顯得有些雞肋了。

單項資料流(vuex && redux)

下面用一張圖來分別介紹以下redux和react的資料流是怎樣的: 淺析前端狀態管理

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中文文件

淺談前端狀態管理(上)

前端狀態管理請三思

前端資料管理與前端框架選擇

相關文章