走近 Redux

1 Redux

Redux is a predictable state container for JavaScript apps

簡單來講,Redux是一個管理應用狀態的框架javascript

2 解決的問題

2.1 前端開發中的廣泛問題

前端開發中,本質的問題就是將 server -> client 的輸入,變成 client -> user 輸入;再將 user -> client 的輸入,變成 client -> server 的輸入。html

在 client 中,前端的角色其實大概能夠當作一個"轉換器"。前端

舉個簡單的例子,後端傳過來的是一個 json 格式的數據,這個 json 格式,實際上是在計算機範疇內的,真正的終端用戶並不知道什麼是json,更不知道要如何修改json,保存本身的信息。因此,這個時候就須要像上面說的把 json 轉換爲頁面上的內容和元素;另外,隨着用戶的一系列操做,數據須要隨時更新保存到服務端。整個的這個過程,可能會很複雜,數據和數據之間會存在聯動關係。java

這個時候,就須要有一個東西,從更高的層面來管理全部的這些狀態,因而有了 mvc,狀態保存在model,數據展現在viewcontroller來串聯用戶的輸入和數據的更新。可是這個時候就會有個問題,理想狀況下,咱們默認全部的狀態更新都是由用戶的操做(也能夠理解爲用戶的輸入)來觸發的,但實際狀況中,會觸發狀態更新的不只僅是單純的用戶操做,還有多是用戶操做帶來的後果,在舉個例子:react

頁面上有個異步獲取信息的按鈕,用戶能夠點擊這個按鈕,那麼用戶點擊這個按鈕以後,會發生:git

按鈕狀態變爲 pending --> 獲取成功,按鈕狀態變成 success
                   |
                   |--> 獲取失敗,按鈕狀態變成 error

這裏改變success/error狀態的並非用戶輸入,而是服務端的返回,這個時候,就須要在 controller裏面 handle 服務端的返回。這只是個簡單的例子,若是相似的狀況發生了不少以後,每次輸入和輸出將變得難以預測,難以預測的後果就是很容易出現 bug,程序的健壯性降低。github

讓每一步輸入和輸出可預測,可預測才能可測試,可測試才能保證健壯性。json

2.2 React 和 Flux

因而,這個時候出現了ReactFluxredux

Flux的核心思想就是維護一個單向數據流,數據的流向永遠是單向的,因此每一個步驟即是可預測的,程序的健壯性獲得了保證。後端

Reactjsx 能夠將前端的 UI 部分變成了一層層套用的方法,再舉個例子,以前寫 html 是這樣的

<div>
    <span>foo</span>
</div>

若是狀態改變以後,大部分狀況下咱們是將某個片斷的 html 用改變的狀態從新拼一遍,而後替換到原有的 dom 結構裏。

可是,用了 jsx 以後,你的代碼將變成這樣:

div(span('foo'))

變成了一個函數,這個函數的輸出就是上面的那段 html,因此整個 UI 變成了一個可輸入輸出的結構,有了輸入和輸出,就是一個完整的可預測的結構了,可預測,也就是表明可測試了。

2.3 使用 Redux

在使用Flux的過程裏,當應用的結構變得複雜以後,會顯得力不從心,雖然數據流仍是單向,可是Flux的總體流程有兩個比較關鍵的點:

  1. store 更新完數據以後,須要emit
  2. component 中須要 handle emit

當數據結構和輸入輸出變得複雜的時候,每每會定義不少個 store,可是每每 store 之間仍是會有依賴和關聯。

這個時候,handle 的過程會變得很臃腫,難以理解。

而後,Redux就出場了。

Flux的思路能夠理解爲多個store組成了一個完整的 App;Redux的思路則是一個完整的store對應一個完整的 App。

Redux相比Flux,多抽象出了一個reducer的概念。這個reducer只負責狀態的更新,而且會返回一個新的狀態對象,整個 App 從結構上看起來,沒有一個一直保存/更新的狀態(使用Flux每一個store都是一直保存住的,而後在此基礎上進行更新),Redux中的數據更像是一個流程。

另外,還有一點比較重要的是,由於沒有了一個一直保存/更新的狀態對象,因此在 component 中的 handle 也就沒有意義了,經過react-redux能夠徹底實現一個順暢的數據流。

這裏舉個簡單的例子,若是咱們更新一個訂單,訂單裏有這麼幾項:

  • 地址
  • 運費
  • 商品數量
  • 總價

其中地址影響運費,運費影響總價;另外,商品數量也會影響總價

使用Flux的話,咱們一般會分解成這樣幾個store:

  • address
  • items
  • deal

其中 addressitems的更新會觸發deal.amount的更新,完整的交易信息會同步到deal中。

component裏,咱們會handel全部這些storeemit,而後再進行setState以更新 UI 部分。

使用Redux的話,咱們會分解成這樣幾個reducer:

  • address
  • items
  • deal

其中address只負責address的更新,item只負責items的更新,deal會響應addressitem中跟交易相關的更新,實現改變價格和訂單地址的操做。

可是並不須要在component中再hanle每一個部分更新以後的emit。數據更新了,頁面就會本身變化。

接下來,咱們看看Redux是如何實現的。

3 實現原理

查看Reduxgithub,會發現Redux的代碼異常的精簡,僅僅包含了這幾個部分:

  • utils/
  • applyMiddlewares.js
  • bindActionCreators.js
  • combineReducers.js
  • compose.js
  • createStore.js
  • index

其中的utils/index.js咱們並不須要關心,只要看接下來的幾部分就能夠。

另外,由於咱們的大部分場景仍是搭配React來使用Redux,因此這裏咱們順便搭配 react-redux來看下

react-redux/src

react-redux中,咱們關心的更少,只有:

  • Provider.js
  • connect.js

這兩部分而已。

3.1 一個真實世界中的例子

拿一個真正的實例來看,咱們要作一個簡單的訂單,目錄結構是這樣的:

|- dealReducer.js
|- dealActions.js
|- dealStore.js
|- dealApp.js
|- main.js

3.1.1 main.js

先看代碼:

import React from 'react'
import ReactDom from 'react-dom'
import { Provider } from 'react-redux'
import configureStore from './dealStore'
import DealApp from './dealApp'

let store = configureStore()

ReactDom.render(
  (
    <Provider store={ store }>
      <DealApp  />
    </Provider>
  ), document.getElementById('app'))

這個部分比較簡單,首先是調用了dealStore中的方法,生成了一個store,而後調用了react-redux中的Provider把這個store綁定到了Provider上。

咱們先看 Provider 的代碼:

3.1.2 react-redux.provider

完整代碼看這裏

咱們只看下核心的部分:

export default class Provider extends Component {
  getChildContext() {
    return { store: this.store }
  }

  constructor(props, context) {
    super(props, context)
    this.store = props.store
  }

  render() {
    return Children.only(this.props.children)
  }
}

其實最核心就是getChildContext方法,這個方法在每次propsstate被調用時會被觸發,這裏更新了store

3.1.3 dealApp.js

仍是先看代碼:

import React, { Component, PropTypes } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import * as dealActions from 'deal/actions/dealActions'
import * as addressActions from 'deal/actions/addressActions'

class DealApp extends Component {
    // some code
}

function mapStateToProps(state) {
  return {
    'deal': state.dealReducer,
    'address': state.addressReducer,
  }
}

function mapDispatchToProps(dispatch) {
  return {
    'dealActions': bindActionCreators(dealActions, dispatch),
    'addressActions': bindActionCreators(addressActions, dispatch),
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(DealApp)

從代碼能夠看到,比通常的 react component 多了對connect的調用,以及mapStateToPropsmapDispatchToProps兩個方法。

因此,接下來看下這個connect是什麼

3.1.3 react-redux.connect

完整代碼見

來看下核心部分的代碼:

// some code
    componentDidMount() {
        this.trySubscribe()
    },
    
    trySubscribe() {
        if (shouldSubscribe && !this.unsubscribe) {
            this.unsubscribe = this.store.subscribe(this.handleChange.bind(this))
          this.handleChange()
        }
      },
    
    handleChange() {
        if (!this.unsubscribe) {
            return
        }

        const storeState = this.store.getState()
        const prevStoreState = this.state.storeState
        if (pure && prevStoreState === storeState) {
            return
        }

        if (pure && !this.doStatePropsDependOnOwnProps) {
            const haveStatePropsChanged = tryCatch(this.updateStatePropsIfNeeded, this)
          if (!haveStatePropsChanged) {
            return
          }
          if (haveStatePropsChanged === errorObject) {
            this.statePropsPrecalculationError = errorObject.value
          }
          this.haveStatePropsBeenPrecalculated = true
        }

        this.hasStoreStateChanged = true
        this.setState({ storeState })
      }

能夠看到,這幾個方法用到了store中的getStatesubscribe這幾個方法。而且在handleChange中,實現了在Flux中須要人肉實現的setState方法。

3.1.4 dealStore.js

既然在上面的connect中,用到了store,那麼就來看看dealStore的內容:

import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import dealReducers from 'deal/reducers/dealReducer'

let creator = compose(
    applyMiddleware(thunk),
    applyMiddleware(address),
  )(createStore)

export default function configureStore(initState) {
  const store = creator(dealReducers, initState)
  return store
}

這個文件裏用到了redux中的createStore , composeapplyMiddleware方法。
經過調用能夠看到,先是經過applyMiddleware方法調用了一些middleware,而後再用compose將對middleware的調用串聯起來,返回一個方法,先簡單列爲f(createStore),而後這個調用再次返回了一個方法,這裏被定義爲creator。經過調用creator方法,最終生成了 store

下面逐個看一下createStore,compose,applyMiddleware這幾個方法。

3.1.5 applyMiddleware

直接看源碼:

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => {
    var store = createStore(reducer, preloadedState, enhancer)
    var dispatch = store.dispatch
    var chain = []

    var middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

這裏直接返回了一個接收createStore做爲參數的方法,這個方法中會遍歷傳入的middleware,並使用compose 調用store.dispatch,接下來看一下compose方法的具體實現。

3.1.6 compose

仍是直接貼源碼:

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  const last = funcs[funcs.length - 1]
  const rest = funcs.slice(0, -1)
  return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
}

能夠看到 compose的源碼十分精簡,整個compose的做用就是傳入一串funcs,而後返回一個方法,先暫定這個方法名爲cc將傳入的funcs按照從右到左的順序,逐個執行c傳入的參數。

爲何要按照從右到左的順序執行,咱們先按下不表,接下來看 createStore 的源碼。

3.1.7 createStore

createStore的源碼比較長,這裏就不貼了,詳情能夠見這裏

咱們這裏只看下這個方法的輸入和輸出既可:

export default function createStore(reducer, preloadedState, enhancer) {

    // code
    
    return {
           dispatch,
           subscribe,
           getState,
           replaceReducer,
          [$$observable]: observable
  }
}

輸入有三個,reducerpreloadState咱們都屬性,可是這個enhancer是什麼呢?

再來看下相關代碼:

if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    return enhancer(createStore)(reducer, preloadedState)
}

enhancer能夠當作是預先設定的,對createStore返回對象執行的方法,好比能夠給返回的對象添加一些新的屬性或者方法之類的操做,就能夠放到enhancer中作。

看到這裏,咱們再來看下compose中爲何調用reducerRight,將方法從右至左執行。

首先,是applyMiddleware方法獲取到傳入的createStore,返回了:

{
    ...store,
    dispatch
}

可是這裏的dispatch已經不是creatStore中返回的store.dispatch了。這個dispatch是經過調用composestore.dispatch傳入middlewares中執行的結果。

再回到主線上來,applyMiddleware返回了一個加強store,若是有多個applyMiddleware的調用,以下所示:

compose(
    applyMiddleware(A),
    applyMiddleware(B),
    applyMiddleware(C)
)

咱們的指望的執行順序固然是A,B,C這樣,因此轉換成方法的話,應該是這樣

C(B(A()))

使用reducerRight的話,最早被調用的方法(也就是上面的C)就會是執行鏈的最外層的方法,因此要按照從右到左的順序執行。

至此,Redux的介紹就先到這裏,以後會再寫一些關於Redux周邊組件的使用。

相關文章
相關標籤/搜索