redux 流程圖
redux 的使用
先看一下目錄結構
components/counter.js:創建store
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk'
import reducers from '../reducers';
const store = createStore(reducers, applyMiddleware(thunk))
export default store;
reducers/counter.js:構建組件初始狀態;創建純函數reducer;接收action,返回全新的state。
const initialState = {
num: 0
}
const reducer = (state = initialState, action) => {
const { type, data } = action;
switch (type) {
case 'increment':
return {
num: state.num + data
}
case 'decrement':
return {
num: state.num - data
}
default:
return state;
}
}
export default reducer;
reducers/index.js:組合多個 reducer,通過 action.type 將 action 分發到具體的reducer中處理
import { combineReducers } from 'redux';
import counter from './counter';
export default combineReducers({
counter
})
actions/counter.js:actionCreator,產出 action 的地方;返回值爲一個擁有 type 字段的 object對象,或者是一個函數(需要加入redux-thunk 中間件予以支持,後面會講到)
import { bindActionCreators } from 'redux';
import store from '../store';
const actionCreator = {
increment: () => {
return {
type: 'increment',
data: 1
}
},
decrement: () => {
return (dispatch, getState) => {
setTimeout(() => {
dispatch({
type: 'decrement',
data: 2
})
}, 1000)
}
},
reset: () => {
return {
type: 'reset'
}
}
}
actionCreator.reset = bindActionCreators(actionCreator.reset, store.dispatch);
export default actionCreator
components/counter.js:view層,執行 dispatch -> 觸發 action -> action 經過相應的 reducer 處理後,返回全新的 state -> 執行 store.subscribe 註冊的監聽函數 -> 監聽函數中 setState 更新狀態 -> 觸發 react 更新機制 -> 組件更新
import React from 'react';
import store from '../store';
import actionCreator from '../actions/counter'
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
num: store.getState().counter.num
}
store.subscribe(() => {
const state = store.getState();
this.setState({
num: state.counter.num
})
})
}
increment = () => {
store.dispatch(actionCreator.increment())
}
decrement = () => {
store.dispatch(actionCreator.decrement())
}
reset = () => {
actionCreator.reset()
}
render() {
const { num } = this.state;
return (
<div className="App">
{
num
}
<div></div>
<button onClick={ this.increment }>+</button>
<button onClick={ this.decrement }>-</button>
<button onClick={ this.reset }>reset</button>
</div>
);
}
}
export default Counter;
redux 原理解析
整個過程用到了幾個重要的方法:
- createStore:創建 store
- store.dispatch:觸發 action
- store.subscribe:註冊監聽函數,dispatch中會調用所有註冊的監聽函數
- store.getState:返回當前 state
- combineReducers:組合多個 reducer 成一個,根據 action.type 分發 action 到對應的 reducer 中處理
- bindActionCreators:用 dispatch 封裝 actionCreator,這樣在組件中直接調用 actionCreator 方法就能執行 dispatch,而不需要在組件中顯示的調用 dispatch
- applyMiddleware:利用中間件,增強 dispatch 的功能
下面分別大致說一下每個方法的實現過程
createStore
createStore 接收三個參數 (reducer, preloadedState, enhancer),reducer 好理解,preloadedState 是初始狀態,很少用到,enhancer 是增強器,用來增強 redux。
上面的代碼中我是這樣寫的 createStore(reducers, applyMiddleware(thunk)),enhancer寫在了第二個參數上,第三個參數沒有傳,之所以能生效是因爲下面這個判斷
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState as StoreEnhancer<Ext, StateExt>
preloadedState = undefined
}
常見的增強器有 redux-thunk、redux-saga,需要使用 applyMiddleWare 處理之後纔是合格的 enhancer。
該函數內部定義了這麼幾個重要的變量
let currentState = preloadedState //從函數createStore第二個參數preloadedState獲得
let currentReducer = reducer //從函數createStore第一個參數reducer獲得
let currentListeners = [] //當前訂閱者列表
let nextListeners = currentListeners //新的訂閱者列表
let isDispatching = false
createStore 執行完會返回這麼幾個東西:
{
dispatch: dispatch as Dispatch<A>,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
dispatch
if (!isPlainObject(action)) {
// dispatch 的入參必須是一個簡單的 Object 對象
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}
if (typeof action.type === 'undefined') {
// 並且這個 Object 需要具有 type 屬性
throw new Error(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
)
}
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
isDispatching = true // 可以理解爲線程鎖,防止兩個action同時執行
currentState = currentReducer(currentState, action) // 得到最新的state
} finally {
isDispatching = false
}
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener() // 執行所有的監聽函數
}
subscribe
nextListeners.push(listener)
// 返回解綁函數
return function unsubscribe() {
...
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
getState
return currentState as S
combineReducers
它的作用是用來合併多個 reducer,最終會返回一個函數代替所有的 reducer。在 dispatch 後,這個合併後的 combination 會遍歷所有的 reducer 並執行它們,拿到最新的 state。然後與原來的 state 對比看是否是同一個引用,如果是就返回老的 state,否則返回新的 state。
先將入參 reducers 拷貝到 finalReducers 中
const finalReducerKeys = Object.keys(finalReducers)
return function combination(state = {}, action) {
let hasChanged = false
const nextState = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
}
bindActionCreators
經過該方法封裝後的 actionCreator 具備了 dispatch 能力,所以 actionCreator 直接調用就可以觸發 action,不用在組件中顯示的調用 dispatch。如 actions/counter.js 中的 reset,在組件中直接調用 counter.reset() 就可以觸發 dispatch,而不用這麼寫 store.dispatch(counter.reset())
function bindActionCreator<A extends AnyAction = AnyAction>(
actionCreator: ActionCreator<A>,
dispatch: Dispatch
) {
return function(this: any, ...args: any[]) {
return dispatch(actionCreator.apply(this, args))
}
}
export default function bindActionCreators(
actionCreators: ActionCreator<any> | ActionCreatorsMapObject,
dispatch: Dispatch
) {
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
/***********容錯**********/
const boundActionCreators: ActionCreatorsMapObject = {}
for (const key in actionCreators) {
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}
applyMiddleware
在 dispatch 函數中我們提到,它的入參必須是一個簡單的 Object 對象,而且還需要有 type 屬性。如 actions/counter.js 中的 increment,它的返回值就是帶有 type 字段的 Object 對象。
而 decrement 這個 actionCreator 的返回值卻是一個函數,最後也順利的觸發了 action,更新了試圖。這是爲什麼呢?如果依據 dispatch 的源碼來看,這肯定會報錯纔對啊?
原因就是我們使用了 redux-thunk 這個中間件增強了 dispatch 的功能,使其具備了接收函數作爲入參的能力。
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args)
let dispatch = () => {
throw new Error(
`Dispatching while constructing your middleware is not allowed. ` +
`Other middleware would not be applied to this dispatch.`
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
- 增強器 enhancer 是 applyMiddleware 方法的返回值,這個 enhancer 主要分爲一下幾步:
- 入參是 createStore,通過它創建一個 store
- 定義 middlewareAPI,所有的中間件都需要實現 getState、dispatch 這兩個方法
- middlewares 調用 Array.prototype.map 進行改造,存放在 chain
- 用compose整合chain數組,並賦值給dispatch
- 將新的dispatch替換原先的store.dispatch
這樣看還有有些懵逼,我們來看一下大名鼎鼎的 redux-thunk 是怎麼實現的。看懂了它再回頭去看 applyMiddleware 就豁然開朗了
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
我們最終得到的 thunk 應該是這個樣子的
const thunk = ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
它的入參是一個簡單 Object,且擁有 dispatch 和 getState 兩個屬性。我們現在再把這個 thunk 帶回到 applyMiddleware 方法中的 chain 那一步
const chain = middlewares.map(middleware => middleware(middlewareAPI))
執行 thunk 可以得到如下結果,是一個函數,它的入參是 next。返回值也是一個函數,入參是 action。爲了方便理解,我們假設使用了兩個中間件
// thunk(middlewareAPI)
const result1 = next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
const result2 = 同上
// 那麼上一步的 chain 就是這個樣子
chain = [result1, result2]
現在我們得到了 chain 的模樣,接着把 chain 帶入到 applyMiddleware 中
dispatch = compose(...chain)(store.dispatch)
compose 函數是用來組合多個函數的,把一個函數的執行結果作爲下一個函數的入參。我們來看下源碼
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
那麼 chain 經過 compose 處理之後得到的應該是一個函數
const composeRst = (args) => result1(result2(args))
走到這裏,我們再回頭看 result1 的代碼,就能夠知道它的入參 next 其實就是下一個中間件。我們接着往下執行
dispatch = compose(...chain)(store.dispatch)
等價於
dispatch = composeRst(store.dispatch)
等價於
dispatch = result1(result2(store.dispatch))
等價於
dispatch = action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return result2(action);
}
也就是說經過 compose(...chain)(store.dispatch) 一頓操作猛如虎,最終得到的其實就是一個函數,這個函數的入參是一個 action。這不就是 dispatch 的功能嗎!!!
但是比原始的 dispatch 多了個 if 判斷,action 原本只能是一個帶有 type 屬性的簡單 Object,現在可以傳 function 了,然後把 dispatch、getState 傳入了這個 function。
利用這一點我們就可以在 actionCreator 中返回一個異步函數去後端獲取數據,然後在異步函數的回調中調用 dispatch 更新狀態,如文章開頭 actions/counter.js 中的 decrement 這個 actionCreator,是不是覺得很巧妙。鼎鼎大名的 redux-thunk 其實就那麼幾行代碼。
整個 redux 的使用及原理講完了,但是代碼這麼寫一點也不夠優雅。通常在 react 項目中使用 redux,我們會借用 react-redux 做爲橋樑,更加優雅的使用 redux。下一篇講解 react-redux 的使用和原理。