這個是很久以前寫的,一直忘記粘過來,裏面有一些是寫做格式是我本身定義的,因此和segmentfault的markdown語法有出入,圖片也不能加載,因此原文效果能夠在原網站上瀏覽,敬請賜教。javascript
<------------
文章原網址
------------>html
Redux is a predictable state container for JavaScript apps1.vue
在過去的十幾年裏web page一直都以指數遞增的方式發展,不管是概念上仍是編程上想要完全讀懂這門設計的藝術已經變得不可爲,現在在許多大型網站中的一個頁面從DOM
與Event
的微觀角度每每彙集着一系列複雜並瑣碎的行爲功能2,它們聚合在一塊兒構成了咱們今天能夠在瀏覽器端可操做的視圖,正是如此,怎樣管理這些行爲功能被提上日程,諸多才華橫溢的工程師們引伸出狀態管理的概念,製做出許多優秀的做品,如Redux、flux、flummox、mobxjs、refluxjs、martyjs、javascript-state-machine、vuex等,其中又以redux
和flux
最爲流行。java
Redux誕生的出發點是做爲一個javascript應用狀態(state)容器,借鑑flux的數據單向流動、elm的The Elm Architecture、函數式編程、柯里化(Currying)函數、組合模式(Composite pattern)的等思想。將視圖(view)中可操做的這些行爲類比爲動做(action),每一個動做傳遞都附帶狀態(state)信息,狀態引導動做對redux狀態容器store更新進而對視圖更新,store對狀態(state)進行同一管理,能夠說store可預測狀態容器是redux的骨架也不爲過。node
不管什麼框架都會設定一些屬於它的規則,規則恆定,附者雲起進而造成生態,react如是redux也是如此,在redux中因此規定了三條原則(Three Principles),即「Single source of truth」、「State is read-only」和「Changes are made with pure functions」,用於描述在redux整個生命週期內怎樣去管理和維護store樹。react
Single source of truth:惟一數據源。State被存儲在一顆惟一的object tree
上,即store對象樹。git
State is read-only:State只讀。在每一個組件(Component)或者reducer等內部,State樹內全部key->value
只讀。github
Changes are made with pure functions:這裏的純函數(pure functions)特指reducer函數。State樹內的state只能依靠純函數reducer對store進行更新。web
姑且先講reducer函數怎樣更新store放下,先討論store變動爲何會引發state變化。這裏就要引伸到單向數據流(Unidirectional data flow)的理論,單向數據流動即從模型到視圖的數據流動,它區別於雙向數據綁定
的方式,用react中的術語解釋的話就是,當某個組件的數據prop
須要變化而且經過相關方法操做更新store對象樹內某個碎片state以後,redux會返回一個新的store會從父節點傳遞到子節點,依次向下遍歷整棵組件樹,以組件爲單位尋找使用了變化的prop
的組件進行渲染。vuex
假定一個react渲染的頁面,黃色部分表明頁面DOM
結構樹,藍色是各個組件,組件之間的包含關係爲A ⊇ {B, C} && C ⊇ D
,其中B
和C
子組件都引用了store樹上的props.test
屬性,這是一個很是典型的從上到下單向流動的階梯式模型。
如今發生變化,當在B
組件內某個操做(UI交互、API調用等)更新了state值(即props.test
屬性值),這個操做自己並不會對B
組件的視圖和渲染進行干擾和操做,可是B
組件和D
組件會在store樹內的相應state值變化後觸發組件使view發生改變。若是是在傳統頁面中,這是事件和DOM結構之間的一對一,在數據雙向綁定概念中是事件與DOM結構的多對多,在react開發中應該是事件與VDOM一對一,可是在redux接管數據源後就變成了事件與VDOM之間沒有直接關係,VDOM的渲染間接由store對象樹決定。
紫色線條是數據流向,紫色方框是觸發渲染的子組件,數據從頂層store開始向下流淌,store頂層數據發生改變後會分別觸發存在props.test
屬性的子組件進行從新渲染更新DOM
,以達到視圖渲染可控、狀態重現可控的目的。
<!--
-->
Store做爲狀態容器、惟一數據源,由一個createStore(reducer, preloadedState, enhancer)
函數建立,在項目部署過程當中使用createStore函數通常會在項目根目錄下單獨列一個文件。
<pre class="pre-no-border">
.
├── bin
│ └── ...
├── ...
├── src
│ ├── components
│ │ └── ...
│ ├── containers
│ │ └── ...
│ ├── routes
│ │ └── ...
│ ├── store
│ │ ├── createStore.js
│ │ └── reducers.js
│ └── utils
│ └── ...
└── ...
</pre>
import {createStore} from 'redux'; import makeRootReducer from './reducers'; ... export default (initialState = {}, history) => { const store = createStore( makeRootReducer(), initialState, ); return store }
在__createStore.js__文件中createStore函數傳入了__makeRootReducer__和__initialState__兩個值,其中makeRootReducer就是一般所說的root reducer,只不過本文所示例的reducer皆爲異步加載,因此可能和其它文章寫的root reducer方式不同,詳細的內容下文會有敘述。閱讀redux源碼的createStore函數能夠看到最後返回四個核心對象和一個symbol對象,也就是說在示例中makeRootReducer()=reducer
、initialState=preloadedState
。
import $$observable from 'symbol'; export default function createStore(reducer, preloadedState, enhancer) { ... return { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable } }
方法 | 使用方式 | 描述 |
---|---|---|
dispatch | store.dispatch(action) | action參數將參與store更新,並分發給subscribe函數正在監聽的reducer |
subscribe | store.subscribe(listener) | listener監聽者,實際上就是回調函數 |
getState | store.getState() | 獲取state |
replaceReducer | store.replaceReducer(nextReducer) | 刷新reducer並初始化store |
Dispatch方法用於更新store狀態樹,流程是在dispatch接受一個action,由action決定調用reducer轉換狀態樹, 且通知監聽者數據已發生變化,從dispatch源碼中看到函數currentReducer(currentState, action)
傳遞state、action,觀察者列表listeners
直接for循環遍歷執行listeners[i]()
。
function dispatch(action) { ... currentState = currentReducer(currentState, action) ... var listeners = currentListeners = nextListeners for (var i = 0; i < listeners.length; i++) { listeners[i]() } return action }
在主流的redux思想裏有一種說法叫「redux命令行模式」,其中dispatch比做分發器,這個形容很貼切。Dispatch方法就是接收action並將action裏的信息分發給store和reducer,這裏畫了一個簡單的圖示以dispatch(action)
方式展示dispatch函數的執行過程。
const action = { type: 'ADD', payload: '***'}; dispatch(action);
除此以外dispatch函數的執行方法兩種形式,其一是bindActionCreators(action)
方式,bindActionCreators函數在本文的後面也有說到。
const action = { type: 'ADD', payload: '***'}; const bindActionCreators = require('redux').bindActionCreators; bindActionCreators(action);
其二是dispatch action creator
方式,大多數項目中應用的都是這種方式。
function addTodo(text) { return { type: ADD, payload: text, } } store.dispatch(addTodo('***'));
每次執行dispatch,經過subscribe註冊的listener都會被執行,當listener列表較多時listeners[i]()
都會被執行由此產生性能損耗,從工程師的角度更難定位到哪一個具體的reducer內的監聽者被觸發,這個時候須要一些輔助工具藉助applyMiddleware
函數擴展中間件來幫助開發者。
import {applyMiddleware} from 'redux'; import thunk from 'redux-thunk'; const store = applyMiddleware([thunk])(createStore); const dispatch = store.dispatch;
Middleware is the suggested way to extend Redux with custom functionality3.
更全面具體一些的擴展就要說到redux中間件的概念,若是熟悉expressjs4或者koajs5,應該會對Middleware很熟悉,在redux中的中間件是一個高階函數,通俗講更多的是對現有dispatch函數進行擴展,其邏輯傾向於AOP6,意在將散佈在各處的橫切代碼(cross-cutting code)以及一些被重複使用的功能性組件被重複使用。
import {applyMiddleware, compose, createStore} from 'redux'; import {routerMiddleware} from 'react-router-redux'; import thunkMiddleware from 'redux-thunk'; import {persistState} from 'redux-devtools'; import makeRootReducer from './reducers'; import DevTools from '../containers/DevTools'; export default (initialState = {}, history) => { ... const middleware = [thunkMiddleware, routerMiddleware(history), ...debugware]; const enhancers = []; ... enhancers.push(devToolsExtension()) ... const store = createStore( makeRootReducer(), initialState, compose( applyMiddleware(...middleware), ...enhancers ) ); ... store.replaceReducer(reducers(store.asyncReducers)) ... return store }
Subscribe從設計的角度來講是一個訂閱者,監聽事件變化。
function subscribe(listener) { ... ensureCanMutateNextListeners() nextListeners.push(listener) return function unsubscribe() { ... ensureCanMutateNextListeners() var index = nextListeners.indexOf(listener) nextListeners.splice(index, 1) } }
getState是獲取當前store的state(currentState);
function getState() { return currentState }
動態替換reducer函數
function replaceReducer(nextReducer) { ... currentReducer = nextReducer dispatch({ type: ActionTypes.INIT }) }
State對象存儲在store狀態樹中,state只能經過dispatch(action)
來觸發更新,更新邏輯由reducer來執行,須要注意的是當state變化時會返回全新的對象,而不是修改傳入的參數。
這裏示例的邏輯由四步分組成,第一部分是定義子路由,在子路由中引入用於收集reducer的回調函數,我命名爲injectReducer,固然也能夠命名其它的名字,如collectReducer、pushReducer等等均可以
import { injectReducer } from '../../store/reducers'; export default (store) => ({ path: 'login', getComponent (nextState, cb) { require.ensure([], (require) => { const LoginPage = require('./components/Login').default; const reducer = require('./extend/reducer').default; injectReducer(store, { key: 'login_reducer', reducer }); cb(null, LoginPage); }, 'login') } })
在第一部分中store和reducer已經被傳入injectReducer中,第二部分就是injectReducer函數的內部邏輯。
import { combineReducers } from 'redux' import { routerReducer as router } from 'react-router-redux' export const makeRootReducer = (asyncReducers) => { return combineReducers({ router, ...asyncReducers }) }; export const injectReducer = (store, { key, reducer }) => { store.asyncReducers[key] = reducer; store.replaceReducer(makeRootReducer(store.asyncReducers)) }; export default makeRootReducer
InjectReducer函數內用replaceReducer方法將store從新計算,這裏這樣作的原因是第一部分的子路由是異步加載的,並非在服務器開始時直接加載完畢,而是隨着用戶在客戶端不斷操做頁面異步更新reducer以及加載組件等信息。
import {applyMiddleware, compose, createStore} from 'redux'; ... import makeRootReducer from './reducers'; export default (initialState = {}, history) => { ... const store = createStore( makeRootReducer(), initialState, compose( applyMiddleware(...middleware), ...enhancers ) ); ... store.replaceReducer(reducers(store.asyncReducers)) ... return store }
第三部分是初始化store。
import CoreLayout from '../layouts/CoreLayout/components/CoreLayout'; import {Dashboard} from './module' export const createRoutes = (store) => ({ path : '/', component : CoreLayout, indexRoute: Home, getChildRoutes(location, cb) { cb(null, [ Dashboard(store) ]) } }); export default createRoutes
第四部分是初始化路由,按道理順序應該是第四部分=>第三部分=>第一部分=>第二部分,可是若是考慮異步等信息的話,我我的認爲按邏輯優先級應該是這樣排比較好。
npm install --save normalizr
Store樹對象或者組件自身state樹對象實質上是JSON對象,因此在redux開發過程當中,爲避免不一樣數據之間相互引用或返回相互嵌套的值,可使用normalizr對state扁平化、範式化處理。
可變對象能夠用Object.assign
或者lodash的cloneDeep
函數。
const assign = Object.assign || require('object.assign'); assign({}, state, { ADD: action.newState })
不可變對象(immutable state)是指在建立後不可再被修改的對象,它能夠經過引用級的比對檢查來提高渲染性能,在redux開發中通常會使用immutablejs實現不可變對象,須要注意的是immutablejs每次操做以後老是返回一個新的數據,原有的數據不會改變。
immutablejs經過結構共享來解決的數據拷貝時的性能問題,即當數據對象key->value
鍵值對被改變時,immutablejs會只clone
數據對象被改變對象節點的父節點以上的部分,其餘保持不變,由此達到舊對象與immutablejs返回的新對象共享部分數據並提升性能。
測試:
Selector擴展組件,因爲reselect帶有緩存功能,因此使用它能夠避免沒必要要的selector計算
Action一樣是一個javascript對象,一般包含type
等一些字段。
「Action Creator」是action的創造者,本質上就是一個函數,返回值是一個action,「Action Creator」能夠是同步也能夠是異步。
function add() { return { tyle: 'ADD' } } dispatch(add());
redux-thunks 和 redux-promise 分別是使用異步回調和 Promise 來解決異步 action 問題的。
若是直接用Fetch API,可能一些瀏覽器並不支持,因此仍是須要添加墊片isomorphic-fetch
function fetchDataAsync() { return function (dispatch) { fetch('/posttest', { method : 'POST', headers: { 'Content-Type': "application/json", 'Accept' : "application/json" }, body : JSON.stringify({item: 'text'}) }).then(res => { if (res.ok) { dispatch({type: LOGIN_REQUEST, loginRequest: true}); ... } }, e => { ... }); } }
bindActionCreators()
能夠自動把多個action建立函數綁定到dispatch()
方法上。
借鑑store對reducer的封裝(減小傳入 state 參數)。能夠對dispatch進行再一層封裝,將多參數轉化爲單參數的形式,經 bindActionCreators包裝事後的「Action Creator」造成了具備改變全局state數據的多個函數,將這些函數分發到各個地方,即能經過調用這些函數來改變全局的state。
var actionCreators = bindActionCreators ( actionCreators , store.dispatch ) ;
Reducer是一個javaScript函數,命名上也與Array.prototype.reduce()相像,函數簽名爲(previousState, action) => newState
,接受previousState和action兩個參數,根據action.type
中攜帶的信息對previousState作出相應的處理,並返回一個新的state。另外在redux中一個action能夠觸發多個reducer,一個reducer中也能夠包含多種「action.type」的處理,因此兩者關係爲多對多。。
import {SET_AUTH} from './actionType'; const assign = Object.assign || require('object.assign'); const initialState = { loggedIn : require('./action').default()(), }; const ACTION_HANDLERS = { [SET_AUTH] : (state, action) => assign({}, state, { loggedIn: action.newState }) }; export default function (state = initialState, action) { const handler = ACTION_HANDLERS[action.type]; return handler ? handler(state, action) : state }
combineReducers()
將調用一系列 reducer,並根據對應的 key 來篩選出 state 中的一部分數據給相應的 reducer,這樣也意味着每個小的 reducer 將只能處理 state 的一部分數據,如:filterReducer 將只能處理及返回 state.filter 的數據,若是須要使用到其餘 state 數據,那仍是須要爲這類 reducer 傳入整個 state。
import { combineReducers } from 'redux' ... combineReducers({ router, ...asyncReducers }) ...
React經過Context屬性,能夠將屬性props直接給子component,無須經過props層層傳遞, Provider得到store而後將其傳遞給子元素。
export default class Provider extends Component { getChildContext() { return { store: this.store } } constructor(props, context) { super(props, context) this.store = props.store } componentWillReceiveProps(nextProps) { const { store } = this const { store: nextStore } = nextProps if (store !== nextStore) { warnAboutReceivingStore() } } render() { let { children } = this.props return Children.only(children) } } Provider.childContextTypes = { store: storeShape.isRequired }
Provider中的store能夠在子組件中用contextTypes獲取。
childrenComponent.contextTypes = { store: storeShape }
須要注意的是因爲react-redux,咱們通常對綁定的組件稱爲Smart and Dumb Components
。
Location | Use React-Redux | To read data, they | To change data, they | |
---|---|---|---|---|
「Smart」 Components | Top level, route handlers | Yes | Subscribe to Redux state | Dispatch Redux actions |
「Dumb」 Components | Middle and leaf components | No | Read data from props | Invoke callbacks from props |
Provider將store放到context中,connect就能夠獲取store,使用store的方法,好比dispatch。其實沒有被connect的組件經過聲明contextTypes屬性也是能夠獲取store,使用store的方法的,可是這個時候,若是使用dispatch修改了store的state,React-Redux並不能把修改後的state做爲props給React組件,可能會致使UI和數據不一樣步,因此這個時候必定要清楚本身在作什麼。
import React, { Component, PropTypes } from 'react'; import { Router } from 'react-router'; import { Provider } from 'react-redux'; class AppContainer extends Component { static propTypes = { history: PropTypes.object.isRequired, routes: PropTypes.object.isRequired, store: PropTypes.object.isRequired }; render () { const { history, routes, store } = this.props; return ( <Provider store={store}> <div style={{ height: '100%' }}> <Router history={history} children={routes} /> </div> </Provider> ) } } export default AppContainer
npm i --save react-redux
Connect是由react-redux提供的一個高階函數。源碼中connect函數接收mapStateToProps、mapDispatchToProps、mergeProps、options
四個參數返回一個用於生產Component的函數wrapWithConnect,而後再將組件Component做爲參數注入wrapWithConnect(WrappedComponent)
函數。
參數 | 描述 |
---|---|
mapStateToProps | 將state做爲返回結果綁定到組件的props對象上 |
mapDispatchToProps | |
mergeProps | |
options |
export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) { ... return function wrapWithConnect(WrappedComponent) { ... } }
值得一說的是hoistStatics函數源於hoist-non-react-statics
第三方,做用是將原來組件中的元素拷貝到目標組件。在使用connect函數的時候直接在已聲明的component後面引用connect。
import React, {Component} from 'react'; ... import {connect} from 'react-redux'; class Login extends Component { ... render() { ... } } ... export default connect(mapStateToProps, mapDispatchToProps)(Login)
Connect不僅爲react組件提供store中的state數據及擴展dispatch方法,它還爲定義的組件添加了一系列事件操做,這些事件的核心點就是store,而後能夠在本身定義的組件內得到store。
constructor(){ //獲取store this.store = props.store || context.store const storeState = this.store.getState() //把store的state做爲組件的state,後面經過更新state更新組件 this.state = { storeState } //清除組件的狀態,內部是一系列的標示還原 this.clearCache() }
Github源碼 | 描述 |
---|---|
ducks-modular-redux | {ctionTypes, actions, reducer}規則解決方案 |
react-slingshot | |
saga-login-flow | |
login-flow | |
redux-saga | |
redux-auth-wrapper | |
dva | |
react-redux-tutorial | |
reduxjs doc | reduxjs中文檔案 |
alloyteam:react-redux | React 數據流管理架構之 Redux 介紹 |
redux.js
文檔中首頁的一段話,對redux
特性的官方描述。 ↩ DOM
元素點擊、頁面路由切換等功能的操做行爲,在redux中被稱爲action。 ↩ middleware
函數可以訪問請求對象 req
、響應對象 res
以及應用程序的請求/響應循環中的下一個中間件middleware
函數。下一個中間件函數一般由名爲next
的變量來表示。 ↩ OOP
)。 ↩