Reading Redux Source Code:深入理解 Redux store 結構與更新機制

Published on

「Redux 的 store 到底怎麼是怎麼設計的?」、「Redux 的 reducer 是怎麼更新 state?」,自己雖然已經會使用 Redux,但對於 Redux 本身的資料流還想理解地更徹底,所以就來讀讀原始碼來理解 Redux 怎麼實作。

Redux 的 lifecycle:嚴格單向資料流(strict unidirectional data flow)

在 Redux 的 tutorial 中,有提到 Redux 的嚴格單向資料流 lifecycle 有這幾個步驟:

  1. call store.dispatch(action).
  2. The Redux store calls the reducer function you gave it.
  3. The root reducer may combine the output of multiple reducers into a single state tree.
  4. The Redux store saves the complete state tree returned by the root reducer.

以下就從 redux 原始程式碼和較簡單的 todomvc 範例(在 Github 的 redux repo 裡有打包了)來一步一步詳細拆解這個過程。如此將更瞭解「store 是什麼」這個基本問題,也就可以更清楚「dispatch action」和「reducer 會去更新 store」這些動作究竟是怎麼發生的。

1. call store.dispatch(action)

顯然 dispatch 是物件 store 的一個方法。要看 dispatch 這個方法是什麼,當然就要去 store 物件裡面找。我們用 redux/examples 底下的 todomvc 案例來看:

// redux/examples/todomvc/src/index.js
import React from "react";
import { render } from "react-dom";
import { createStore } from "redux";
import { Provider } from "react-redux";
import App from "./containers/App";
import reducer from "./reducers";
import "todomvc-app-css/index.css";
const store = createStore(reducer);
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root"),
);

看到 store = createStore(reducer) ,非常簡單。那再來看呼叫 createStore() 回傳的東西是什麼,就知道我們的 store 是什麼:

// redux/src/createStore.js
export default function createStore(reducer, preloadedState, enhancer) {
// ...... something else ......
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable,
};
}

呼叫 createStore() 就只是 return 一個有四個 properties 的物件,這就是我們的 store。

2. The Redux store calls the reducer function you gave it.

那再來看 dispatch 這個 property 是什麼:

// redux/src/createStore.js
export default function createStore(reducer, preloadedState, enhancer) {
var currentReducer = reducer;
var currentState = preloadedState;
var currentListeners = [];
var nextListeners = currentListeners;
var isDispatching = false;
/* ...... something else ...... */
function dispatch(action) {
/* ...... some if-else checker ...... */
try {
isDispatching = true;
currentState = currentReducer(currentState, action);
} finally {
isDispatching = false;
}
var listeners = (currentListeners = nextListeners);
for (var i = 0; i < listeners.length; i++) {
var listener = listeners[i];
listener();
}
return action;
}
/* ...... something else ...... */
}

重點在此:

currentState = currentReducer(currentState, action);

這邊呼叫的 currentReducer() 就是我們在創建 store 時傳進 createStore() 的參數 reducer(也是一個函式)。

3. The root reducer may combine the output of multiple reducers into a single state tree.

讓我們繼續追蹤在 store.dispatch() 裡面呼叫的 currentReducer(),也就是傳進 createStore() 的參數 reducer 是什麼。

// redux/examples/todomvc/src/reducers/index.js
import { combineReducers } from "redux";
import todos from "./todos";
const rootReducer = combineReducers({
todos,
});
export default rootReducer;

上面這邊可以發現,我們傳進 createStore() 的參數,是一個變數 rootReducer,他的值是呼叫 combineReducers(ruducers) 的回傳值。

那繼續沿著這個線索追蹤,看看 combineReducers() 的回傳值是什麼:

// redux/src/combineReducers.js
export default function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers);
const finalReducers = {};
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i];
if (NODE_ENV !== "production") {
if (typeof reducers[key] === "undefined") {
warning(`No reducer provided for key "${key}"`);
}
}
if (typeof reducers[key] === "function") {
finalReducers[key] = reducers[key];
}
}
const finalReducerKeys = Object.keys(finalReducers);
/* ...... some checker ...... */
return function combination(state = {}, action) {
/* ...... some checker ...... */
var hasChanged = false;
var nextState = {};
for (var i = 0; i < finalReducerKeys.length; i++) {
var key = finalReducerKeys[i];
var reducer = finalReducers[key];
var previousStateForKey = state[key];
var nextStateForKey = reducer(previousStateForKey, action);
if (typeof nextStateForKey === "undefined") {
var errorMessage = getUndefinedStateErrorMessage(key, action);
throw new Error(errorMessage);
}
nextState[key] = nextStateForKey;
hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
}
return hasChanged ? nextState : state;
};
}

這邊可以看到,回傳的 combination(state, action) 這個函式就是會將 reducers object 裡面的每一個 reducer 跑過一遍,最後回傳一個合併各個「sub-reducer 回傳之 sub-state」的「總合大 state object」。

在這裡也可以看出,每個個別 sub-reducer 回傳的 sub-state 的名稱是怎麼來的。State 裡面每個 sub-state 的 key 就是來自傳進 combineReducers(reducers) 的 參數 reducers object 的 key。

舉例來說,如果 reducers 長這樣:

reducers = {
aooo, // <- a reducer function
booo, // <- a reducer function
cooo, // <- a reducer function
};

回傳的總和 state 就會長這樣:

{
aooo: { ... }, // <- key 是 reducers 內存放 reducer 函式的變數名 aooo(也就是 reducers 內的 key),value 是呼叫 reducer aooo() 回傳的 state
booo: { ... }, // 同上
cooo: { ... } // 同上
}

4. The Redux store saves the complete state tree returned by the root reducer.

回到 store.dispatch 看:

// redux/src/createStore.js
export default function createStore(reducer, preloadedState, enhancer) {
var currentReducer = reducer;
var currentState = preloadedState;
var currentListeners = [];
var nextListeners = currentListeners;
var isDispatching = false;
/* ...... something else ...... */
function dispatch(action) {
/* ...... some if-else checker ...... */
try {
isDispatching = true;
currentState = currentReducer(currentState, action);
} finally {
isDispatching = false;
}
var listeners = (currentListeners = nextListeners);
for (var i = 0; i < listeners.length; i++) {
var listener = listeners[i];
listener();
}
return action;
}
// ...... something else ......
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable,
};
}

上面的 code 可以看到在 dispatch(action){ ... } 這個函式實字裡面,用到上層函式的 currentStatecurrentReducercurrentListenersnextListenersisDispatching 等變數,這些變數都是在 dispatch 所在的上層函式 createStore 內宣告的區域變數。

因為在執行 createStore() return 的 object 裡面的方法會用到這些變數,所以即使 createStore() 執行完,這些區域變數也會被包在 closure 裡面繼續存在。

我們透過呼叫 dispatch() 方法,就會呼叫暫存(傳址?)在 currentReducer(也在 closure 內)的 rootReducer 函式,用它的回傳值更新存在 closure 裡面的 currentState 變數,這個變數也就是一般俗稱的 store 裡面的唯一的 state tree object。

而要讀取例如 currentState 這個存在於 createStore 函式的 closure 裡面的區域變數,就必須要透過被 return 出來的取值方法,例如 getState() 才讀得到。


After the store state has been updated…

另外,在上面 dispatch 方法的最後我們還有看到這段:

var listeners = (currentListeners = nextListeners); // assign from right to left
for (var i = 0; i < listeners.length; i++) {
var listener = listeners[i];
listener();
}

這會把存在 closure 的變數 nextListeners 裡面的 listener 函式全部跑過一遍。

而要把 listener 存到 nextListeners 變數裡面,則是透過 store.subscribe 方法進行。(原始碼同樣參見 redux/src/createStore.js

而如果是搭配 React,通常則會用 React Redux library 裡的 connect() 函式來進行:

技術上來說,一個 container component 就只是個使用 store.subscribe() 來讀取一部份 Redux state tree 並提供 props 讓 presentational component 來 render 用的 React component。你可以手寫一個 container component,但 React Redux 囊括了許多有用的優化,所以我們建議從 React Redux library 使用 connect() function 來產生 container component,它提供了許多有用的最佳化來避免不必要的重新 render。(其中一個結果就是,你不必擔心關於 React 的效能建議而自己實作 shouldComponentUpdate。)

Technically, a container component is just a React component that uses store.subscribe() to read a part of the Redux state tree and supply props to a presentational component it renders. You could write a container component by hand, but we suggest instead generating container components with the React Redux library’s connect() function, which provides many useful optimizations to prevent unnecessary re-renders. (One result of this is that you shouldn’t have to worry about the React performance suggestion of implementing shouldComponentUpdate yourself.)

http://redux.js.org/docs/basics/UsageWithReact.html#implementing-container-components

關於從 redux store 傳遞資料到 react props 還有一些進階技巧,如:

萬一你擔心 mapStateToProps 會太常建立新的 object,可以參考 reselect