Flux、Redux到react-redux發展衍變三部曲之react-redux解讀
首先要知道,Redux只是一種MVC的機制,它可以執行在任何的前端框架中。react-redux則是對Redux的核心模組(如store)進行封裝,並加入了一些有利於react開發的模組,這樣就可以更便捷的在react中運用redux。
本篇將詳細介紹react-redux開發中的常用模組。在閱讀本文前,假設您已經基本理解了Redux的執行機制,若對其機制還不是很清楚,請參考上一篇《Flux、Redux到react-redux發展衍變之Redux解讀》
下面是兩個關於react-redux應用的demo,可以作為本篇知識的參考案例:
TODOS:https://github.com/smallH/react-redux-todomvc-demo.git
購物車:https://github.com/smallH/react-redux-shoppingcart-demo.git
結合上篇,執行一下上面兩個例子,就可以學會 'react-redux' 用法,本篇不再累贅,這裡主要介紹其一些注意的細節和外掛。
mapDispatchToProps的多種入參方式
mapDispatchToProps的作用是:告訴元件所需的actions函式並把它們作為入參props傳遞到元件裡,供元件使用。
大多數情況下使用標準的引用方法如下:
// actions.js export const add = () => ({ type: 'ADD' }) // Link.js class Link extends React.Component { render() { const {addAction} = this.props; return( <div onClick={addAction}>增加一條</div> ) } } export default Link // ContainersLink.js 給Link元件定義一個addAction入參事件:增加一條記錄 import { connect } from 'react-redux' import { add } from '../actions' import Link from '../components/Link' const mapDispatchToProps = (dispatch, ownProps) => ({ addAction: () => { dispatch(add()) } }) export default connect(null, mapDispatchToProps)(Link)
有沒有發現程式碼有點多,於是,我們可以優化為:
// Link.js class Link extends React.Component { render() { const {add} = this.props; return( <div onClick={add}>增加一條</div> ) } } export default Link // ContainersLink.js import { connect } from 'react-redux' import { add } from '../actions' export default connect(null, {add})(Link)
兩種用法效果是一樣的。另外,當actions.js裡需要引用的函式較多時,可以寫成:
// Link
class Link extends React.Component {
render() {
const { add } = this.props;
return(
<div onClick={ add }>增加</div>
)
}
}
export default Link
// ContainersLink.js
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import * as AllActions from '../actions'
export default connect(null, AllActions)(Link)
或者如果不想通過connect,則可以使用bindActionCreators(actions, dispatch)。
classnames 實現樣式管理
react中樣式管理的方式有很多,有很多專案習慣直接在每個元件最底下定義常量樣式檔案style,但這種方法有一定侷限性。本篇中介紹通過classnames實現了對樣式的條件引用,對於一些現實/隱藏元件的操作非常方便。
模組安裝:
npm install classnames -S
用法有以下幾種:
classNames('foo', 'bar'); // => 'foo bar'
classNames('foo', { bar: true }); // => 'foo bar'
classNames({ 'foo-bar': true }); // => 'foo-bar'
classNames({ 'foo-bar': false }); // => ''
classNames({ foo: true }, { bar: true }); // => 'foo bar'
classNames({ foo: true, bar: true }); // => 'foo bar'
如給div增加active樣式,程式碼可以是:
import classnames from 'classnames'
// 以下兩種寫法效果是一樣的
let bool = true;
<div className={classnames({ "active": bool})}></div>
<div className={classnames({ "active": true })}></div>
上例中,可以用過計算bool值實現動態顯示/隱藏元件。
combineReducers 合併多個reduce
隨著應用變得複雜,需要對reducers函式進行拆分,拆分後的每一塊獨立負責管理state的一部分。combineReducers函式的作用是:把由多個不同reducer函式作為value,合併成一個最終的reducers函式,然後就可以對這個reducers呼叫createStore。合併後的reducers把各個子reduer返回的結果合併成一個state。state物件的結構由傳入的多個reducer的key決定。如state 物件的結構會是這樣:
{
reducer1: ...
reducer2: ...
}
如多個reducer合併實現:
// todos.js
const todos = (state = {}, action) => {
switch(action.type) {
case ActionTypes.UPDATE:
console.log("todo update!");
return state
default:
return state
}
}
// visibilityFilter.js
const visibilityFilter = (state = {}, action) => {
switch(action.type) {
case ActionTypes.UPDATE:
console.log("visibilityFilter update!");
return state
default:
return state
}
}
// 合併reducer
import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'
const rootReducer = combineReducers({
todos,
visibilityFilter
})
export default rootReducer
這裡要注意,合併後reducers裡的子reducer依然會遍歷執行所有觸發的actions函式。如當觸發ActionTypes.UPDATE時,上例中會列印todo update! 和 visibilityFilter update!
除此之外,在reducers裡的子reducer也允許二次合併,如把todos修改如下:
// todos
const todo_child1 = (state = [], action) => {}
const todo_child2 = (state = [], action) => {}
const todos = combineReducers({
todo_child1,
todo_child2
})
export default todos
這時state返回的是物件是 {{todo_child1, todo_child2}, visibilityFilter}
reselect 快取機制,演算法優化
reselect 模組使用到了快取機制,Selector可以計算衍生的資料,可以讓Redux做到儲存儘可能少的state,在元件互動操作的時候,優化了state發生變化帶來的壓力。簡單來說,它會執行函式時產生的結果儲存至記憶體中,當下一次輸入不變的情況下,直接從記憶體中獲取輸出結果。使用方法:
import { createSelector } from 'reselect'
const getTodos = state => state.todos.present
// 使用 Selector
export const getCompletedTodoCount = createSelector([getTodos], (todos) => (
todos.reduce((count, todo) => todo.completed ? count + 1 : count, 0)
))
// 不使用 Selector
export const getCompletedTodoCount = state =>
state.todos.reduce((count, todo) => (todo.completed ? count + 1 : count), 0);
上例中若不使用Selector,每次state發生變化都會重新計算一次。
redux-undo 實現撤銷重做
可以實現對每次state狀態變化的監聽和記錄,通過觸發undo和惹到實現撤銷重做的效果。如給todos模組增加undoable監聽:
import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'
import undoable from 'redux-undo'
// todos 任務列表增刪改查
// visibilityFilter設定濾邏條件
const rootReducer = combineReducers({
todos: undoable(todos),
visibilityFilter
})
export default rootReducer
撤銷重做的觸發:
import React from 'react'
import { ActionCreators as UndoActionCreators } from 'redux-undo'
import { connect } from 'react-redux'
import classnames from 'classnames'
let UndoRedo = ({
onUndo,
onRedo
}) => (
<div className={classnames({ "undoredu": true })}>
<div onClick={onUndo} className={classnames({ "undoredu-btn": true })}>
撤銷
</div>
<div onClick={onRedo} className={classnames({ "undoredu-btn": true })}>
重做
</div>
</div>
)
const mapDispatchToProps = ({
onUndo: UndoActionCreators.undo,
onRedo: UndoActionCreators.redo
})
UndoRedo = connect(
null,
mapDispatchToProps
)(UndoRedo)
export default UndoRedo
這裡要注意,要獲取經過undoable包裝後的todos狀態值時,要由state.todos 改為 state.todos.present。
redux-thunk 獲取dispatch控制權,實現非同步處理
redux-thunk屬於中介軟體,它可以讓action建立函式先不返回一個action物件,而是返回一個函式,函式傳遞兩個引數(dispatch,getState),在函式體內進行業務邏輯的封裝。就是在Action傳遞到Store之前再做點事情,常用於非同步處理。先看看thunk原始碼如下:
function createThunkMiddleware(extraArgument) {
return function (_ref) {
var dispatch = _ref.dispatch,
getState = _ref.getState;
return function (next) {
return function (action) {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
};
};
}
var thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
使用方法:
import React from 'react'
import { render } from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import thunk from 'redux-thunk'
import reducer from './reducers'
import App from './containers/App'
const middleware = [thunk];
const store = createStore(reducer, applyMiddleware(...middleware))
render(
<Provider store={store}>
<App />
</Provider>, document.getElementById('root')
)
引入thunk後,這時,原來的Action裡的函式會由:
export const addToCart = productId => {}
變為:
export const addToCart = productId => (dispatch, getState) => {}
可以發現,Action中的addToCart拿到了dispatch的控制權,這樣就可以配合axios和Promise實現非同步請求的實現。如:
// shop.js
const _get = ({
url,
query
}) => {
return axios({
method: 'get',
url,
params: { ...query}
}).then((res) => {
if(res.status == 200) {
return res.data
}
return Promise.reject(res);
}, (err) => {
return Promise.reject(err.message || err.data);
});
};
export const getNetProducts = () => {
const url = "/data/products.json"
const query = {}
return _get({
url,
query
}).then((data) => Promise.resolve(data)).catch((e) => Promise.reject(e));
}
export default {
getNetProducts: getNetProducts
}
// actions.js
import shop from '../api/shop'
import * as types from '../constants/ActionTypes'
const receiveProducts = products => ({
type: types.RECEIVE_PRODUCTS,
products
})
export const getAllProducts = () => dispatch => {
// 請求網路JSON資料
shop.getNetProducts().then((data) => {
dispatch(receiveProducts(data)) // 等到請求結束再觸發實現非同步
}).catch((e) => {
console.log("請求資料發生錯誤");
});
}
自定義中介軟體的結構寫法如下:
const myMiddleware = (store) => (next) => (action) => {
// 對action資料進行操作
// 返回action物件
next(action)
}
const middleware = [thunk, myMiddleware];
一般情況下是不需要用到自定義中介軟體的,因為npm上有提供了大部分可用的中介軟體,可自己選擇性查詢使用。
整篇的《Flux、Redux到react-redux衍變發展》三部曲介紹至此總結完畢啦,希望這些知識點的歸納能對大家有用,我會繼續努力分享更多前端知識總結給大家,共同進步。