【原創】Redux 卍解

Redux 卍解

ReduxFlux設計模式的又一種實現形式。html

提及Flux,筆者以前,曾寫過一篇《ReFlux細說》的文章,重點對比講述了Flux的另外兩種實現形式:『Facebook Flux vs Reflux』,有興趣的同窗能夠一併看看。node

時過境遷,如今社區裏,Redux的風頭早已蓋過其餘Flux,它與React的組合使用更是你們所推薦的。react

Redux很火,很流行,並非沒有道理!!它自己靈感來源於Flux,但卻不侷限於Flux,它還帶來了一些新的概念和思想,集成了immutability的同時,也促成了Redux自身生態圈git

筆者在看完reduxreact-redux源碼後,以爲它的一些思想和原理拿出來聊一聊,會更有利於使用者的瞭解和使用Redux。github

:若是你是初學者,能夠先閱讀一下Redux中文文檔,瞭解Redux基礎知識。)express

數據流

做爲Flux的一種實現形式,Redux天然保持着數據流的單向性,用一張圖來形象說明的話,能夠是這樣:redux

redux-data-flow

上面這張圖,在展示單向數據流的同時,還爲咱們引出了幾個熟悉的模塊:Store、Actions、Action Creators、以及Views。設計模式

相信你們都不會陌生,由於它們就是Flux設計模式中所提到的幾個重要概念,在這裏,Redux沿用了它們,並在這基礎之上,又融入了兩個重要的新概念:ReducersMiddlewares(稍後會講到)。api


接下來,咱們先說說Redux在已有概念上的一些變化,以後再聊聊Redux帶來的幾個新概念。數組

Store

Store — 數據存儲中心,同時鏈接着Actions和Views(React Components)。

鏈接的意思大概就是:

  1. Store須要負責接收Views傳來的Action
  2. 而後,根據Action.type和Action.payload對Store裏的數據進行修改
  3. 最後,Store還須要通知Views,數據有改變,Views便去獲取最新的Store數據,經過setState進行從新渲染組件(re-render)。

上面這三步,實際上是Flux單向數據流所表達出來的思想,然而要實現這三步,纔是Redux真正要作的工做。

下面,咱們經過答疑的方式,來看看Redux是如何實現以上三步的?


問:Store如何接收來自Views的Action?

答:每個Store實例都擁有dispatch方法,Views只須要經過調用該方法,並傳入action對象做爲形參,Store天然就就能夠收到Action,就像這樣:

store.dispatch({
    type: 'INCREASE'
});

問:Store在接收到Action以後,須要根據Action.type和Action.payload修改存儲數據,那麼,這部分邏輯寫在哪裏,且怎麼將這部分邏輯傳遞給Store知道呢?

答:數據修改邏輯寫在Reducer(一個純函數)裏,Store實例在建立的時候,就會被傳遞這樣一個reducer做爲形參,這樣Store就能夠經過Reducer的返回值更新內部數據了,先看一個簡單的例子(具體的關於reducer咱們後面再講):

// 一個reducer
function counterReducer(state = 0, action) {
  switch (action.type) {
    case 'INCREASE':
      return state + 1;
    case 'DECREASE':
      return state - 1;
    default:
      return state;
  }
}

// 傳遞reducer做爲形參
let store = Redux.createStore(counterReducer);

問:Store經過Reducer修改好了內部數據以後,又是如何通知Views須要獲取最新的Store數據來更新的呢?

答:每個Store實例都提供一個subscribe方法,Views只須要調用該方法註冊一個回調(內含setState操做),以後在每次dispatch(action)時,該回調都會被觸發,從而實現從新渲染;對於最新的Store數據,能夠經過Store實例提供的另外一個方法getState來獲取,就像下面這樣:

let unsubscribe = store.subscribe(() =>
  console.log(store.getState())
);

因此,按照上面的一問一答,Redux.createStore()方法的內部實現大概就是下面這樣,返回一個包含上述幾個方法的對象:

function createStore(reducer, initialState, enhancer) {
  var currentReducer = reducer
  var currentState = initialState
  var listeners = []
  
  // 省略若干代碼
  //...
  
  // 經過reducer初始化數據
  dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer
  }
}

總結概括幾點:

  1. Store的數據修改,本質上是經過Reducer來完成的。
  2. Store只提供get方法(即getState),不提供set方法,因此數據的修改必定是經過dispatch(action)來完成,即:action -> reducers -> store
  3. Store除了存儲數據以外,還有着消息發佈/訂閱(pub/sub)的功能,也正是由於這個功能,它纔可以同時鏈接着Actions和Views。
    • dispatch方法 對應着 pub
    • subscribe方法 對應着 sub

Reducer

Reducer,這個名字來源於數組的一個函數 — reduce,它們倆比較類似的地方在於:接收一箇舊的prevState,返回一個新的nextState

在上文講解Store的時候,得知:Reducer是一個純函數,用來修改Store數據的

這種修改數據的方式,區別於其餘Flux,因此咱們疑惑:經過Reducer修改數據給咱們帶來了哪些好處?

這裏,我列出了兩點:

  1. 數據拆解
  2. 數據不可變(immutability)

數據拆解

Redux有一個原則:單一數據源,即:整個React Web應用,只有一個Store,存儲着全部的數據。

這個原則,其實也不難理解,假若多個Store存在,且Store之間存在數據關聯的狀況,處理起來每每會是一件比較頭疼的事情。

然而,單一Store存儲數據,就有可能面臨着另外一個問題:數據結構嵌套太深,數據訪問變得繁瑣,就像下面這樣:

let store = {
    a: 1,
    b: {
        c: true,
        d: {
            e: [2, 3]
        }
    }
};

// 增長一項: 4
store.b.d.e = [...store.b.d.e, 4]; // es7 spread

console.log(store.b.d.e); // [2, 3, 4]

這樣的store.b.d.e數據訪問和修改方式,對於剛接手的項目,或者不清楚數據結構的同窗,簡直是晴天霹靂!!

爲此,Redux提出經過定義多個reducer對數據進行拆解訪問或者修改,最終再經過combineReducers函數將零散的數據拼裝回去,將是一個不錯的選擇!

在JavaScript中,數據源其實就是一個object tree,object中的每個key均可以認爲是tree的一個節點,每個葉子節點都含有一個value(非plain object),就像下面這張圖所描述的:

redux-node

而咱們對數據的修改,其實就是對葉子節點value的修改,爲了不每次都從tree的根節點r開始訪問,能夠爲每個葉子節點建立一個reducer,並將該葉子節點的value直接傳遞給該reducer,就像下面這樣:

// state 就是store.b.d.e的值
// [2, 3]爲默認初始值
function eReducer(state = [2, 3], action) {
  switch (action.type) {
    case 'ADD':
      return [...state, 4]; // 修改store.b.d.e的值
    default:
      return state;
  }
}

如此,每個reducer都將直接對應數據源(store)的某一個字段(如:store.b.d.e),這樣的直接的修改方式會變得簡單不少。

拆解以後,數據就會變得零散,要想將修改後的數據再從新拼裝起來,並統一返回給store,首先要作的就是:將一個個reducer自上而下一級一級地合併起,最終獲得一個rootReducer

合併reducer時,須要用到Redux另外一個api:combineReducers,下面這段代碼,是對上述store的數據拆解:

import { combineReducers } from 'redux';

// 葉子reducer
function aReducer(state = 1, action) {/*...*/}
function cReducer(state = true, action) {/*...*/}
function eReducer(state = [2, 3], action) {/*...*/}

const dReducer = combineReducers({
  e: eReducer
});

const bReducer = combineReducers({
  c: cReducer,
  d: dReducer
});

// 根reducer
const rootReducer = combineReducers({
  a: aReducer,
  b: bReducer
});

這樣的話,rootReducer的返回值就是整個object tree。

總結一點:Redux經過一個個reducer完成了對整個數據源(object tree)的拆解訪問和修改。


數據不可變

React在利用組件(Component)構建Web應用時,其實無形中建立了兩棵樹:虛擬dom樹組件樹,就像下圖所描述的那樣(原圖):

react component tree

因此,針對這樣的樹狀結構,若是有數據更新,使得某些組件應該獲得從新渲染(re-render)的話,比較推薦的方式就是:自上而下渲染(top-down rendering),即頂層組件經過props傳遞新數據給子孫組件。

然而,每次須要更新的組件,可能就是那麼幾個,可是React並不知道,它依然會遍歷執行每一個組件的render方法,將返回的newVirtualDom和以前的prevVirtualDom進行diff比較,而後最後發現,計算結果極可能是:該組件所產生的真實dom無需改變!/(ㄒoㄒ)/~~(無用功緻使的浪費性能)

因此,爲了不這樣的性能浪費,每每咱們都會利用組件的生命週期函數shouldComponentUpdate進行判斷是否有必要進行對該組件進行更新(即,是否執行該組件render方法以及進行diff計算)?

就像這樣:

shouldComponentUpdate(nextProps) {
    if (nextProps.e !== this.props.e) { // 這裏的e是一個字段,多是對象引用,也多是數值,布爾值
      return true; // 須要更新
    }
    return false; // 無需更新
  }

但,每每這樣的比較,對於字面值還行,對於對象引用(object,array),就糟糕了,由於:

let prevProps = {
    e: [2, 3]
};

let nextProps = prevProps;

nextProps.e.push(4);

console.log(prevProps.e === nextProps.e); // 始終爲true

雖然你能夠經過deepEqual來解決這個問題,但對嵌套較深的結構,性能始終會是一個問題。

因此,最後對於對象引用的比較,就引出了不可變數據(immutable data)這個概念,大致的意思就是:一個數據被建立了,就不能夠被改變(mutation)

若是你想改變數據,就得從新建立一個新的數據(即新的引用),就像這樣:

let prevProps = {
    e: [2, 3]
};

let nextProps = {
  e:[...prevProps.e, 4] // es7 spread
};

console.log(prevProps.e === nextProps.e); // false

也許,你已經發現每一個Reducer函數在修改數據的時候,正是這樣作的,最後返回的都是一個新的引用,而不是直接修改引用的數據,就像這樣:

function eReducer(state = [2, 3], action) {
  switch (action.type) {
    case 'ADD':
      return [...state, 4]; // 並無直接地經過state.push(4),修改引用的數據
    default:
      return state;
  }
}

最後,由於combineReducers的存在,以前的那個object tree的總體數據結構就會發生變化,就像下面這樣:

redux-node-change

如今,你就能夠在shouldComponentUpdate函數中,肆無忌憚地比較對象引用了,由於數據若是變化了,比較的就會是兩個不一樣的對象!

總結一點:Redux經過一個個reducer實現了不可變數據(immutability)。

PS:固然,你也能夠經過使用第三方插件(庫)來實現immutable data,好比:React.addons.update、Immutable.js。(只不過在Redux中會顯得那麼沒有必要)。

Middleware

Middleware — 中間件,最初的思想毫無疑問來自:Express

中間件講究的是對數據的流式處理,比較優秀的特性是:鏈式組合,因爲每個中間件均可以是獨立的,所以能夠造成一個小的生態圈。

在Redux中,Middlerwares要處理的對象則是:Action

每一箇中間件能夠針對Action的特徵,能夠採起不一樣的操做,既能夠選擇傳遞給下一個中間件,如:next(action),也能夠選擇跳過某些中間件,如:dispatch(action),或者更直接了當的結束傳遞,如:return

標準的action應該是一個plain object,可是對於中間件而言,action還能夠是函數,也能夠是promise對象,或者一個帶有特殊含義字段的對象,但無論怎樣,由於中間件會對特定類型action作必定的轉換,因此最後傳給reducer的action必定是標準的plain object。

好比說:

  • [redux-thunk]裏的action能夠是一個函數,用來發起異步請求。
  • [redux-promise]裏的action能夠是一個promise對象,用來更優雅的進行異步操做。
  • [redux-logger]裏的action就是一個標準的plain object,用來記錄action和nextState的。
  • 一個自定義中間件:延遲action的執行,這裏就存在一個特殊字段:action.meta.delay,具體以下:
// 用 { meta: { delay: N } } 來讓 action 延遲 N 毫秒。
const timeoutScheduler = store => next => action => {
  if (!action.meta || !action.meta.delay) {
    return next(action)
  }

  let timeoutId = setTimeout(
    () => next(action),
    action.meta.delay
  )

  return function cancel() {
    clearTimeout(timeoutId)
  }
}

那麼問題來了,這麼多的中間件,如何使用呢?

先看一個簡單的例子:

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import createLogger from 'redux-logger';
import rootReducer from '../reducers';

// store擴展
const enhancer = applyMiddleware(
  thunk,
  createLogger()
);

const store = createStore(rootReducer, initialState, enhancer);

// 觸發action
store.dispatch({
    type: 'ADD',
    num: 4
});

注意:單純的Redux.createStore(...)建立的Store實例,在執行store.dispatch(action)的時候,是不會執行中間件的,只是單純的action分發。

要想給Store實例附加上執行中間件的能力,就必須改造createStore函數,最新版的Redux是經過傳入store擴展(store enhancer)來解決的,而具備中間件功能的store擴展,則須要使用applyMiddleware函數生成,就像下面這樣:

// store擴展
const enhancer = applyMiddleware(
  thunk,
  createLogger()
);

const store = createStore(rootReducer, initialState, enhancer);

上面的寫法是新版Redux纔有的,之前的寫法則是這樣的(新版兼容的哦):

// 舊寫法
const createStoreWithMiddleware = applyMiddleware(
  thunk,
  createLogger()
)(createStore);

const store = createStoreWithMiddleware(reducer, initialState)

至於改造後的createStore方法爲什麼擁有了執行中間件的能力,你們能夠看一下appapplyMiddleware的源碼。

最後,簡單用一張圖來驗證一句話的正確性:中間件提供的是位於 action 被髮起以後,到達 reducer 以前的擴展點

redux-middleware

react-redux

爲了讓Redux可以更好地與React配合使用,react-redux庫的引入就顯得必不可少。

react-redux主要暴露出兩個api:

  1. Provider組件
  2. connect方法

Provider

Provider存在的意義在於:想經過context的方式將惟一的數據源store傳遞給任意想訪問的子孫組件

好比,下面要說的connect方法在建立Container Component時,就須要經過這種方式獲得store,這裏就不展開說了。

不熟悉React context的同窗,能夠看看官方介紹


connect

Redux中的connect方法,跟Reflux.connect方法有點相似,最主要的目的就是:讓Component與Store進行關聯,即Store的數據變化能夠及時通知Views從新渲染。

下面這段源碼(來自connect.js),可以說明上述觀點:

trySubscribe() {
    if (shouldSubscribe && !this.unsubscribe) {
      // 跟store關聯,消息訂閱
      this.unsubscribe = this.store.subscribe(this.handleChange.bind(this))
      this.handleChange()
    }
}

handleChange() {
    if (!this.unsubscribe) {
      return
    }

    const prevStoreState = this.state.storeState
    const storeState = this.store.getState()

    if (!pure || prevStoreState !== storeState) {
      this.hasStoreStateChanged = true
      this.setState({ storeState }) // 組件從新渲染
    }
}

另外,connect方法,還引出了另外兩個概念,即:容器組件(Container Component)和展現組件(Presentational Component)。

感興趣的同窗,能夠看下這篇文章《Presentational and Container Components》,瞭解二者的區別,這裏就不展開討論了。

最後

以上就是筆者對Redux及其相關知識的理解,不對的地方歡迎留言交流,新浪微博

相關文章
相關標籤/搜索