11. Redux
Redux 中文文檔 Join the chat at https://gitter.im/camsong/redux-in-chinese redux中文官方文檔,寫的真的很不錯,贊一個
Redux簡介
Redux可以被看作 Flux 的一種實現嗎? 是,也可以說 不是。
(別擔心,它得到了Flux 作者的認可,如果你想確認。)Redux 的靈感來源於 Flux 的幾個重要特性。和 Flux 一樣,Redux 規定,將模型的更新邏輯全部集中於一個特定的層(Flux 里的 store,Redux 里的 reducer)。Flux 和 Redux 都不允許程序直接修改數據,而是用一個叫作 「action」 的普通對象來對更改進行描述。而不同於 Flux ,Redux 並沒有 dispatcher 的概念。原因是它依賴純函數來替代事件處理器。純函數構建簡單,也不需額外的實體來管理它們。你可以將這點看作這兩個框架的差異或細節實現,取決於你怎麼看 Flux。Flux 常常被表述為 (state, action) => state 。從這個意義上說,Redux 無疑是 Flux 架構的實現,且得益於純函數而更為簡單。和 Flux 的另一個重要區別,是 Redux 設想你永遠不會變動你的數據。你可以很好地使用普通對象和數組來管理 state ,而不是在多個 reducer 里變動數據。正確且簡便的方式是,你應該在 reducer 中返回一個新對象來更新 state, 同時配合 object spread 運算符提案 或一些庫,如 Immutable。雖然出於性能方面的考慮,寫不純的 reducer 來變動數據在技術上是可行的,但我們並不鼓勵這麼做。不純的 reducer 會使一些開發特性,如時間旅行、記錄/回放或熱載入不可實現。此外,在大部分實際應用中,這種數據不可變動的特性並不會帶來性能問題,就像 Om 所表現的,即使對象分配失敗,仍可以防止昂貴的重渲染和重計算。而得益於 reducer 的純度,應用內的變化更是一目了然。
上面的話摘自react中文文檔。從文檔中,我們可以抽取出兩點:
- redux可以被看作是flux的實現
- redux包含了三個原則
flux是什麼?
react崇尚的是單項數據流,而flux抽象了一個對於react的單向數據流進行管理的框架。
flux包含三個部分:store, action,dispatcher。
- store存著所有的數據state;
- store會被載入到view,即整個react程序render的最外層的那個標籤,裡層的標籤可以從外部標籤中獲取它想要的部分或者全部的數據;一層層單項流入;
- store中的數據通過action觸發dispatch函數來進行改變。dispatcher改變store數據後,就會觸發setState函數(redux衍生的react-redux這個類庫來調用setState函數),來進行頁面的render;
- 我們可以通過頁面交互,點按鈕等,來觸發action;
我們可以用下面兩個公式來更好的對flux進行描述:
1. action->dispatcher->setState->render2. dispatcher(action, state(in store)) => state
redux的三個原則
Redux 可以用這三個基本原則來描述:
1. 單一數據源
整個應用的 state 被儲存在一棵 object tree 中,並且這個 object tree 只存在於唯一一個 store 中。
相當於flux中的store。相對於flux可以有多個store,redux中只有一個統一管理state的store
2. State 是只讀的
惟一改變 state 的方法就是觸發 action,action 是一個用於描述已發生事件的普通對象。
redux可以用下面的公式來更好的描述,可以對比flux的那個:
action->dispatch(action)->reducer->return newState->render
redux是一個可以獨立的類庫,即不僅可以被react用,還可以被其他前端類庫使用。
上面公式中return的newState會被react-redux進行setState(高階組件中),從而進行re-render
3. 使用純函數來執行修改
為了描述 action 如何改變 state tree ,你需要編寫 reducers
三個原則也顯示了Redux有三個部分組成:
1. store
包含state和dispatch。整個應用的 state 被儲存在一棵 object tree 中。dispatch用來觸發action操作,然後這個action會由reducer處理。action和reducer,它們通過actionType進行關聯。
2. action
由actionCreator創建/return一個action,action包含type和payload。type欄位,即actionType用來查找reducer,而payload是數據欄位,被reducer用來進行生成新的state(store中的state的一部分)。
3. reducer
通過action中的type來找到對應的處理函數reducer,reducer對action中的payload進行處理後會return一個新的state。然後react-redux根據新的newState來進行setState,從而觸發頁面重新渲染
我們可以總結一下redux的構成。redux(應該是用觀察者模式實現,類似於瀏覽器的事件機制)包含:
1. store,裡面包含了兩個部分
a. state
b. dispatch
2. action
a. 它是由actionCreator產生的
b. 它包含一個type欄位和一個payload欄位
c. 它通過dispatch觸發
3. reducer
a. action的處理函數
b. 通過action的type欄位來找到對應的reducer
c. 它會產生一個新的state
d. 交給react-redux實現的高階組件,來進行setState,這樣的話,就觸發了render
高階組件
redux中還需要講到的一個概念就是高階組件。
什麼是高階組件
Higher-Order Components (HOCs) are JavaScript functions which add functionality to existing component classes.
翻譯:高階組件就是js函數,這些函數會給現存的組件添加功能(返回一個新的組件)。
首先高階組件是一個函數,這可能和組件的名字有些不符。它接受一些函數和一個組件,然後把這些函數的功能加到這個組件中,最後返回一個新的組件。
也就是說它的形式可能如下:
const HOC = (...funcs) =>(EC) =>(NC)
ECC為existing component, NC為new component,當然一般會用composed component這個名詞
react-redux中的connect就是一個高階組件或者說一個返回高階組件的函數。讓我們來模仿connect。
首先定義一個「低階」組件,一般稱為View。
import React from react;class Chapter11View extends React.Component { render() { const { actions, chapter11 } = this.props; // here, chapter11 belongs to props; return ( <div> Redux is good </div> ); }}Chapter11View.propTypes = { chapter11: PropTypes.object.isRequired, actions: PropTypes.object.isRequired}export default Chapter11View
然後定義一個connect函數,用來返回一個高階組件,一般為container。
const connect = function(mapStateToProps, mapDispatchToProps) { const actions = mapDispatchToProps(); const chapter11 = mapStateToProps(); const props = { actions, chapter11 // here, chapter 11 is the state getting from store } return (Chapter11View) => <Chapter11View { ...props } />}
Redux vs Relay
項目組剛開始的時候,並沒有用到Redux,後來為了更好的管理state和props,同時更好的管理和處理非同步api,有了引入Redux的想法。
我個人學習Redux花了相當長的時間,其中也有各種原因:
- 人比較笨,苦笑
- 英語文檔看的實在是頭疼
- 一直沒有上手操作,一直在做紙上談兵(現在已經有很多開源項目,從它們可以直接開始上手redux)。
對於Relay:看過一陣子的Relay,個人覺得更加複雜,而且一般要結合graphql(相對於restful的一些取後台數據的方式,這個還是很好用的)的使用,這樣使得學習曲線更長。當然我個人還是在不斷學習這個知識棧,畢竟facebook出品的多是經典。
網上有很多兩者對比的文檔,有興趣的讀者可以去參考參考。而對於我們的文章來說,會繼續介紹redux在react的使用。
redux與react的結合
npm install --save reduxnpm install --save react-reduxnpm install --save react-router-reduxnpm install --save redux-thunk
一條命令搞定三個模塊的安裝
npm install redux react-redux react-router-redux redux-thunk --save
可選組件,一般開發可能需要用到,但是本書為了在講全知識點的同時盡量減少引入的模塊,所以並沒有在實例中引入這個模塊。
npm install --save-dev redux-devtools
安裝完模塊後,讓我們修改代碼。
在root.jsx中加入Redux相關的
import React from react;import { Router, Route, IndexRoute, hashHistory } from react-router;import { syncHistoryWithStore } from react-router-redux;import { Provider } from react-reduximport configureStore from ./redux/configureStore;import routers from ./components;import App from ./app.jsx;const store = configureStore(hashHistory, window.__INITIAL_STATE__);const history = syncHistoryWithStore(hashHistory, store);export default class Root extends React.Component { render() { return ( <Provider store={store}> <Router history={hashHistory}> <Route path="/" component={App} > <IndexRoute component={routers[0].component} /> {routers.map((route, i)=><Route key={i} path={route.key} component={route.component} />)} </Route> </Router> </Provider> ) }}
單項數據流, Provider里的數據,傳向Router組件,router組件又把它的數據往route裡面傳遞,一層一層,形成一個單項的數據流。
接著,讓我們新建一個文件夾用來存放Redux需要用到的store,actions和reducers新建三個文件
1. configureStore.js
import { applyMiddleware, createStore } from redux;import thunkMiddleware from redux-thunk;import { routerMiddleware } from react-router-redux;import rootReducer from ./rootReducer;export default function configureStore(history, initialState = {}) { const store = createStore( rootReducer, initialState, applyMiddleware(thunkMiddleware, routerMiddleware(history)) ); if (module.hot) { module.hot.accept(./rootReducer, () => { const nextRootReducer = require(./rootReducer).default; store.replaceReducer(nextRootReducer); }); } return store;}
我們會從之後的每一個reducer文件中看到,它們返回的其實都是state,那麼這個createStore函數,也就是把所有的reducer用到/返回的state放在了一個統一的store裡面了。
2. rootReducer.js
import { combineReducers } from redux;import { routerReducer } from react-router-redux;import { loadingBarReducer } from react-redux-loading-bar;import chapter11Reducer from ./chapter11;export default combineReducers({ chapter11: chapter11Reducer, routing: routerReducer, //for routing loadingBar: loadingBarReducer,});
combineReducers傳入的對象字面量的key會變成store中的state的key
3. chapter11.js
這個文件是為本文的例子中用到的高階組件建立action和reducer。
// ------------------------------------// Constants// ------------------------------------// ------------------------------------// Actions// ------------------------------------export const actions = {}// ------------------------------------// Action Handlers// ------------------------------------const ACTION_HANDLERS = {}// ------------------------------------// Reducer// ------------------------------------const initialState = {}export default function chapter13Reducer (state = initialState, action) { const handler = ACTION_HANDLERS[action.type] return handler ? handler(state, action) : state}
默認導出的reducers函數正好反應了一個公式:
state+action => state
在完成redux的三個基本文件的配置之後,我們可以開始寫containter了。
兩種方式
connect(...funcs)(View)的寫法
import { connect } from react-reduximport { bindActionCreators } from redux;import { actions } from ../../redux/chapter11import Chapter11View from ./Chapter11Viewconst mapDispatchToProps = (dispatch) => ({ actions: bindActionCreators(actions, dispatch)});const mapStateToProps = (state) => ({ chapter13 : state.chapter11,})export default connect(mapStateToProps, mapDispatchToProps)(Chapter11View)
利用ES7的@decorator
import React, {PropTypes, Component}from react;import {connect} from react-redux;import {bindActionCreators} from redux;import { actions } from ../../redux/chapter11@connect(state => ({chapter11: state.chapter11}), dispatch => bindActionCreators(actions, dispatch))export default class Chapter11View extends Component { // code for Chapter11View}```
對於es7的這個@decorator,其實用高階組件的定義剛好能闡釋。裝飾,給一個「低階組件」一點裝飾(功能),讓它返回一個新的組件。
angular2中也用了很多@特性。
這兩種寫法都可以。一般來說,前者需要兩個文件,分別定義view和container。後者只需要一個文件。
簡單的redux操作
接下來讓我們來寫一套簡單的redux操作(同步redux操作)。
在src/redux/chapter11.js中
1. 設置初始值,有時候初始值不是必須的,但是為了防止不必要的error,我們最好為每一個用到的變數都設置一個初始值
const initialState = { name: ,}
我們設置了一個name,它的初始值為空。
2. 寫一個常量,即actionType,用來聯繫actionCreator和reducer
// ------------------------------------// Constants// ------------------------------------export const ADD_NAME = ADD_NAME;
3. 寫一個actionCreator
// ------------------------------------// Actions// ------------------------------------function addName (name) { return { type: ADD_NAME, payload: { name } }}
4. 寫一個reducer
// ------------------------------------// Action Handlers// ------------------------------------const ACTION_HANDLERS = { [ADD_NAME]: (state, action) => (Object.assign({}, state, name: action.payload.name)),}
上面那個Object.assign還可以用下面的形式,其中用到了Spread Operator特性。
const ACTION_HANDLERS = { [ADD_NAME]: (state, action) => ({ ...state, ...action.payload }),}
5. 在view中使用action
這個action的功能就是點擊button,給name變數一個值
import React, { PropTypes, Component } from react;class Chapter11View extends Component { render() { const { actions, chapter11 } = this.props; const { name } = chapter11; return ( <div> My name is: {name} <br /> <button onClick={()=>actions.addName(ProfLiu)} >SHOW MY NAME</button> </div> ); }}Chapter11View.propTypes = { chapter11: PropTypes.object.isRequired, actions: PropTypes.object.isRequired}export default Chapter11View
在實際開發過程中,一般我們先是先頁面的,因此,可能第5步,在頁面中調用actionCreator最先進行。
處理非同步redux操作
1. 設置初始值,這裡我們使用之前的欄位name
2. 寫一個常量actionType,用來聯繫action和reducer
// ------------------------------------// Constants// ------------------------------------export const ADD_NAME = ADD_NAME;export const ASYNC_ADD_NAME = ASYNC_ADD_NAME;
3. 寫非同步action
這個非同步操作,會清除原來的name欄位,然後再5s後設置一個新的name值
// ------------------------------------// Actions// ------------------------------------const asyncAddName = (name) => { return (dispatch, getState) => { // init operation dispatch(addName()); setTimeout(()=>{ // async operation dispatch(asyncNameAdded(`Async ${name}`)); }, 5000) }}```
注意這裡的(dispatch, getState) => {}的寫法,這個是redux注入的兩個函數。第一個dispatch幫助我們來觸發actionCreator,getState函數則可以或者store中的state。
對於非同步action,我們需要處理非同步返回的操作,一般我們還需要一個同步action來處理非同步返回(比如處理從非同步的網路請求中得到的數據)。
// ------------------------------------// Actions// ------------------------------------function asyncNameAdded (name) { return { type: ASYNC_NAME_ADDED, payload: { name } }}
4. 寫一個reducer
```javascript// ------------------------------------// Action Handlers// ------------------------------------const ACTION_HANDLERS = { [ADD_NAME]: (state, action) => (Object.assign({}, state, name: action.payloa.name)), [ASYNC_NAME_ADDED]: (state, action) => (Object.assign({}, state, { name: action.payload.name })),}```
5. 在view中加入
<button onClick={()=>actions.asyncAddName(Lina)}>async show name</button>
這樣我們就完成了非同步redux的操作。
兩個redux操作的效果
讓我們進入第11章的頁面,我們看到有兩個按鈕和一個lable,其中lable的冒號後面是空的。
當我們點擊「SHOW MY NAME」按鈕是,My name is:後面馬上出現了一個值(同步redux操作)。
當我們點擊「SHOW MY NAME ASYNC」按鈕時,My name is:後面的值馬上被清空了(同步redux操作),然後過了5s後,出現了新的值(非同步redux操作)。
ES7的Spread Operator
這個特性用的是"...",和es6的rest parameter一樣的符號。
rest paramete主要用在解構的時候,或者剩餘的所有key-value或者元素,然後那個變數就是一個新的對象或者數組。
而spread operator用來在生成新的對象或數組,為老的對象或數組快速的添加新元素。
要使用ES6 rest parameter和ES7 spread operator,我們需要引入新的模塊es2015的stage-0或es2017。這裡我們以stage-0為例。
es2015有好幾個stage,跟的數字越小,包含的功能越多。stage-0包含了stage-1、stage-2。。。的所有功能
如何使用
安裝他們
然後在webpack的配置文件中加入一個處理js或者jsx文件的loader,裡面的presets欄位對應的數組需要加入stage-0。
{test: /.(js|jsx)$/,exclude: /node_modules/,loader: babel,query: {cacheDirectory: true,plugins: [transform-runtime,transform-decorators-legacy],presets: [es2015, react, stage-0]}}
這樣,我們就可以在我們的程序中使用spread operator特性了。而且使用了stage-0之後,基本的一些新語法都支持了。
推薦閱讀:
※揭秘Redux(1): 自動機
※Flux架構模式
※Immutable.js與React,Redux及reselect的實踐
※redux 中的 state 樹太大會不會有性能問題?
※React+AntD後台管理系統解決方案(補)