前言
在學習 React 的過程中的小伙伴一定對 Redux 不陌生,并且對 Redux 的一系列流程感到困惑。
其實要了解 Redux 首先要知道三點:
- Redux 和 React 沒有任何關(guān)系, 它也可以用在 Vue 等任何的框架中;
- Redux 是單向數(shù)據(jù)流;
- Redux 僅僅是一個狀態(tài)管理工具。
源碼解析
既然 Redux 是一個狀態(tài)管理工具,那么我們就先從簡單的計數(shù)器開始實現(xiàn)一個最簡單的狀態(tài)管理器。
1. 狀態(tài)管理器
先設(shè)置一個 state 用于保存狀態(tài):
let state = {
number: 1
}
如果需要使用狀態(tài)的時候需要調(diào)用 state.number, 如果需要重新賦值則調(diào)用 state.number = 2,但是這樣會出現(xiàn)一個問題。使用 number 的地方感知不到 number 的變化。
我們使用發(fā)布訂閱模式來通知使用過 number 的訂閱者。
let state = {
number: 1
}
let listeners = [];
/*
訂閱
*/
function subscribe(listener) {
listeners.push(listener)
}
/*
賦值 number
*/
function setNumber(number) {
state.number = number
/* 廣播所有訂閱者 */
for(let listener of listeners) {
listener()
}
}
這樣一個通過訂閱發(fā)布模式的計數(shù)器就完成了,我們嘗試一下。
/* TRY IT */
/*
訂閱
*/
subscribe(() => {
console.log('Number haven been changed.')
})
/*
改變 number
*/
setNumber(2)
現(xiàn)在可以看到當執(zhí)行了 setNumber 之后,訂閱的方法被觸發(fā)并且控制臺輸出更新后的值。
2. 提取公共代碼
雖然代碼奏效,但是新的問題又出現(xiàn)了,這個狀態(tài)管理器只能處理 number。因此咱們把公共的方法提取出來試一下。
export default function createStore(preloadedState) {
let state = preloadedState
let listeners = []
function subscribe(listener) {
/* 訂閱 */
listeners.push(listener)
}
function getState() {
/* 獲取當前 State */
return state
}
function changeState(newState) {
/* 更新 State */
state = newState
/* 廣播所有訂閱者 */
for (listener of listeners) {
listener()
}
}
/* 將封裝的方法返回 */
return {
subscribe,
getState,
changeState
}
}
給初始 State 增加不同的值再試試
/* TRY IT */
/* state初始值 */
let initState = {
counter: {
number: 1
},
article: {
title: '',
author: ''
}
}
/* 創(chuàng)建 Store */
let store = createStore(initState)
/* 訂閱 */
store.subscribe(() => {
let state = store.getState()
console.log(`State haven been changed. ${state.counter.number}`)
})
store.subscribe(() => {
let state = store.getState()
console.log(`State haven been changed. ${state.article.title} By ${state.article.author}`)
})
/* 更改State */
store.changeState({
...store.getState(),
counter: {
number: 2
},
article: {
title: 'How to use Redux',
author: 'Cheuk'
}
})
store.changeState({
...store.getState(),
counter: {
number: 3
},
article: {
title: 'Simple Demo',
author: 'Lee'
}
})
運行之后可以在控制臺看到輸出
State haven been changed. 2
State haven been changed. How to use Redux By Cheuk
State haven been changed. 3>
State haven been changed. Simple Demo By Lee
看來成功了!我們現(xiàn)在已經(jīng)完成一個簡單的狀態(tài)管理器。通過 createStore 創(chuàng)建 Store,其中提供了三個方法:
-
subscribe用于改變訂閱狀態(tài); -
changeState用于改變 State 狀態(tài); -
getState用于獲取當前狀態(tài)。
3. 實現(xiàn)有計劃的狀態(tài)管理器
咱們的狀態(tài)管理器看起來不錯,但是目前還是存在一個問題。基于上一節(jié)的狀態(tài)管理器實現(xiàn)一個自增自減的計數(shù)器。
/* TRY IT */
/* state初始值 */
let initState = {
count: 0
}
/* 創(chuàng)建 Store */
let store = createStore(initState)
/* 訂閱 */
store.subscribe(() => {
let state = store.getState()
console.log(`Count: ${state.count}`)
})
/* 自增 */
store.changeState({
...store.getState(),
count: store.getState().count + 1
})
/* 自減 */
store.changeState({
...store.getState(),
count: store.getState().count - 1
})
/* WTF?! */
store.changeState({
...store.getState(),
count: 'Bad String'
})
問題很明顯,State 中的 count 被改成了字符串。由于計數(shù)器沒有任何約束, State 中的值可能會在任何地方被改成任何值,這樣程序就很難維護。因此我們要給現(xiàn)在的狀態(tài)管理器添加約束。
兩步來解決問題:
- 初始化 store 的時候,讓他知道我們的修改計劃是什么,制定一個 state 的修改計劃;
- 修改 store.changeState 方法,讓他在修改 state 的時候,按照我們的計劃來修改。
我們來聲明一個 plan 方法用于接收 state 和修改計劃(action.type),最后返回改變后的 state 。
/* Our Plan */
/* action 中必須要有 type 屬性 */
function plan (state, action){
switch(action.type) {
case 'INCREMENT':
return {
...state,
count: state.count + 1
}
case 'DECREMENT':
return {
...state,
count: state.count - 1
}
default:
return state
}
}
接著修改一下之前的 createStore
/* createStore 的時候?qū)?plan 方法作為參數(shù)傳入 */
function createStore(plan, preloadedState) {
let state = preloadedState
let listeners = []
function subscribe(listener) {
listeners.push(listener)
}
function getState() {
return state
}
function changeState(action) {
/* 根據(jù) plan 修改 state */
state = plan(state, action)
for (listener of listeners) {
listener()
}
}
return {
subscribe,
getState,
changeState
}
}
現(xiàn)在我們再試試新的 createStore 實現(xiàn)的自增自減:
/* TRY IT */
/* state初始值 */
let initState = {
count: 0
}
/* 創(chuàng)建 Store */
let store = createStore(plan, initState)
/* 訂閱 */
store.subscribe(() => {
let state = store.getState()
console.log(`Count: ${state.count}`)
})
/* 自增 */
store.changeState({
type: 'INCREMENT'
})
/* 自減 */
store.changeState({
type: 'DECREMENT'
})
/* 傳入無效的值和 type 不會影響我們的 State */
store.changeState({
count: 'not work'
})
store.changeState({
type: 'BAD_TYPE'
})
到了這一步我們的狀態(tài)管理器已經(jīng)可以根據(jù)自定義的計劃來工作了。
接下來把代碼中的 plan 改成 reducer,把 changeState 改成 dispatch 就和 Redux 中的變量名一樣了。
4. 合并 reducer
目前我們的狀態(tài)管理器就更加完善了,我們可以通過 reducer 接受舊的 state 做一系列處理再返回新的 state。但是在實際項目中可能會有大量的 state,如果每個 state 的 reducer 都寫在一個方法中做 type 判斷那實在是難以維護。
因此我們可以按照組件的維度來拆分 reducer 函數(shù),然后再通過一個合并函數(shù)將每個 reducer 組合起來。
比如現(xiàn)在有兩個 state:
let initState = {
counter: {
number: 1
},
article: {
title: 'How to write a Redux',
author: 'Cheuk'
}
}
需要兩個 reducer:
/* counterReducer */
function countReducer (state, action){
switch(action.type) {
case 'INCREMENT':
return {
...state,
count: state.count + 1
}
case 'DECREMENT':
return {
...state,
count: state.count - 1
}
default:
return state
}
}
/* articleReducer */
function articleReducer (state, action){
switch(action.type) {
case 'SET_NAME':
return {
...state,
name: action.name
}
case 'SET_TITLE':
return {
...state,
title: action.title
}
default:
return state
}
}
我們再通過 combineReducers 方法,返回一個合并的 reducer,傳入 state 和 action, 可以返回新的 state:
let reducer = combineReducers({
counter: countReducer,
article: articleReducer
})
實現(xiàn) combineReducers 方法:
/*
* combineReducers.js
*/
function combineReducers (reducers){
const reducerKeys = Object.keys(reducers)
return function combination(state = {}, action) {
// 初始化新的 state
const nextState = {}
for (let key of reducerKeys) {
// 根據(jù) key 得到對應模塊的reducer
const reducer = reducers[key]
// 根據(jù) key 得到對應模塊的舊的 state
const previousStateForKey = state[key]
// 將 state 和 action 傳入 reducer 返回新的 state
const nextStateForKey = reducer(previousStateForKey, action)
// 每個模塊的 state 都通過 key 傳入新的 state 中
nextState[key] = nextStateForKey;
}
// 返回一個新的 state
return nextState
}
}
到這里我們已經(jīng)可以將不同模塊的 reducer 通過combineReducers 方法組合在一起,來測試一下:
/* TRY IT */
let reducer = combineReducers({
counter: countReducer,
article: articleReducer
})
/* state初始值 */
let initState = {
counter: {
count: 1
},
article: {
title: 'old title',
author: 'old author'
}
}
/* 創(chuàng)建 Store */
let store = createStore(reducer, initState)
/* 訂閱 */
store.subscribe(() => {
let state = store.getState()
console.log(`state.article: { author: ${state.article.author}, title: ${state.article.title} }`)
})
store.subscribe(() => {
let state = store.getState()
console.log(`state.counter: { count: ${state.counter.count} }`)
})
/* 自增 */
store.dispatch({
type: 'INCREMENT'
})
/* 自減 */
store.dispatch({
type: 'DECREMENT'
})
/* 更新 author */
store.dispatch({
type: 'SET_AUTHOR',
author: 'new author'
})
/* 更新 title */
store.dispatch({
type: 'SET_TITLE',
title: 'new title'
})
棒極了,現(xiàn)在我們已經(jīng)可以在項目中把 reducer 按照組件模塊拆分,這樣業(yè)務中每個組件只需要維護自己的reducer,最后再合并到一起就可以了。
5. 整合 state
上一節(jié)中盡管我們現(xiàn)在拆分了 reducer,可是 state 還是寫在了一起。那么我們需要把 state 也按照組件拆分到各個模塊中。
比如上一節(jié)的 counter 在業(yè)務中我們希望:
/* counterReducer.js*/
//單個的state
let initState = {
count: 1
/* counterReducer */
function countReducer (state, action){
/*如果參數(shù) state 沒有初始值,那就給他初始值*/
if (!state) {
state = initState;
}
switch(action.type) {
case 'INCREMENT':
return {
...state,
count: state.count + 1
}
case 'DECREMENT':
return {
...state,
count: state.count - 1
}
default:
return state
}
}
在初始化的時候每個組件的 reducer 中我們都設(shè)置了各自組件的默認值,那么最后我們需要在 createStore 中整合所有的 state :
function createStore(reducer, preloadedState) {
let state = preloadedState
let listeners = []
function subscribe(listener) {
listeners.push(listener)
}
function getState() {
return state
}
function dispatch(action) {
state = reducer(state, action)
for (listener of listeners) {
listener()
}
}
/**通過一個不匹配任何 reducer的 type,來全部的初始值*/
/**redux 中是使用一個隨機的字符串來保證不匹配任何 reducer, 這里使用了 ES6 的 Symbol 類型*/
dispatch({ type: Symbol() });
return {
subscribe,
getState,
dispatch
}
}
完美,現(xiàn)在我們的狀態(tài)管理器已經(jīng)完全可以根據(jù)組件拆分成不同模塊了。
對比 Redux 我們的狀態(tài)管理器還缺少中間件。
6.實現(xiàn)中間件
什么是中間件?
It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer. People use Redux middleware for logging, crash reporting, talking to an asynchronous API, routing, and more.
根據(jù)官方文檔的解釋就是,中間件是對 dispatch 功能的擴展。我們可以通過中間件做日志記錄,崩潰報告,與異步API通信,路由等工作。
實現(xiàn)一個記錄日志的中間件
我們在每次要修改 state 的時候記錄下修改前的 state 和修改后的 state 以及action:
/* 創(chuàng)建 Store */
let store = createStore(reducer, initState)
/* Logger Middleware */
const next = store.dispatch
store.dispatch = action => {
console.log("======日志======", new Date())
console.log("修改前的state: ", store.getState())
console.log("action: ", action)
next(action)
console.log("修改后的state: ", store.getState())
console.log("================")
}
接著上一節(jié)的例子,我們添加了一個日志的中間件,執(zhí)行之后就能看到日志輸出了。
很棒!我們的日志中間件成功了,但是我們收到了新的需求,需要把錯誤日志也打印出來。好吧,只有加下班了。
實現(xiàn)一個記錄異常日志的中間件
修改一下我們的日志中間件:
store.dispatch = action => {
try{
console.log("======日志======", new Date())
console.log("修改前的state: ", store.getState())
console.log("action: ", action)
next(action)
console.log("修改后的state: ", store.getState())
console.log("================")
} catch(err) {
console.error("錯誤報告: ", err)
}
}
完美!可是這時候我們發(fā)現(xiàn)如果一直有新的需求就需要不斷地增加 dispatch,這可實在是不好維護,我們需要把中間件提取出來:
/* 提取 Logger Middleware */
store.dispatch
let loggerMiddleware = action => {
console.log("======日志======", new Date())
console.log("修改前的state: ", store.getState())
console.log("action: ", action)
next(action)
console.log("修改后的state: ", store.getState())
console.log("================")
}
/* 提取 Exception Middleware */
let exceptionMiddleware = action => {
try{
loggerMiddleware(action)
} catch(err) {
console.error("錯誤報告: ", err)
}
}
store.dispatch = exceptionMiddleware
但是這樣一來,每個中間件之間互相耦合,也不太好。我們用一種組合的方式將每個中間件結(jié)合起來:
/* 提取 Logger Middleware */
store.dispatch
let loggerMiddleware = next => action => {
console.log("======日志======", new Date())
console.log("修改前的state: ", store.getState())
console.log("action: ", action)
next(action)
console.log("修改后的state: ", store.getState())
console.log("================")
}
/* 提取 Exception Middleware */
let exceptionMiddleware = next =>action => {
try{
next(action)
} catch(err) {
console.error("錯誤報告: ", err)
}
}
/* 通過一層層地執(zhí)行 */
store.dispatch = exceptionMiddleware(loggerMiddleware(next))
看起還行,但是還存在一個問題,因為中間件大多數(shù)是第三方插件,因此像loggerMiddelware 這樣的中間需要從外部獲取 store,因此我們需要把 store 也在作為參數(shù)傳進來:
/* 提取 Logger Middleware */
store.dispatch
let loggerMiddleware = store => next => action => {
console.log("======日志======", new Date())
console.log("修改前的state: ", store.getState())
console.log("action: ", action)
next(action)
console.log("修改后的state: ", store.getState())
console.log("================")
}
/* 提取 Exception Middleware */
let exceptionMiddleware = store => next =>action => {
try{
next(action)
} catch(err) {
console.error("錯誤報告: ", err)
}
}
/* 傳入store */
let exception = exceptionMiddleware(store)
let logger = loggerMiddleware(store)
/* 通過一層層地執(zhí)行 */
store.dispatch = exception(logger(next))
現(xiàn)在我們已經(jīng)可以完全將這兩個中間件作為第三方的插件獨立于項目之外,我們的狀態(tài)管理器得到了擴展。
7.優(yōu)化中間件
上面的例子使用起來其實還不夠友好,既然已知三個需要用到的中間件,我們可以把其中實現(xiàn)的細節(jié)封裝起來:
const newCreateStore = applyMiddleware(
exceptionMiddleware,
timeMiddleware,
loggerMiddleware
)(createStore)
const store = newCreateStore(reducer, initState);
實現(xiàn) Redux 中的 applyMiddleware
function applyMiddleware(...middlewares) {
return (createStore) => (...args) => {
let store = createStore(...args)
/*
給每一個 middleware 傳入store
*/
let chain = middlewares.map(middleware => middleware(store))
let dispatch = store.dispatch
/* 依次嵌套調(diào)用 */
chain.reverse().map(middleware => {
dispatch = middleware(dispatch)
})
/* 重寫 store 中的 dispatch */
store.dispatch = dispatch
return store
}
}
但是這樣就出現(xiàn)了兩個 createStore 的方法,解決的方法是將我們的中間件作為 enhancers 傳入 createStore 中處理:
function createStore(reducer, preloadedState, enhancers) {
let state = preloadedState
let listeners = []
// 如果有 enhancers 就將 createStore 傳入生成重寫過 dispatch 的 store
if(enhancers) {
return enhancer(createStore)(reducer, preloadedState)
}
// do something
}
OK, 目前為止我們的狀態(tài)管理器已經(jīng)把 Redux 中的 applyMiddleware 也是現(xiàn)實了。
最終中間件使用方法已經(jīng)和 Redux 一樣了:
const enhancers = applyMiddleware(
exceptionMiddleware,
timeMiddleware,
loggerMiddleware
)
const store = newCreateStore(reducer, initState, enhancer)
8.退訂
修改一下 subscribe,增加退訂的方法
function subscribe(listener) {
listeners.push(listener)
return function unsubscribe() {
const index = listeners.indexOf(listener);
listeners.splice(index, 1)
}
}
使用:
const unsubscribe = store.subscribe(() => {
let state = store.getState()
console.log(state.counter.count)
})
/*退訂*/
unsubscribe()
9. 實現(xiàn)compose 方法
我們的 applyMiddleware 中,把 [A, B, C] 轉(zhuǎn)換成 A(B(C(next))),是這樣實現(xiàn)的
let dispatch = store.dispatch
/* 依次嵌套調(diào)用 */
chain.reverse().map(middleware => {
dispatch = middleware(dispatch)
})
在 Redux 中實際上還有一個 compose 方法可以將中間件組合:
export default function compose(...funcs) {
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
10. 按需加載reducer
reducer 做了拆分之后,就可以和 UI 組件一樣做按需加載,用新的 reducer 替換舊的 reducer:
const createStore = function(reducer, initState) {
function replaceReducer(nextReducer) {
reducer = nextReducer
/*刷新一遍 state 的值,新來的 reducer 把自己的默認狀態(tài)放到 state 樹上去*/
dispatch({ type: Symbol() })
}
//其他代碼
return {
...replaceReducer
}
}
使用的例子:
const reducer = combineReducers({
counter: counterReducer
})
const store = createStore(reducer)
/*生成新的reducer*/
const nextReducer = combineReducers({
counter: counterReducer,
info: infoReducer
});
/*replaceReducer*/
store.replaceReducer(nextReducer)
11. 實現(xiàn) bindActionCreators
bindActionCreators 的作用是通過閉包將 dispatch 和 createStore 方法隱藏,讓其他調(diào)用 action 的地方感知不到內(nèi)部的操作,簡單的實現(xiàn)是這樣的:
const reducer = combineReducers({
counter: counterReducer,
info: infoReducer
});
const store = createStore(reducer);
/*返回 action 的函數(shù)就叫 actionCreator*/
function increment() {
return {
type: "INCREMENT"
}
}
function setName(name) {
return {
type: "SET_NAME",
name: name
}
}
const actions = {
increment: function() {
return store.dispatch(increment.apply(this, arguments))
},
setName: function() {
return store.dispatch(setName.apply(this, arguments))
}
}
/*其他引用action的地方,無需知道 dispatch,actionCreator等細節(jié)*/
actions.increment() /*自增*/
actions.setName("Cheuk") /*修改 info.name*/
接下來提取公共代碼:
function bindActionCreator(actionCreator, dispatch) {
return function() {
return dispatch(actionCreator.apply(this, arguments))
}
}
export default function bindActionCreators(actionCreators, dispatch) {
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
if (typeof actionCreators !== 'object' || actionCreators === null) {
throw new Error()
}
const boundActionCreators = {}
for (const key in actionCreators) {
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}
小結(jié)
目前為止, 我們的狀態(tài)管理器已經(jīng)把Redux 中的 API 就已經(jīng)都實現(xiàn)了一遍。Redux 的源碼不多,經(jīng)過自己手寫了一遍之后再多讀幾次,就能對 Redux 的工作流程有更深刻的理解。在第一次讀的時候可能一頭霧水,當理解了每個API 的用途并且自己寫幾次,大概就能理解作者的意圖了。互勉!