手把手教你擼一套Redux(Redux原始碼解讀)
阿新 • • 發佈:2020-05-12
Redux 版本:3.7.2
> Redux 是 JavaScript 狀態容器,提供可預測化的狀態管理。
說白了Redux就是一個數據儲存工具,所以資料基礎模型有get方法,set方法以及資料改變後通知的物件subscribe訂閱者。
* getState: getter(取)
* dispatch: setter(存)
* subscribe: 訂閱
Redux 提供了五個方法
* createStore
* combineReducers
* bindActionCreators
* compose
* applyMiddleware
接下來我們來一一解析。
## createStore > 建立一個 Redux store 來以存放應用中所有的 state。應用中應有且僅有一個 store。 引數: * reducer (Function): 接收兩個引數,分別是當前的 state 樹和要處理的 action,返回新的 state 樹。 * [ reloadedState ] (any):初始時的 state。 * enhancer (Function):後面再講。 返回值: * getState:獲取store方法 * dispatch:修改store方法 * subscribe:訂閱store變化方法 * replaceReducer:重置reducer方法 先來寫一個基礎的 createStore 如下: ```js function createStore() { function getState() { } // 取 function dispatch() { } // 存 function subscribe() { } // 訂閱 function replaceReducer() { } // 重置reducer return { getState, dispatch, subscribe, replaceReducer } } ``` ### getState getState 實現很簡單,直接返回 currentState。 ```js function createStore() { let currentState = {}; // 資料 function getState() { // 取 return currentState; } function dispatch() { } // 存 function subscribe() { } // 訂閱 function replaceReducer() { } // 重置reducer return { getState, dispatch, subscribe, replaceReducer } } ``` ### dispatch dispatch 傳入 action,通過 action.type 區別操作。 ```js function createStore() { let currentState = {}; function getState() { // 取 return currentState; } function dispatch(action) { // 存 switch (action.type) { case 'PLUS': currentState = { ...currentState, count: currentState.count + 1, }; } return action; } function subscribe() { } // 訂閱 function replaceReducer() { } // 重置reducer return { getState, dispatch, subscribe, replaceReducer } } ``` 因為 Redux 要通用,所以 dispatch 內和業務相關的程式碼要提取出來,Redux 給它起了個名字,叫 reducer。 提取reducer, ```js const initialState = { count: 0, } export default (state = initialState, action) => { switch (action.type) { case 'PLUS': return { ...state, count: state.count + 1, } case 'MINUS': return { ...state, count: state.count - 1, } default: return state } } ``` 給 createStore 新增兩個引數 reducer, preloadedState。 preloadedState非必傳,如果不傳,currentState 預設值就是 undefined。 在 createStore 中新增初始化方法 ; 初始化的 action.type 必須是 reducer 中沒有使用過的,Redux 原始碼中使用了 。
注意:實際上,replaceReducer 中的
## combineReducers 隨著專案越來越大,把 reducer 放在一個檔案裡寫會越來越臃腫,於是 Redux 提供了 combineReducers 方法。 先來看下如何使用 ```js rootReducer = combineReducers({potato: potatoReducer, tomato: tomatoReducer}) // rootReducer 將返回如下的 state 物件 { potato: { // ... potatoes, 和一些其他由 potatoReducer 管理的 state 物件 ... }, tomato: { // ... tomatoes, 和一些其他由 tomatoReducer 管理的 state 物件,比如說 sauce 屬性 ... } } ``` combineReducers 引數是 reducers 物件,返回一個合成後的 reducer。 實現邏輯比較簡單,迴圈把 reducers 裡的每一個 reducer 都執行, 執行結果放在 nextState 裡,如果資料改變了就返回 nextState,如果資料沒有改變就返回傳入的 state。 注意:如果資料沒有改變,返回的是傳入的 state,雖然此時和 nextState 資料是一樣的,但是實際地址並不一樣。為了區分,Redux 特意用了 hasChanged 變數來記錄。 ```js function combineReducers(reducers) { const reducerKeys = Object.keys(reducers); // key[] return function combination(state = {}, action) { let hasChanged = false; // state 是否改變 const nextState = {}; // 改變後的 state // 迴圈 reducers reducerKeys.forEach(key => { const reducer = reducers[key]; // 當前 reducer const previousStateForKey = state[key]; // 當前 state const nextStateForKey = reducer(previousStateForKey, action); // 如果 沒有匹配到action.type,會在 reducer 中的 switch default 返回傳入的 state,即 previousStateForKey nextState[key] = nextStateForKey; hasChanged = hasChanged || nextStateForKey !== previousStateForKey; }) return hasChanged ? nextState : state; } } ```
## bindActionCreators bindActionCreators(actionCreators, dispatch) 把一個 value 為不同 action creator 的物件,轉成擁有同名 key 的物件。 action 生成器名字叫做叫 action creator, 如下 ```js function addTodo(text) { return { type: 'ADD_TODO', text, }; } ``` 修改資料需要這樣寫 ```js dispatch(addTodo('Use Redux')) ``` 如果我們多個 action creator,寫起來會比較繁瑣, ```js dispatch(addTodo('Use Redux')) dispatch(plusTodo()) dispatch(setDataTodo({ id: 1 })) ``` 所以 Redux 提供了 bindActionCreators 函式,傳入 action creators 和 dispatch, 返回綁定了 dispatch 的 action creators。 實現也很簡單,遍歷 actionCreators, 把每個元素用 dispatch 處理後生成新的函式,返回新函式的集合。 actionCreators 引數是 action creator 的集合物件,如 。實現程式碼如下:
```js
function bindActionCreators(actionCreators, dispatch) {
const boundActionCreators = {};
Object.keys(actionCreators).forEach(key => {
const actionCreator = actionCreators[key];
if (typeof actionCreator === 'function') {
boundActionCreators[key] = (...args) => dispatch(actionCreator(...args));
}
})
return boundActionCreators;
}
```
使用 bindActionCreators 寫起來就會方便很多
```js
const boundActionCreators = bindActionCreators({
addTodo,
plusTodo,
setDataTodo,
}, dispatch);
// 寫入資料
boundActionCreators.addTodo('Use Redux')
boundActionCreators.plusTodo()
boundActionCreators.addTodo({ id: 1 })
```
Redux 支援 actionCreators 是一個單個 action creator 的函式,所以提取公共方法。改造如下:
```js
function bindActionCreator(actionCreator, dispatch) {
return (...args) => dispatch(actionCreator(...args));
}
function bindActionCreators(actionCreators, dispatch) {
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
const boundActionCreators = {};
Object.keys(actionCreators).forEach(key => {
const actionCreator = actionCreators[key];
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch);
}
})
return boundActionCreators;
}
```
## compose 從右到左來組合多個函式。 先來看看原始碼: ```js 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))) } ``` 最後一行很難理解,把它換成function寫法如下 ```js funcs.reduce(function (a, b) { return function (...args) { return a(b(...args)) } }) ``` 先看下reduce方法 ```js reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): T; reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T; // 從左到右為每個陣列元素執行一次回撥函式,並把上次回撥函式的返回值放在一個暫存器中傳給下次回撥函式,並返回最後一次回撥函式的返回值。 ``` previousValue 上次迴圈的返回值 currentValue 當前迴圈item 所以第二次迴圈過程如下 ```js // 第一次迴圈返回值為 function (...args) { return a(b(...args)) } // 第二次迴圈時,第一個引數為:第一次迴圈的返回值,第二個引數為:funcs 內第三個元素,用c來表示 // 第二次迴圈返回值為 function (...args) { return (function (...args) { return a(b(...args)) })(c(...args)) } // 整理後 function (...args) { return a(b(c(...args))) } ``` 所以
## applyMiddleware applyMiddleware 是把 dispatch 一層一層包裝。洋蔥圈模型。 先看看 createStore 的第三個引數 enhancer ```js function createStore(reducer, preloadedState, enhancer) { // 實現了 preloadedState 引數可以省略 if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { enhancer = preloadedState preloadedState = undefined } if (typeof enhancer !== 'undefined') { // 看起來 enhancer 是個高階函式,返回值還是 store creator // 可以看出 enhancer 的大概結構為 // (createStore) => (reducer, preloadedState) => createStore(educer, preloadedState) return enhancer(createStore)(reducer, preloadedState) } // 這裡是其他程式碼 // ... } ``` 再看看官網給的 applyMiddleware 使用例子 ```js let store = createStore( todos, [ 'Use Redux' ], applyMiddleware(logger) ) ``` 所以 applyMiddleware 的結構應該是 ```js (...middlewares) => (createStore) => (reducer, preloadedState) => createStore(educer, preloadedState) ``` 所以猜出來了 applyMiddleware 的引數是函式,返回值執行多次後還是 createStore(educer, preloadedState)。 所以再來看官方定義就比較好理解 > Middleware 可以讓你包裝 store 的 dispatch 方法來達到你想要的目的。同時, middleware 還擁有“可組合”這一關鍵特性。多個 middleware 可以被組合到一起使用,形成 middleware 鏈。其中,每個 middleware 都不需要關心鏈中它前後的 middleware 的任何資訊。 來看 applyMiddleware 看原始碼, 跟著 序號看會稍微清晰點: ```js applyMiddleware(...middlewares) { return (createStore) => (reducer, preloadedState, enhancer) => { const store = createStore(reducer, preloadedState, enhancer) let dispatch = store.dispatch let chain = [] const middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) } // 2、chain內元素結構為 (store.dispatch) => store.dispatch // 所以 middleware(middlewareAPI) 結果為 (store.dispatch) => store.dispatch // 所以 middleware 結構為 (middlewareAPI) => (store.dispatch) => store.dispatch // 即 引數 middlewares 內元素結構為 (middlewareAPI) => (store.dispatch) => store.dispatch chain = middlewares.map(middleware => middleware(middlewareAPI)) // 1、上面解釋過 compose 的返回值是 (...arg) => a(b(c(...arg))), // 所以下面 dispatch = ((...arg) => a(b(c(...arg))))(store.dispatch) // 即 dispatch = a(b(c(store.dispatch))) // 所以 a、b、c 即 chain內元素 的結構需要為 (store.dispatch) => store.dispatch dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch // 這裡可以看出,applyMiddleware 只包裝替換了 createStore 的 dispatch } } } ``` 現在我們知道了 applyMiddleware 的引數結構是
## createStore > 建立一個 Redux store 來以存放應用中所有的 state。應用中應有且僅有一個 store。 引數: * reducer (Function): 接收兩個引數,分別是當前的 state 樹和要處理的 action,返回新的 state 樹。 * [ reloadedState ] (any):初始時的 state。 * enhancer (Function):後面再講。 返回值: * getState:獲取store方法 * dispatch:修改store方法 * subscribe:訂閱store變化方法 * replaceReducer:重置reducer方法 先來寫一個基礎的 createStore 如下: ```js function createStore() { function getState() { } // 取 function dispatch() { } // 存 function subscribe() { } // 訂閱 function replaceReducer() { } // 重置reducer return { getState, dispatch, subscribe, replaceReducer } } ``` ### getState getState 實現很簡單,直接返回 currentState。 ```js function createStore() { let currentState = {}; // 資料 function getState() { // 取 return currentState; } function dispatch() { } // 存 function subscribe() { } // 訂閱 function replaceReducer() { } // 重置reducer return { getState, dispatch, subscribe, replaceReducer } } ``` ### dispatch dispatch 傳入 action,通過 action.type 區別操作。 ```js function createStore() { let currentState = {}; function getState() { // 取 return currentState; } function dispatch(action) { // 存 switch (action.type) { case 'PLUS': currentState = { ...currentState, count: currentState.count + 1, }; } return action; } function subscribe() { } // 訂閱 function replaceReducer() { } // 重置reducer return { getState, dispatch, subscribe, replaceReducer } } ``` 因為 Redux 要通用,所以 dispatch 內和業務相關的程式碼要提取出來,Redux 給它起了個名字,叫 reducer。 提取reducer, ```js const initialState = { count: 0, } export default (state = initialState, action) => { switch (action.type) { case 'PLUS': return { ...state, count: state.count + 1, } case 'MINUS': return { ...state, count: state.count - 1, } default: return state } } ``` 給 createStore 新增兩個引數 reducer, preloadedState。 preloadedState非必傳,如果不傳,currentState 預設值就是 undefined。 在 createStore 中新增初始化方法
dispatch({ type: '@@redux/INIT' })
'@@redux/INIT'
。初始化方法會執行一次 dispatch。
初始化時,如果 currentState 是 undefined, 那麼在 reducer 中, state = initialState
會把 initialState 賦值給 state,然後通過 default return 出去, 最後修改 currentState。相當於 currentState = initialState。
最後 createStore 如下
```js
function createStore(reducer, preloadedState) {
let currentState = preloadedState;
function getState() { // 取
return currentState;
}
function dispatch(action) { // 存
currentState = reducer(currentState, action);
return action;
}
function subscribe() { } // 訂閱
function replaceReducer() { } // 重置reducer
dispatch({ type: '@@redux/INIT' }); // 初始化
return { getState, dispatch, subscribe, replaceReducer }
}
```
根據程式碼可以看出,reducer 和 action 都是開發者自定義的,Redux 只是把 reducer 返回的 state 賦值給了 currentState,那麼開發者自定義其他格式的action ,並且在 reducer 中作出對應的解析,然後返回 state,當然也是完全可以的。只是 Redux 統一了這種寫法,降低了個性化帶來的開發成本。
實際上 createStore 還有第三個引數 enhancer,目前用不到,後面再講。
### subscribe
subscribe 有一個引數 listener (Function): 每當 dispatch action 的時候都會執行的回撥。
subscribe 使用了設計模式中的 釋出-訂閱模式,又叫 觀察者模式。
實現:
* 在 createStore 中新增一個儲存 變化監聽器 的陣列 currentListeners;
* subscribe 將 變化監聽器 放入 currentListeners;
* 每次 dispatch 時, 迴圈執行 currentListeners 中的 變化監聽器。
```js
function createStore(reducer, preloadedState) {
let currentState = preloadedState;
let currentListeners = [];
function getState() { // 取
return currentState;
}
function dispatch(action) { // 存
currentState = reducer(currentState, action);
currentListeners.forEach(fn => fn());
return action;
}
function subscribe(listener) { // 訂閱
currentListeners.push(listener);
}
function replaceReducer() { } // 重置reducer
dispatch({ type: '@@redux/INIT' }); // 初始化
return { getState, dispatch, subscribe, replaceReducer }
}
```
### replaceReducer
重置 reducer, 並不會重置 currentState。
實現:
* 新增變數 currentReducer;
* dispatch 使用 currentReducer;
* replaceReducer 方法將 nextReducer 賦值給 replaceReducer, 然後執行 dispatch({ type: '@@redux/INIT' })
dispatch({ type: '@@redux/INIT' })
,只有此時 currentState 是 undefined 時,才有作用,會把新的 initialState 賦值給 currentState。
```js
function createStore(reducer, preloadedState) {
let currentReducer = reducer
let currentState = preloadedState;
let currentListeners = [];
function getState() { // 取
return currentState;
}
function dispatch(action) { // 存
currentState = currentReducer(currentState, action);
currentListeners.forEach(fn => fn());
return action;
}
function subscribe(listener) { // 釋出訂閱
currentListeners.push(listener);
}
function replaceReducer(nextReducer) { // 重置reducer
currentReducer = nextReducer;
dispatch({ type: '@@redux/INIT' }); // 重置
}
dispatch({ type: '@@redux/INIT' }); // 初始化
return { getState, dispatch, subscribe, replaceReducer }
}
```
createStore 的實現到這裡已經完成,Redux 原始碼除此之外還做了大量的錯誤校驗。
## combineReducers 隨著專案越來越大,把 reducer 放在一個檔案裡寫會越來越臃腫,於是 Redux 提供了 combineReducers 方法。 先來看下如何使用 ```js rootReducer = combineReducers({potato: potatoReducer, tomato: tomatoReducer}) // rootReducer 將返回如下的 state 物件 { potato: { // ... potatoes, 和一些其他由 potatoReducer 管理的 state 物件 ... }, tomato: { // ... tomatoes, 和一些其他由 tomatoReducer 管理的 state 物件,比如說 sauce 屬性 ... } } ``` combineReducers 引數是 reducers 物件,返回一個合成後的 reducer。 實現邏輯比較簡單,迴圈把 reducers 裡的每一個 reducer 都執行, 執行結果放在 nextState 裡,如果資料改變了就返回 nextState,如果資料沒有改變就返回傳入的 state。 注意:如果資料沒有改變,返回的是傳入的 state,雖然此時和 nextState 資料是一樣的,但是實際地址並不一樣。為了區分,Redux 特意用了 hasChanged 變數來記錄。 ```js function combineReducers(reducers) { const reducerKeys = Object.keys(reducers); // key[] return function combination(state = {}, action) { let hasChanged = false; // state 是否改變 const nextState = {}; // 改變後的 state // 迴圈 reducers reducerKeys.forEach(key => { const reducer = reducers[key]; // 當前 reducer const previousStateForKey = state[key]; // 當前 state const nextStateForKey = reducer(previousStateForKey, action); // 如果 沒有匹配到action.type,會在 reducer 中的 switch default 返回傳入的 state,即 previousStateForKey nextState[key] = nextStateForKey; hasChanged = hasChanged || nextStateForKey !== previousStateForKey; }) return hasChanged ? nextState : state; } } ```
## bindActionCreators bindActionCreators(actionCreators, dispatch) 把一個 value 為不同 action creator 的物件,轉成擁有同名 key 的物件。 action 生成器名字叫做叫 action creator, 如下 ```js function addTodo(text) { return { type: 'ADD_TODO', text, }; } ``` 修改資料需要這樣寫 ```js dispatch(addTodo('Use Redux')) ``` 如果我們多個 action creator,寫起來會比較繁瑣, ```js dispatch(addTodo('Use Redux')) dispatch(plusTodo()) dispatch(setDataTodo({ id: 1 })) ``` 所以 Redux 提供了 bindActionCreators 函式,傳入 action creators 和 dispatch, 返回綁定了 dispatch 的 action creators。 實現也很簡單,遍歷 actionCreators, 把每個元素用 dispatch 處理後生成新的函式,返回新函式的集合。 actionCreators 引數是 action creator 的集合物件,如
{ addTodo, addTodo1 }
## compose 從右到左來組合多個函式。 先來看看原始碼: ```js 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))) } ``` 最後一行很難理解,把它換成function寫法如下 ```js funcs.reduce(function (a, b) { return function (...args) { return a(b(...args)) } }) ``` 先看下reduce方法 ```js reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): T; reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T; // 從左到右為每個陣列元素執行一次回撥函式,並把上次回撥函式的返回值放在一個暫存器中傳給下次回撥函式,並返回最後一次回撥函式的返回值。 ``` previousValue 上次迴圈的返回值 currentValue 當前迴圈item 所以第二次迴圈過程如下 ```js // 第一次迴圈返回值為 function (...args) { return a(b(...args)) } // 第二次迴圈時,第一個引數為:第一次迴圈的返回值,第二個引數為:funcs 內第三個元素,用c來表示 // 第二次迴圈返回值為 function (...args) { return (function (...args) { return a(b(...args)) })(c(...args)) } // 整理後 function (...args) { return a(b(c(...args))) } ``` 所以
[a, b, c, d, e]
的執行結果是 (...args) => a(b(c(d(e(...args)))))
。
所以能看出來,funcs 內函式需要滿足 函式引數和函式返回值結構一致。
## applyMiddleware applyMiddleware 是把 dispatch 一層一層包裝。洋蔥圈模型。 先看看 createStore 的第三個引數 enhancer ```js function createStore(reducer, preloadedState, enhancer) { // 實現了 preloadedState 引數可以省略 if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { enhancer = preloadedState preloadedState = undefined } if (typeof enhancer !== 'undefined') { // 看起來 enhancer 是個高階函式,返回值還是 store creator // 可以看出 enhancer 的大概結構為 // (createStore) => (reducer, preloadedState) => createStore(educer, preloadedState) return enhancer(createStore)(reducer, preloadedState) } // 這裡是其他程式碼 // ... } ``` 再看看官網給的 applyMiddleware 使用例子 ```js let store = createStore( todos, [ 'Use Redux' ], applyMiddleware(logger) ) ``` 所以 applyMiddleware 的結構應該是 ```js (...middlewares) => (createStore) => (reducer, preloadedState) => createStore(educer, preloadedState) ``` 所以猜出來了 applyMiddleware 的引數是函式,返回值執行多次後還是 createStore(educer, preloadedState)。 所以再來看官方定義就比較好理解 > Middleware 可以讓你包裝 store 的 dispatch 方法來達到你想要的目的。同時, middleware 還擁有“可組合”這一關鍵特性。多個 middleware 可以被組合到一起使用,形成 middleware 鏈。其中,每個 middleware 都不需要關心鏈中它前後的 middleware 的任何資訊。 來看 applyMiddleware 看原始碼, 跟著 序號看會稍微清晰點: ```js applyMiddleware(...middlewares) { return (createStore) => (reducer, preloadedState, enhancer) => { const store = createStore(reducer, preloadedState, enhancer) let dispatch = store.dispatch let chain = [] const middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) } // 2、chain內元素結構為 (store.dispatch) => store.dispatch // 所以 middleware(middlewareAPI) 結果為 (store.dispatch) => store.dispatch // 所以 middleware 結構為 (middlewareAPI) => (store.dispatch) => store.dispatch // 即 引數 middlewares 內元素結構為 (middlewareAPI) => (store.dispatch) => store.dispatch chain = middlewares.map(middleware => middleware(middlewareAPI)) // 1、上面解釋過 compose 的返回值是 (...arg) => a(b(c(...arg))), // 所以下面 dispatch = ((...arg) => a(b(c(...arg))))(store.dispatch) // 即 dispatch = a(b(c(store.dispatch))) // 所以 a、b、c 即 chain內元素 的結構需要為 (store.dispatch) => store.dispatch dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch // 這裡可以看出,applyMiddleware 只包裝替換了 createStore 的 dispatch } } } ``` 現在我們知道了 applyMiddleware 的引數結構是
(middlewareAPI) => (store.dispatch) => store.dispatch
,然後我們來寫個簡單的 middleware
```js
// 原始長這個樣子
function logger(middlewareAPI) {
return (dispatch) => dispatch;
}
// 然後 給 dispatch 包裝以下,並且換個名字叫 next
function logger(middlewareAPI) {
return (next) => (action) => {
let value = next(action);
return value;
};
}
// 然後 加入功能
function logger(middlewareAPI) {
return (next) => (action) => {
// 這裡的 dispatch 是 createStore 建立的。一般不用。
const { getState, dispatch } = middlewareAPI;
console.log('will dispatch', action);
let value = next(action);
console.log('state after dispatch', getState());
// createStore 裡實現的 dispatch 返回 action,
// 一般會是 action 本身,除非
// 後面的 middleware 修改了它。
return value;
};
}
```
最後再來回味下 applyMiddleware 的這幾個結構
```js
// compose
([a, b, c, d, e]) => (...args) => a(b(c(d(e(...args)))))
// applyMiddleware
(...middlewares) => (createStore) => (reducer, preloadedState) => createStore(educer, preloadedState)
// middleware
(middlewareAPI) => (dispatch) => dispatch
``