前端技術 | 從Flux到Redux

上一篇分析了Flux出現的背景和原理,最核心的思想就是「組件化+單向數據流」。javascript

可是,Flux在設計上並不是完美,具體來講主要存在如下2個不足:前端

1. 多Store數據依賴

因爲Flux採用多Store設計,各個Store之間可能存在數據依賴。以flux-chat爲例:在這個聊天軟件裏,可能會有多我的給你發消息,好比Dave給你發了3條,Brian給你發了2條,當你點開某我的給你發的消息後,界面須要刷新,顯示你目前還有幾我的的未讀消息沒有查看:java

爲了解決這個需求,建立了3個Store:react

  • ThreadStore用來存儲消息組狀態
  • MessageStore用來存儲每一個組裏的消息的狀態
  • UnreadThreadStore用來計算目前還有幾個消息組沒有查看

當你點開某個消息組時,顯然你須要先更新ThreadStore和MessageStore,而後再更新UnreadThreadStore。因爲Store的註冊順序是不肯定的,爲了應付這種依賴,Flux提供了waitFor()機制,每一個Store在註冊以後都會生成一個令牌(dispatchToken),經過等待令牌的方式確保其餘Store被優先更新。數據庫

所以UnreadThreadStore的代碼會寫成下面這個樣子:編程

Dispatcher.waitFor([
  ThreadStore.dispatchToken,
  MessageStore.dispatchToken
]);

switch (action.type) {
  case ActionTypes.CLICK_THREAD:
    UnreadThreadStore.emitChange();
    break;
  ...
}
複製代碼

雖然能夠工做,可是總以爲不是很優雅,在一個Store中須要顯示地包含其餘Store的調用。固然你會說,乾脆把這3個Store的代碼糅到一塊兒,搞成一個Store不就好了?可是這樣又會致使代碼結構不夠清晰,不利於多模塊分工協做。redux

爲了兼顧這兩個方面,Redux使用全局惟一Store,外部可使用多個reducer來修改Store的不一樣部分,最後會把全部reducer的修改再組合成一個新的Store狀態。bash

2.狀態修改不是純函數

所謂純函數,是指輸出只和輸入相關,相同的輸入必定會獲得相同的輸出。用專業一點的術語來講,純函數沒有「反作用」。咱們先來看看Flux中是怎麼修改狀態的:網絡

Dispatcher.register(action => {
  switch(action.type) {
    case ActionTypes.CLICK_THREAD:
      _currentID = action.threadID;
      ThreadStore.emitChange();
      break;
    ...
}
複製代碼

能夠看到,是直接修改變量值,而後顯式發送一個change事件來通知View。app

咱們再來看看Redux中是怎麼修改狀態的:

export default function threadReducer(state = {}, action) {
  switch (action.type) {
    case ActionTypes.CLICK_THREAD: {
      return { ...state, _currentID: action.threadID };
    ...
}
複製代碼

細心的人可能已經看出來了,主要有3點區別:

  • 前面的函數裏只有一個action參數,而這裏多了一個state參數
  • 不是直接修改state中的字段,而是須要返回一個新的state對象
  • 不須要顯式發送事件通知View,實際上,Redux內部會檢測state對象的引用是否發生了變化,而後自動通知View進行刷新

那麼有人會說了,爲啥要這麼作,好像也沒看到啥好處嘛?固然是有好處的,這樣能夠支持「時間旅行調試(Time Travel Debugging)」。所謂時間旅行調試,指的是能夠支持狀態的無限undo / redo。因爲state對象是被總體替換的,若是想回到上一個狀態從新執行,那麼直接替換成上一步的state對象就能夠了。

3.什麼是Redux?

首先咱們要搞清楚,Redux解決了哪些問題?主要是如下3點:

1.如何在應用程序的整個生命週期內維持全部數據?

Redux是一個「狀態容器」。寫過React或者ReactNative的同窗可能會有感覺,若是多個頁面須要共享數據時,須要把數據一層層地傳遞下去,很是繁瑣。若是能有一個全局統一的地方存儲數據,當數據發生變化時自動通知View刷新界面,是否是很美好呢?所以,咱們須要一個「狀態容器」。

2.如何修改這些數據?

Redux借鑑了分佈式計算中的map-reduce的思想,把Store中的數據分割(map)成多個小的對象,經過純函數修改這些對象,最後再把全部的修改合併(reduce)成一個大的對象。修改數據的純函數被稱爲reducer

3.如何把數據變動傳播到整個應用程序?

經過訂閱(subscribe)。若是你的View須要跟隨數據的變化動態刷新,能夠調用subscribe()註冊回調函數。在這一點上,Redux是很是粗粒度的,每次只要有新的action被分發,你都會收到通知。顯然,你須要對通知進行過濾,這意味着你可能會寫不少重複代碼。不過,這也是出於通用性和靈活性考慮,實際上Redux不只能夠用於React,也能夠用在Vue.js或者Angular上。能夠搭配特定框架相關的適配層好比react-redux來規避這些重複代碼。

說了這麼多,咱們來看一下Redux的基本框架:

和前一篇的Flux框架圖對比一下能夠發現,Redux去除了dispatcher組件(由於只有一個Store),增長了recuder組件(用於更新Store的不一樣部分)。下面詳細介紹各個部分的做用。

4.Redux基本概念

4.1 Store

首先咱們須要建立一個全局惟一的Store,Redux提供了輔助函數createStore():

import { createStore } from 'redux'
var store = createStore(() => {})
複製代碼

你可能注意到了,createStore()須要提供一個參數,這個參數就是reducer。

4.2 Reducer

前面介紹過,reducer就是一個純函數,輸入參數是state和action,輸出新的state。通常的代碼模板以下:

var reducer = (state = {}, action) => {
	switch (action.type) {
	case 'MY_ACTION': 
    	return {...state, message: action.message}
    default:
    	return state
    }
}
複製代碼

須要注意的是,default分支必定要返回state,不然會致使狀態丟失。

好了,如今咱們有了reducer,能夠做爲參數傳遞給4.1節中的createStore()函數了。

createStore()只能接受一個reducer參數,若是咱們有多個reducer怎麼辦?這時須要使用另外一個輔助函數combineReducers():

import { combineReducers } from 'redux'
var reducer = combineReducers({
    first: firstReducer,
    second: secondReducer
})
複製代碼

combineReducers()會把多個reducer組合成一個,當有action過來時會依次調用每一個子reducer,因此實際上你能夠組織成一個樹狀結構。

4.3 Action

所謂action,其實就是一個普通的javascript對象,通常會包含一個type屬性用於標識類型,以及一個payload屬性用於傳遞參數(名字能夠隨便取):

var action = {
    type: 'MY_ACTION',
    payload: { message: 'hello' }
}
複製代碼

那麼如何發送action呢?store提供了一個dispatch()函數:

store.dispatch(action)
複製代碼

4.4 Action Creator

所謂action creator,其實就是一個用來構建action對象的函數:

var actionCreator = (message) => {
	return {
        type: 'MY_ACTION',
        payload: { message: message }
	}
}
複製代碼

因此4.3節發送action的代碼也能夠寫成這樣:

store.dispatch(actionCreator('hello'))
複製代碼

4.5 狀態讀取和訂閱

當你發送了一個action,reducer被調用並完成狀態修改,那麼前端視是怎麼感知到狀態變化的呢?咱們須要經過subscribe()進行訂閱:

store.subscribe(() => {
	let state = store.getState()
	... ...
})
複製代碼

store的getState()函數能夠得到當前狀態的一個副本,而後就能夠刷新界面了,以React爲例,能夠調用this.setState()或者this.forceUpdate()觸發從新渲染。

當視圖組件比較多時,每次都要寫這段訂閱代碼會比較繁瑣,後面會介紹經過react-redux來簡化這一過程。

4.6 Middleware

第3章的那張圖其實還少畫了個東西,叫作middleware(中間件)。那麼這個middleware是幹什麼用的呢?

在Web應用中常常會有異步調用,好比請求網絡、查詢數據庫什麼的。咱們首先發送一個action啓動異步任務,並但願在異步任務完成之後再更新狀態,應該如何實現呢?在Flux中,咱們能夠在dispatcher裏完成:首先啓動異步任務,而後在回調函數中再發送一個新的action去更新Store。可是Redux中去除了dispatcher的概念,你能調用的只有store的dispatch()函數而已,那咱們該怎麼辦呢?答案就是middleware。

因此,Redux的完整流程應參見下面這張動圖:

咱們先來看一個簡單的middleware的例子:

var thunkMiddleware = ({ dispatch, getState }) => {
    return (next) => {
        return (action) => {
            return typeof action === 'function' ?
                action(dispatch, getState) :
                next(action)
        }
    }
}
複製代碼

能夠發現,其實middleware就是一個三層嵌套的函數:

  • 第一層向其他兩層提供dispatch和 getState 函數
  • 第二層提供 next 函數,它容許你顯式的將處理過的輸入傳遞給下一個middleware或 reducer
  • 第三層提供從上一個中間件或從 dispatch 傳遞來的 action

因此,實際上middleware能夠理解在action進入reducer以前進行了一次攔截。在這個例子裏,若是action是一個函數,咱們就不會把action繼續傳遞下去,而是調用這個函數去執行異步任務。當異步任務執行完畢後,咱們能夠調用dispatch()函數發送一個新的action,用於調用reducer更新狀態。

那麼咱們如何註冊一箇中間件呢?Redux提供了一個工具函數applyMiddleware(),能夠直接做爲createStore()的一個參數傳遞進去:

const store = createStore(
  reducer,
  applyMiddleware(myMiddleware1, myMiddleware2)
)
複製代碼

預告一下,後面一篇要介紹的redux-saga,其實就是一個Redux中間件。

5.使用react-redux

Redux的設計主要考慮的是通用性和靈活性,若是想更好的配合React的組件化編程習慣,你可能須要react-redux。

Redux使用全局惟一的Store,另外當你須要發送action的時候,必須經過store的dispatch()函數。這對於一個有不少頁面的React應用來講,意味着只有兩種選擇:

  • 在全部頁面中import全局store對象
  • 經過props把store對象一層一層地傳遞下去

這顯然極其繁瑣,幸運的是,React提供了Context機制,說白了就是全部頁面都能訪問的一個上下文對象:

react-redux利用React的Context機制進行了封裝,提供了<Provider>組件和connect()函數來實現store對象的全局可訪問性。

5.1 <Provider>

這是一個React組件,使用時須要把它包裹在應用層根組件的外面,而後把全局store對象賦值給它的store屬性

import { Provider } from 'react-redux'
import store from './mystore'
export default class Application extends React.Component {
  render () {
    return (
      <Provider store={ store }>
        <Home />
      </Provider>
    )
  }
}
複製代碼

5.2 connect()

Provider組件只是把store對象放進了Context中,若是你須要訪問它,還須要一些額外的代碼,react-redux提供了一個connect()函數來幫你完成這些工做。

實際上,connect()就幫你作了兩件事:

  • 在你的組件外面包裝了<Context.Consumer>組件,獲取Context中的store對象
  • 根據你提供的selector函數,幫你把state中的值以及store.dispatch()函數映射到props中,這樣在代碼中你就能夠直接經過this.props.xxx進行訪問了

實現層面上,connect()採用了React的HOC(高階組件)技術,動態建立新組件及其實例:

那麼這個connect()怎麼用呢?咱們經過3個應用場景依次介紹。

1.你只是但願能在組件中使用dispatch()直接派發action

這是最簡單的狀況,你只須要在導出組件的時候加上connect()就能夠了:

export default connect()(MyComponent)
複製代碼

當你須要派發action的時候,能夠直接調用this.props.dispatch()。

2.你不想直接使用dispatch(),但願可以自動派發action

實際上你會發現,若是action不少的話,你須要不停地調用dispatch()函數。爲了使咱們的實現更加「聲明式」,最好是把派發邏輯封裝起來。實際上Redux中有一個輔助函數bindActionCreators()來完成這項工做,它會爲每一個action creator生成同名的函數,自動調用dispatch()函數:

const increment = () => ({ type: "INCREMENT" });
const decrement = () => ({ type: "DECREMENT" });
const boundActionCreators = bindActionCreators({ increment, decrement }, dispatch);
// 返回值:
// {
//   increment: (...args) => dispatch(increment(...args)),
//   decrement: (...args) => dispatch(decrement(...args)),
// }
複製代碼

這樣你就能夠直接調用boundActionCreators.increment()派發action了。那麼如何跟connect()聯繫起來呢?這裏須要用到它的第2個參數(第1個參數後面再介紹)mapDispatchToProps,舉個例子:

const mapDispatchToProps = (dispatch) => {
  return bindActionCreators({ increment, decrement }, dispatch);
}

export default connect(null, mapDispatchToProps)(MyComponent)
複製代碼

這樣,你就能夠在組件中直接調用this.props.increment()函數了。

你覺得這樣就結束了?還有更簡單的方法,連bindActionCreators()都不用寫!你能夠直接提供一個對象,包含全部的action creator就好了(這被稱爲「對象簡寫」方式):

const mapDispatchToProps = { increment, decrement }
export default connect(null, mapDispatchToProps)(MyComponent)
複製代碼

注意:若是你提供了mapDispatchToProps參數,那麼默認狀況下dispatch就不會再注入到props中了。若是你還想使用this.props.dispatch(),能夠在mapDispatchToProps的返回值對象中加上dispatch屬性。

3.你但願訪問store中的數據

這應該是使用最多的場景,組件訪問store中的數據並刷新界面。根據「無狀態組件」設計原則,咱們不該該直接訪問store,而須要經過一個「selector函數」把store中的數據映射的props中進行訪問,這個「selector函數」就是conntect()的第1個參數mapStateToProps。舉個例子:

const mapStateToProps = (state = {}, ownProps) => {
  return {
    xxx: state.xxx
  }
}

export default connect(mapStateToProps)(MyComponent)
複製代碼

這樣你在組件中就能夠經過this.props.xxx進行訪問了。另外,它還會幫你自動訂閱store,任什麼時候候store狀態數據發生變化,mapStateToProps都會被調用並致使界面從新渲染。除了第一個參數state以外,還有一個可選參數ownProps,若是你的組件須要用自身的props數據到store中檢索數據,能夠經過這個參數獲取。

固然,你能夠同時提供mapStateToProps和mapDispatchToProps參數,這樣你就能夠得到兩方面的功能:

export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)
複製代碼

最後,以一張思惟導圖結束本篇文章,下一篇介紹redux-saga。

相關文章
相關標籤/搜索