在上一篇文章中,我們通過一個示例頁面,瞭解到Redux的使用方法以及各個功能模組的作用。如果還不清楚Redux如何使用,可以先看看Redux其實很簡單(示例篇),然後再來看本文,理解起來會更加輕鬆。
那麼在這一篇文章中,筆者將帶大家編寫一個完整的Redux,深度剖析Redux的方方面面,讀完本篇文章後,大家對Redux會有一個深刻的認識。
核心API
這套程式碼是筆者閱讀完Redux原始碼,理解其設計思路後,自行總結編寫的一套程式碼,API的設計遵循與原始一致的原則,省略掉了一些不必要的API。
createStore
這個方法是Redux核心中的核心,它將所有其他的功能連線在一起,暴露操作的API供開發者呼叫。
const INIT = `@@redux/INIT_` + Math.random().toString(36).substring(7)
export default function createStore (reducer, initialState, enhancer) {
if (typeof initialState === `function`) {
enhancer = initialState
initialState = undefined
}
let state = initialState
const listeners = []
const store = {
getState () {
return state
},
dispatch (action) {
if (action && action.type) {
state = reducer(state, action)
listeners.forEach(listener => listener())
}
},
subscribe (listener) {
if (typeof listener === `function`) {
listeners.push(listener)
}
}
}
if (typeof initialState === `undefined`) {
store.dispatch({ type: INIT })
}
if (typeof enhancer === `function`) {
return enhancer(store)
}
return store
}
複製程式碼
在初始化時,createStore會主動觸發一次dispach,它的action.type是系統內建的INIT,所以在reducer中不會匹配到任何開發者自定義的action.type,它走的是switch中default的邏輯,目的是為了得到初始化的狀態。
當然我們也可以手動指定initialState,筆者在這裡做了一層判斷,當initialState沒有定義時,我們才會dispatch,而在原始碼中是都會執行一次dispatch,筆者認為沒有必要,這是一次多餘的操作。因為這個時候,監聽流中沒有註冊函式,走了一遍reducer中的default邏輯,得到新的state和initialState是一樣的。
第三個引數enhancer只有在使用中介軟體時才會用到,通常情況下我們搭配applyMiddleware來使用,它可以增強dispatch的功能,如常用的logger和thunk,都是增強了dispatch的功能。
同時createStore會返回一些操作API,包括:
- getState:獲取當前的state值
- dispatch:觸發reducer並執行listeners中的每一個方法
- subscribe:將方法註冊到listeners中,通過dispatch來觸發
applyMiddleware
這個方法通過中介軟體來增強dispatch的功能。
在寫程式碼前,我們先來了解一下函式的合成,這對後續理解applyMiddleware的原理大有裨益。
函式的合成
如果一個值要經過多個函式,才能變成另外一個值,就可以把所有中間步驟合併成一個函式,這叫做函式的合成(compose)
舉個例子
function add (a) {
return function (b) {
return a + b
}
}
// 得到合成後的方法
let add6 = compose(add(1), add(2), add(3))
add6(10) // 16
複製程式碼
下面我們通過一個非常巧妙的方法來寫一個函式的合成(compose)。
export 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)))
}
複製程式碼
上述程式碼巧妙的地方在於:通過陣列的reduce方法,將兩個方法合成一個方法,然後用這個合成的方法再去和下一個方法合成,直到結束,這樣我們就得到了一個所有方法的合成函式。
有了這個基礎,applyMiddleware就會變得非常簡單。
import { compose } from `./utils`
export default function applyMiddleware (...middlewares) {
return store => {
const chains = middlewares.map(middleware => middleware(store))
store.dispatch = compose(...chains)(store.dispatch)
return store
}
}
複製程式碼
光看這段程式碼可能有點難懂,我們配合中介軟體的程式碼結構來幫助理解
function middleware (store) {
return function f1 (dispatch) {
return function f2 (action) {
// do something
dispatch(action)
// do something
}
}
}
複製程式碼
可以看出,chains
是函式f1的陣列,通過compose將所欲f1合併成一個函式,暫且稱之為F1,然後我們將原始dispatch傳入F1,經過f2函式一層一層地改造後,我們得到了一個新的dispatch方法,這個過程和Koa的中介軟體模型(洋蔥模型)原理是一樣的。
為了方便大家理解,我們再來舉個例子,有以下兩個中介軟體
function middleware1 (store) {
return function f1 (dispatch) {
return function f2 (action) {
console.log(1)
dispatch(action)
console.log(1)
}
}
}
function middleware2 (store) {
return function f1 (dispatch) {
return function f2 (action) {
console.log(2)
dispatch(action)
console.log(2)
}
}
}
// applyMiddleware(middleware1, middleware2)
複製程式碼
大家猜一猜以上的log輸出順序是怎樣的?
好了,答案揭曉:1, 2, (原始dispatch), 2, 1。
為什麼會這樣呢?因為middleware2接收的dispatch是最原始的,而middleware1接收的dispatch是經過middleware1改造後的,我把它們寫成如下的樣子,大家應該就清楚了。
console.log(1)
/* middleware1返回給middleware2的dispatch */
console.log(2)
dispatch(action)
console.log(2)
/* end */
console.log(1)
複製程式碼
三個或三個以上的中介軟體,其原理也是如此。
至此,最複雜最難理解的中介軟體已經講解完畢。
combineReducers
由於Redux是單一狀態流管理的模式,因此如果有多個reducer,我們需要合併一下,這塊的邏輯比較簡單,直接上程式碼。
export default function combineReducers (reducers) {
const availableKeys = []
const availableReducers = {}
Object.keys(reducers).forEach(key => {
if (typeof reducers[key] === `function`) {
availableKeys.push(key)
availableReducers[key] = reducers[key]
}
})
return (state = {}, action) => {
const nextState = {}
let hasChanged = false
availableKeys.forEach(key => {
nextState[key] = availableReducers[key](state[key], action)
if (!hasChanged) {
hasChanged = state[key] !== nextState[key]
}
})
return hasChanged ? nextState : state
}
}
複製程式碼
combineReucers將單個reducer塞到一個物件中,每個reducer對應一個唯一鍵值,單個reducer狀態改變時,對應鍵值的值也會改變,然後返回整個state。
bindActionCreators
這個方法就是將我們的action和dispatch連線起來。
function bindActionCreator (actionCreator, dispatch) {
return function () {
dispatch(actionCreator.apply(this, arguments))
}
}
export default function bindActionCreators (actionCreators, dispatch) {
if (typeof actionCreators === `function`) {
return bindActionCreator(actionCreators, dispatch)
}
const boundActionCreators = {}
Object.keys(actionCreators).forEach(key => {
let actionCreator = actionCreators[key]
if (typeof actionCreator === `function`) {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
})
return boundActionCreators
}
複製程式碼
它返回一個方法集合,直接呼叫來觸發dispatch。
中介軟體
在自己動手編寫中介軟體時,你一定會驚奇的發現,原來這麼好用的中介軟體程式碼竟然只有寥寥數行,卻可以實現這麼強大的功能。
logger
function getFormatTime () {
const date = new Date()
return date.getHours() + `:` + date.getMinutes() + `:` + date.getSeconds() + ` ` + date.getMilliseconds()
}
export default function logger ({ getState }) {
return next => action => {
/* eslint-disable no-console */
console.group(`%caction %c${action.type} %c${getFormatTime()}`, `color: gray; font-weight: lighter;`, `inherit`, `color: gray; font-weight: lighter;`)
// console.time(`time`)
console.log(`%cprev state`, `color: #9E9E9E; font-weight: bold;`, getState())
console.log(`%caction `, `color: #03A9F4; font-weight: bold;`, action)
next(action)
console.log(`%cnext state`, `color: #4CAF50; font-weight: bold;`, getState())
// console.timeEnd(`time`)
console.groupEnd()
}
}
複製程式碼
thunk
export default function thunk ({ getState }) {
return next => action => {
if (typeof action === `function`) {
action(next, getState)
} else {
next(action)
}
}
}
複製程式碼
這裡要注意的一點是,中介軟體是有執行順序的。像在這裡,第一個引數是thunk,然後才是logger,因為假如logger在前,那麼這個時候action可能是一個包含非同步操作的方法,不能正常輸出action的資訊。
心得體會
到了這裡,關於Redux的方方面面都已經講完了,希望大家看完能夠有所收穫。
但是筆者其實還有一個擔憂:每一次dispatch都會重新渲染整個檢視,雖然React是在虛擬DOM上進行diff,然後定向渲染需要更新的真實DOM,但是我們知道,一般使用Redux的場景都是中大型應用,管理龐大的狀態資料,這個時候整個虛擬DOM進行diff可能會產生比較明顯的效能損耗(diff過程實際上是物件和物件的各個欄位挨個比較,如果資料達到一定量級,雖然沒有操作真實DOM,也可能產生可觀的效能損耗,在小型應用中,由於資料較少,因此diff的效能損耗可以忽略不計)。
本文原始碼地址:github.com/ansenhuang/…