【React】717- 從零實(shí)現(xiàn) React-Redux
1. 前言
在 React 誕生之初,F(xiàn)acebook 宣傳這是一個(gè)用于前端開發(fā)的界面庫,僅僅是一個(gè) View 層。前面我們也介紹過 React 的組件通信,在大型應(yīng)用中,處理好 React 組件通信和狀態(tài)管理就顯得非常重要。為了解決這一問題,F(xiàn)acebook 最先提出了單向數(shù)據(jù)流的 Flux 架構(gòu),彌補(bǔ)了使用 React 開發(fā)大型網(wǎng)站的不足。
Flux:
隨后,Dan Abramov 受到 Flux 和函數(shù)式編程語言 Elm 啟發(fā),開發(fā)了 Redux 這個(gè)狀態(tài)管理庫。Redux 源碼非常精簡,實(shí)現(xiàn)也很巧妙,這篇文章將帶你從零手寫一個(gè) Redux 和 react-redux 庫,以及告訴你該如何設(shè)計(jì) Redux 中的 store。在開始前,我已經(jīng)將這篇文章的完整代碼都整理到 GitHub 上,大家可以參考一下。
Redux:simple-redux
React-redux:simple-react-redux
2. 狀態(tài)管理
2.1 理解數(shù)據(jù)驅(qū)動(dòng)
在開始講解狀態(tài)管理前,我們先來了解一下現(xiàn)代前端框架都做了些什么。以 Vue 為例子,在剛開始的時(shí)候,Vue 官網(wǎng)首頁寫的賣點(diǎn)是數(shù)據(jù)驅(qū)動(dòng)、組件化、MVVM 等等(現(xiàn)在首頁已經(jīng)改版了)。那么數(shù)據(jù)驅(qū)動(dòng)的意思是什么呢?不管是原生 JS 還是 jQuery,他們都是通過直接修改 DOM 的形式來實(shí)現(xiàn)頁面刷新的。而 Vue/React 之類的框架不是粗暴地直接修改 DOM,而是通過修改 data/state 中的數(shù)據(jù),實(shí)現(xiàn)了組件的重新渲染。也就是說,他們封裝了從數(shù)據(jù)變化到組件渲染這一個(gè)過程。

原本我們用 jQuery 開發(fā)應(yīng)用,除了要實(shí)現(xiàn)業(yè)務(wù)邏輯,還要操作 DOM 來手動(dòng)實(shí)現(xiàn)頁面的更新。尤其是涉及到渲染列表的時(shí)候,更新起來非常麻煩。
var ul = document.getElementById("todo-list");$.each(todos, function(index, todo) {var li = document.createElement('li');li.innerHTML = todo.content;li.dataset.id = todo.id;li.className = "todo-item";ul.appendChild(li);})
所以后來出現(xiàn)了 jQuery.tpl 和 Underscore.template 之類的模板,這些讓操作 DOM 變得容易起來,有了數(shù)據(jù)驅(qū)動(dòng)和組件化的雛形,可惜我們還是要手動(dòng)去渲染一遍。
如果說用純原生 JS 或者 jQuery 開發(fā)頁面是原始農(nóng)耕時(shí)代,那么 React/Vue 等現(xiàn)代化框架則是自動(dòng)化的時(shí)代。有了前端框架之后,我們不需要再去關(guān)注怎么生成和修改 DOM,只需要關(guān)心頁面上的這些數(shù)據(jù)以及流動(dòng)。所以如何管理好這些數(shù)據(jù)流動(dòng)就成了重中之重,這也是我們常說的“狀態(tài)管理”。
2.2 什么狀態(tài)需要管理?
前面講了很多例子,可狀態(tài)管理到底要管理什么呢?在我看來,狀態(tài)管理的一般就是這兩種數(shù)據(jù)。
Domain State Domain State 就是服務(wù)端的狀態(tài),這個(gè)一般是指通過網(wǎng)絡(luò)請求來從服務(wù)端獲取到的數(shù)據(jù),比如列表數(shù)據(jù),通常是為了和服務(wù)端數(shù)據(jù)保持一致。
{"data": {"hotels": [{"id": "31231231","name": "希爾頓","price": "1300"}]}}
UI State UI State 常常和交互相關(guān)。例如模態(tài)框的開關(guān)狀態(tài)、頁面的 loading 狀態(tài)、單(多)選項(xiàng)的選中狀態(tài)等等,這些狀態(tài)常常分散在不同的組件里面。
{"isLoading": true,"isShowModal": false,"isSelected": false}
2.3 全局狀態(tài)管理
我們用 React 寫組件的時(shí)候,如果需要涉及到兄弟組件通信,經(jīng)常需要將狀態(tài)提升到兩者父組件里面。一旦這種組件通信多了起來,數(shù)據(jù)管理就是個(gè)問題。結(jié)合上面的例子,如果想要對應(yīng)用的數(shù)據(jù)流進(jìn)行管理,那是不是可以將所有的狀態(tài)放到頂層組件中呢?將數(shù)據(jù)按照功能或者組件來劃分,將多個(gè)組件共享的數(shù)據(jù)單獨(dú)放置,這樣就形成了一個(gè)大的樹形 store。這里更建議按照功能來劃分。

這個(gè)大的 store 可以放到頂層組件中維護(hù),也可以放到頂層組件之外來維護(hù),這個(gè)頂層組件我們一般稱之為“容器組件”。容器組件可以將組件依賴的數(shù)據(jù)以及修改數(shù)據(jù)的方法一層層傳給子組件。我們可以將容器組件的 state 按照組件來劃分,現(xiàn)在這個(gè) state 就是整個(gè)應(yīng)用的 store。將修改 state 的方法放到 actions 里面,按照和 state 一樣的結(jié)構(gòu)來組織,最后將其傳入各自對應(yīng)的子組件中。
class App extends Component {constructor(props) {this.state = {common: {},headerProps: {},bodyProps: {sidebarProps: {},cardProps: {},tableProps: {},modalProps: {}},footerProps: {}}this.actions = {header: {changeHeaderProps: this.changeHeaderProps},footer: {changeFooterProps: this.changeFooterProps},body: {sidebar: {changeSiderbarProps: this.changeSiderbarProps}}}}changeHeaderProps(props) {this.setState({headerProps: props})}changeFooterProps() {}changeSiderbarProps() {}...render() {const {headerProps,bodyProps,footerProps} = this.state;const {header,body,footer} = this.actions;return ()}}
我們可以看到,這種方式可以很完美地解決子組件之間的通信問題。只需要修改對應(yīng)的 state 就行了,App 組件會(huì)在 state 變化后重新渲染,子組件接收新的 props 后也跟著渲染。

這種模式還可以繼續(xù)做一些優(yōu)化,比如結(jié)合 Context 來實(shí)現(xiàn)向深層子組件傳遞數(shù)據(jù)。
const Context = createContext(null);class App extends Component {...render() {return ()}}const Header = () => {// 獲取到 Context 數(shù)據(jù)const context = useContext(Context);}
如果你已經(jīng)接觸過 Redux 這個(gè)狀態(tài)管理庫,你會(huì)驚奇地發(fā)現(xiàn),如果我們把 App 組件中的 state 移到外面,這不就是 Redux 了嗎?沒錯(cuò),Redux 的核心原理也是這樣,在組件外部維護(hù)一個(gè) store,在 store 修改的時(shí)候會(huì)通知所有被 connect 包裹的組件進(jìn)行更新。這個(gè)例子可以看做 Redux 的一個(gè)雛形。
3. 實(shí)現(xiàn)一個(gè) Redux
根據(jù)前面的介紹我們已經(jīng)知道了,Redux 是一個(gè)狀態(tài)管理庫,它并非綁定于 React 使用,你還可以將其和其他框架甚至原生 JS 一起使用,比如這篇文章:如何在非 React 項(xiàng)目中使用 Redux (https://segmentfault.com/a/1190000009963395)
Redux 工作原理:

在學(xué)習(xí) Redux 之前需要先理解其工作原理,一般來說流程是這樣的:
用戶觸發(fā)頁面上的某種操作,通過 dispatch 發(fā)送一個(gè) action。 Redux 接收到這個(gè) action 后通過 reducer 函數(shù)獲取到下一個(gè)狀態(tài)。 將新狀態(tài)更新進(jìn) store,store 更新后通知頁面重新渲染。
從這個(gè)流程中不難看出,Redux 的核心就是一個(gè)?發(fā)布-訂閱?模式。一旦 store 發(fā)生了變化就會(huì)通知所有的訂閱者,view 接收到通知之后會(huì)進(jìn)行重新渲染。
Redux 有三大原則:
單一數(shù)據(jù)源
前面的那個(gè)例子,最終將所有的狀態(tài)放到了頂層組件的 state 中,這個(gè) state 形成了一棵狀態(tài)樹。在 Redux 中,這個(gè) state 則是 store,一個(gè)應(yīng)用中一般只有一個(gè) store。
State 是只讀的
在 Redux 中,唯一改變 state 的方法是觸發(fā) action,action 描述了這次修改行為的相關(guān)信息。只允許通過 action 修改可以使應(yīng)用中的每個(gè)狀態(tài)修改都很清晰,便于后期的調(diào)試和回放。
通過純函數(shù)來修改
為了描述 action 使?fàn)顟B(tài)如何修改,需要你編寫 reducer 函數(shù)來修改狀態(tài)。reducer 函數(shù)接收前一次的 state 和 action,返回新的 state。無論被調(diào)用多少次,只要傳入相同的 state 和 action,那么就一定返回同樣的結(jié)果。
關(guān)于 Redux 的用法,這里不做詳細(xì)講解,建議參考阮一峰老師的《Redux 入門》系列的教程:Redux 入門教程
3.1 實(shí)現(xiàn) store
在 Redux 中,store 一般通過 createStore 來創(chuàng)建。
import { createStore } from 'redux';const store = createStore(rootReducer, initalStore, middleware);
先看一下 Redux 中暴露出來的幾個(gè)方法。

其中 createStore 返回的方法主要有?subscribe、dispatch、replaceReducer、getState。
createStore?接收三個(gè)參數(shù),分別是 reducers 函數(shù)、初始值 initalStore、中間件 middleware。
store?上掛載了?getState、dispatch、subscribe?三個(gè)方法。
getState?是獲取到 store 的方法,可以通過?store.getState()?獲取到?store。
dispatch?是發(fā)送 action 的方法,它接收一個(gè) action 對象,通知?store?去執(zhí)行 reducer 函數(shù)。
subscribe?則是一個(gè)監(jiān)聽方法,它可以監(jiān)聽到?store?的變化,所以可以通過?subscribe?將 Redux 和其他框架結(jié)合起來。
replaceReducer?用來異步注入 reducer 的方法,可以傳入新的 reducer 來代替當(dāng)前的 reducer。
3.2 實(shí)現(xiàn) getState
store 的實(shí)現(xiàn)原理比較簡單,就是根據(jù)傳入的初始值來創(chuàng)建一個(gè)對象。利用閉包的特性來保留這個(gè) store,允許通過 getState 來獲取到 store。之所以通過 getState 來獲取 store 是為了獲取到當(dāng)前 store 的快照,這樣便于打印日志以對比前后兩次 store 變化,方便調(diào)試。
const createStore = (reducers, initialState, enhancer) => {let store = initialState;const getState = () => store;return {getState}}
當(dāng)然,現(xiàn)在這個(gè) store 實(shí)現(xiàn)的比較簡單,畢竟 createStore 還有兩個(gè)參數(shù)沒用到呢。先別急,這倆參數(shù)后面會(huì)用到的。
3.3 實(shí)現(xiàn) subscribe && unsubscribe
既然 Redux 本質(zhì)上是一個(gè)?發(fā)布-訂閱?模式,那么就一定會(huì)有一個(gè)監(jiān)聽方法,類似 jQuery 中的?$.on,在 Redux 中提供了監(jiān)聽和解除監(jiān)聽的兩個(gè)方法。實(shí)現(xiàn)方式也比較簡單,使用一個(gè)數(shù)組來保存所有監(jiān)聽的方法。
const createStore = (...) => {...let listeners = [];const subscribe = (listener) => {listeners.push(listener);}const unsubscribe = (listener) => {const index = listeners.indexOf(listener)listeners.splice(index, 1)}}
3.4 實(shí)現(xiàn) dispatch
dispatch 和 action 是息息相關(guān)的,只有通過 dispatch 才能發(fā)送 action。而發(fā)送 action 之后才會(huì)執(zhí)行 subscribe 監(jiān)聽到的那些方法。所以 dispatch 做的事情就是將 action 傳給 reducer 函數(shù),將執(zhí)行后的結(jié)果設(shè)置為新的 store,然后執(zhí)行 listeners 中的方法。
const createStore = (reducers, initialState) => {...let store = initialState;const dispatch = (action) => {store = reducers(store, action);listeners.forEach(listener => listener())}}
這樣就行了嗎?當(dāng)然還不夠。如果有多個(gè) action 同時(shí)發(fā)送,這樣很難說清楚最后的 store 到底是什么樣的,所以需要加鎖。在 Redux 中 dispatch 執(zhí)行后的返回值也是當(dāng)前的 action。
const createStore = (reducers, initialState) => {...let store = initialState;let isDispatch = false;const dispatch = (action) => {if (isDispatch) return action// dispatch必須一個(gè)個(gè)來isDispatch = truestore = reducers(store, action);isDispatch = falselisteners.forEach(listener => listener())return action;}}
至此為止,Redux 工作流程的原理就已經(jīng)實(shí)現(xiàn)了。但你可能還會(huì)有很多疑問,如果沒有傳 initialState,那么 store 的默認(rèn)值是什么呢?如果傳入了中間件,那么又是什么工作原理呢?
3.5 實(shí)現(xiàn) combineReducers
在剛開始接觸 Redux 的 store 的時(shí)候,我們都會(huì)有一種疑問,store 的結(jié)構(gòu)究竟是怎么定的?combineReducers 會(huì)揭開這個(gè)謎底?,F(xiàn)在來分析 createStore 接收的第一個(gè)參數(shù),這個(gè)參數(shù)有兩種形式,一種直接是一個(gè) reducer 函數(shù),另一個(gè)是用 combineReducers 把多個(gè) reducer 函數(shù)合并到一起。

可以猜測 combineReducers 是一個(gè)高階函數(shù),接收一個(gè)對象作為參數(shù),返回了一個(gè)新的函數(shù)。這個(gè)新的函數(shù)應(yīng)當(dāng)和普通的 reducer 函數(shù)傳參保持一致。
const combineReducers = (reducers) => {return function combination(state = {}, action) {}}
那么 combineReducers 做了什么工作呢?主要是下面幾步:
收集所有傳入的 reducer 函數(shù) 在 dispatch 中執(zhí)行 combination 函數(shù)時(shí),遍歷執(zhí)行所有 reducer 函數(shù)。如果某個(gè) reducer 函數(shù)返回了新的 state,那么 combination 就返回這個(gè) state,否則就返回傳入的 state。
const combineReducers = reducers => {const finalReducers = {},nativeKeys = Object.keys// 收集所有的 reducer 函數(shù)nativeKeys(reducers).forEach(reducerKey => {if(typeof reducers[reducerKey] === "function") {finalReducers[reducerKey] = reducers[reducerKey]}})return function combination(state, action) {let hasChanged = false;const store = {};// 遍歷執(zhí)行 reducer 函數(shù)nativeKeys(finalReducers).forEach(key => {const reducer = finalReducers[key];// 很明顯,store 的 key 來源于 reducers 的 key 值const nextState = reducer(state[key], action)store[key] = nextStatehasChanged = hasChanged || nextState !== state[key];})return hasChanged ? nextState : state;}}
細(xì)心的童鞋一定會(huì)發(fā)現(xiàn),每次調(diào)用 dispatch 都會(huì)執(zhí)行這個(gè) combination 的話,那豈不是不管我發(fā)送什么類型的 action,所有的 reducer 函數(shù)都會(huì)被執(zhí)行一遍?如果 reducer 函數(shù)很多,那這個(gè)執(zhí)行效率不會(huì)很低嗎?但不執(zhí)行貌似又無法完全匹配到?switch...case?中的?action.type。如果能通過鍵值對的形式來匹配?action.type?和 reducer 是不是效率更高一些?類似這樣:
// reduxconst count = (state = 0, action) => {switch(action.type) {case 'increment':return state + action.payload;case 'decrement':return state - action.payload;default:return state;}}// 改進(jìn)后的const count = {state: 0, // 初始 statereducers: {increment: (state, payload) => state + payload,decrement: (state, payload) => state - payload}}
這樣每次發(fā)送新的 action 的時(shí)候,可以直接用?reducers?下面的 key 值來匹配了,無需進(jìn)行暴力的遍歷。天啊,你實(shí)在太聰明了。小聲告訴你,社區(qū)中一些類 Redux 的方案就是這樣做的。以 rematch 和 relite 為例:rematch:
import { init, dispatch } from "@rematch/core";import delay from "./makeMeWait";const count = {state: 0,reducers: {increment: (state, payload) => state + payload,decrement: (state, payload) => state - payload},effects: {async incrementAsync(payload) {await delay();this.increment(payload);}}};const store = init({models: { count }});dispatch.count.incrementAsync(1);
relite:
const increment = (state, payload) => {state.count = state.count + payload;return state;}const decrement = (state, payload) => {state.count = state.count - payload;return state;}
3.6 中間件 和 Store Enhancer
考慮到這樣的情況,我想要打印每次 action 的相關(guān)信息以及 store 前后的變化,那我只能到每個(gè) dispatch 處手動(dòng)打印信息,這樣繁瑣且重復(fù)。createStore 中提供的第三個(gè)參數(shù),可以實(shí)現(xiàn)對 dispatch 函數(shù)的增強(qiáng),我們稱之為?Store Enhancer。?Store Enhancer?是一個(gè)高階函數(shù),它的結(jié)構(gòu)一般是這樣的:
const enhancer = () => {return (createStore) => (reducer, initState, enhancer) => {...}}
enhancer?接收 createStore 作為參數(shù),最后返回的是一個(gè)加強(qiáng)版的?store,本質(zhì)上是對 dispatch 函數(shù)進(jìn)行了擴(kuò)展。logger:
const logger = () => {return (createStore) => (reducer, initState, enhancer) => {const store = createStore(reducer, initState, enhancer);const dispatch = (action) => {console.log(`action=${JSON.stringify(action)}`);const result = store.dispatch(action);const state = store.getState();console.log(`state=${JSON.stringify(state)}`);return result;}return {...state,dispatch}}}
createStore 中如何使用呢?一般在參數(shù)的時(shí)候,會(huì)直接返回。
const createStore = (reducer, initialState, enhancer) => {if (enhancer && typeof enhancer === "function") {return enhancer(createStore)(reducer, initialState)}}
如果你有看過 applyMiddleware 的源碼,會(huì)發(fā)現(xiàn)這兩者實(shí)現(xiàn)方式很相似。applyMiddleware 本質(zhì)上就是一個(gè)?Store Enhancer。
3.7 實(shí)現(xiàn) applyMiddleware
在創(chuàng)建 store 的時(shí)候,經(jīng)常會(huì)使用很多中間件,通過 applyMiddleware 將多個(gè)中間件注入到 store 之中。
const store = createStore(reducers, initialStore, applyMiddleware(thunk, logger, reselect));applyMiddleware 的實(shí)現(xiàn)類似上面的?Store Enhancer。由于多個(gè)中間件可以串行使用,因此最終會(huì)像洋蔥模型一樣,action 傳遞需要經(jīng)過一個(gè)個(gè)中間件處理,所以中間件做的事情就是增強(qiáng) dispatch 的能力,將 action 傳遞給下一個(gè)中間件。那么關(guān)鍵就是將新的 store 和 dispatch 函數(shù)傳給下一個(gè)中間件。

來看一下 applyMiddleware 的源碼實(shí)現(xiàn):
const applyMiddleware = (...middlewares) => {return (createStore) => (reducer, initState, enhancer) => {const store = createStore(reducer, initState, enhancer)const middlewareAPI = {getState: store.getState,dispatch: (action) => dispatch(action)}let chain = middlewares.map(middleware => middleware(middlewareAPI))store.dispatch = compose(...chain)(store.dispatch)return {...store,dispatch}}}
這里用到了一個(gè) compose 函數(shù),compose 函數(shù)類似管道,可以將多個(gè)函數(shù)組合起來。compose(m1, m2)(dispatch)?等價(jià)于?m1(m2(dispatch))。使用 reduce 函數(shù)可以實(shí)現(xiàn)函數(shù)組合。
const compose = (...funcs) => {if (!funcs) {return args => args}if (funcs.length === 1) {return funcs[0]}return funcs.reduce((f1, f2) => (...args) => f1(f2(...args)))}
再來看一下 redux-logger 中間件的精簡實(shí)現(xiàn),會(huì)發(fā)現(xiàn)兩者恰好能匹配到一起。
function logger(middlewareAPI) {return function (next) { // next 即 dispatchreturn function (action) {console.log('dispatch 前:', middlewareAPI.getState());var returnValue = next(action);console.log('dispatch 后:', middlewareAPI.getState(), '\n');return returnValue;};};}
至此為止,Redux 的基本原理就很清晰了,最后整理一個(gè)精簡版的 Redux 源碼實(shí)現(xiàn)。
// 這里需要對參數(shù)為0或1的情況進(jìn)行判斷const compose = (...funcs) => {if (!funcs) {return args => args}if (funcs.length === 1) {return funcs[0]}return funcs.reduce((f1, f2) => (...args) => f1(f2(...args)))}const bindActionCreator = (action, dispatch) => {return (...args) => dispatch(action(...args))}const createStore = (reducer, initState, enhancer) => {if (!enhancer && typeof initState === "function") {enhancer = initStateinitState = null}if (enhancer && typeof enhancer === "function") {return enhancer(createStore)(reducer, initState)}let store = initState,listeners = [],isDispatch = false;const getState = () => storeconst dispatch = (action) => {if (isDispatch) return action// dispatch必須一個(gè)個(gè)來isDispatch = truestore = reducer(store, action)isDispatch = falselisteners.forEach(listener => listener())return action}const subscribe = (listener) => {if (typeof listener === "function") {listeners.push(listener)}return () => unsubscribe(listener)}const unsubscribe = (listener) => {const index = listeners.indexOf(listener)listeners.splice(index, 1)}return {getState,dispatch,subscribe,unsubscribe}}const applyMiddleware = (...middlewares) => {return (createStore) => (reducer, initState, enhancer) => {const store = createStore(reducer, initState, enhancer);const middlewareAPI = {getState: store.getState,dispatch: (action) => dispatch(action)}let chain = middlewares.map(middleware => middleware(middlewareAPI))store.dispatch = compose(...chain)(store.dispatch)return {...store}}}const combineReducers = reducers => {const finalReducers = {},nativeKeys = Object.keysnativeKeys(reducers).forEach(reducerKey => {if(typeof reducers[reducerKey] === "function") {finalReducers[reducerKey] = reducers[reducerKey]}})return (state, action) => {const store = {}nativeKeys(finalReducers).forEach(key => {const reducer = finalReducers[key]const nextState = reducer(state[key], action)store[key] = nextState})return store}}
4. 實(shí)現(xiàn)一個(gè) react-redux
如果想要將 Redux 結(jié)合 React 使用的話,通??梢允褂?react-redux 這個(gè)庫。看過前面 Redux 的原理后,相信你也知道 react-redux 是如何實(shí)現(xiàn)的了吧。react-redux 一共提供了兩個(gè) API,分別是 connect 和 Provider,前者是一個(gè) React 高階組件,后者是一個(gè)普通的 React 組件。react-redux 實(shí)現(xiàn)了一個(gè)簡單的發(fā)布-訂閱庫,來監(jiān)聽當(dāng)前 store 的變化。兩者的作用如下:
Provider:將 store 通過 Context 傳給后代組件,注冊對 store 的監(jiān)聽。 connect:一旦 store 變化就會(huì)執(zhí)行 mapStateToProps 和 mapDispatchToProps 獲取最新的 props 后,將其傳給子組件。

使用方式:
// ProviderReactDOM.render({, document.getElementById('app')})// connect@connect(mapStateToProps, mapDispatchToProps)class App extends Component {}
4.1 實(shí)現(xiàn) Provider
先來實(shí)現(xiàn)簡單的 Provider,已知 Provider 會(huì)使用 Context 來傳遞 store,所以 Provider 直接通過?Context.Provider?將 store 給子組件。
// Context.jsconst ReactReduxContext = createContext(null);// Provider.jsconst Provider = ({ store, children }) => {return ({children})}
Provider 里面還需要一個(gè)發(fā)布-訂閱器。
class Subscription {constructor(store) {this.store = store;this.listeners = [this.handleChangeWrapper];}notify = () => {this.listeners.forEach(listener => {listener()});}addListener(listener) {this.listeners.push(listener);}// 監(jiān)聽 storetrySubscribe() {this.unsubscribe = this.store.subscribe(this.notify);}// onStateChange 需要在組件中設(shè)置handleChangeWrapper = () => {if (this.onStateChange) {this.onStateChange()}}unsubscribe() {this.listeners = null;this.unsubscribe();}}
將 Provider 和 Subscription 結(jié)合到一起,在 useEffect 里面注冊監(jiān)聽。
// Provider.jsconst Provider = ({ store, children }) => {const contextValue = useMemo(() => {const subscription = new Subscription(store);return {store,subscription}}, [store]);// 監(jiān)聽 store 變化useEffect(() => {const { subscription } = contextValue;subscription.trySubscribe();return () => {subscription.unsubscribe();}}, [contextValue]);return ({children})}
4.2 實(shí)現(xiàn) connect
再來看 connect 的實(shí)現(xiàn),這里主要有三步:
使用 useContext 獲取到傳入的 store 和 subscription。 對 subscription 添加一個(gè) listener,這個(gè) listener 的作用就是一旦 store 變化就重新渲染組件。 store 變化之后,執(zhí)行 mapStateToProps 和 mapDispatchToProps 兩個(gè)函數(shù),將其和傳入的 props 進(jìn)行合并,最終傳給 WrappedComponent。

先來實(shí)現(xiàn)簡單的獲取 Context。
const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {return function Connect(props) {const { store, subscription } = useContext(ReactReduxContext);return}}
接下來就要來實(shí)現(xiàn)如何在 store 變化的時(shí)候更新這個(gè)組件。我們都知道在 React 中想實(shí)現(xiàn)更新組件只有手動(dòng)設(shè)置 state 和調(diào)用 forceUpdate 兩種方法,這里使用 useState 每次設(shè)置一個(gè) count 來觸發(fā)更新。
const connect = (mapStateToProps, mapDispatchToProps) => {return (WrappedComponent) => {return (props) => {const { store, subscription } = useContext(ReactReduxContext);const [count, setCount] = useState(0)useEffect(() => {subscription.onStateChange = () => setCount(count + 1)}, [count])const newProps = useMemo(() => {const stateProps = mapStateToProps(store.getState()),dispatchProps = mapDispatchToProps(store.dispatch);return {...stateProps,...dispatchProps,...props}}, [props, store, count])return}}}
react-redux 的原理和上面比較類似,這里只作為學(xué)習(xí)原理的一個(gè)例子,不建議用到生產(chǎn)環(huán)境中。
5. 如何設(shè)計(jì) store
在開發(fā)中,如果想要查看當(dāng)前頁面的 store 結(jié)構(gòu),可以使用 [Redux-DevTools][14] 或者 [React Developer Tools][15] 這兩個(gè) chrome 插件來查看。前者一般用于開發(fā)環(huán)境中,可以將 store 及其變化可視化展示出來。后者主要用于 React,也可以查看 store。關(guān)于 Redux 中 store 如何設(shè)計(jì)對初學(xué)者來說一直都是難題,在我看來這不僅是 Redux 的問題,在任何前端 store 設(shè)計(jì)中應(yīng)該都是一樣的。
5.1 store 設(shè)計(jì)誤區(qū)
這里以知乎的問題頁 store 設(shè)計(jì)為例。在開始之前,先安裝 React Developer Tools,在 RDT 的 Tab 選中根節(jié)點(diǎn)。

然后在 Console 里面輸入?$r.state.store.getState(),將 store 打印出來。

可以看到 store 中有一個(gè) entities 屬性,這個(gè)屬性中分別有 users、questions、answer 等等。
這是一個(gè)問題頁,自然包括問題、回答、回答下面的評論 等等。

一般情況下,這里應(yīng)該是當(dāng)進(jìn)入頁面的時(shí)候,根據(jù) question_id 來分批從后端獲取到所有的回答。點(diǎn)開評論的時(shí)候,會(huì)根據(jù) answer_id 來分批從后端獲取到所有的評論。所以你可能會(huì)想到 store 結(jié)構(gòu)應(yīng)當(dāng)這樣設(shè)計(jì),就像俄羅斯套娃一樣,一層套著一套。
{questions: [{content: 'LOL中哪個(gè)英雄最能表達(dá)出你對刺客的想象?',question_id: '1',answers: [{answer_id: '1-1',content: '我就是來提名一個(gè)已經(jīng)式微的英雄的。沒錯(cuò),就是提莫隊(duì)長...'comments: [{comment_id: '1-1-1',content: '言語精煉,每一句話都是一幅畫面,一組鏡頭'}]}]}]}
看圖可以更直觀感受數(shù)據(jù)結(jié)構(gòu):

這是初學(xué)者經(jīng)常進(jìn)入的一個(gè)誤區(qū),按照 API 來設(shè)計(jì) store 結(jié)構(gòu),這種方法是錯(cuò)誤的。以評論區(qū)回復(fù)為例子,如何將評論和回復(fù)的評論關(guān)聯(lián)起來呢?也許你會(huì)想,把回復(fù)的評論當(dāng)做評論的子評論不就行了嗎?
{comments: [{comment_id: '1-1-1',content: '言語精煉,每一句話都是一幅畫面,一組鏡頭',children: [{comment_id: '1-1-2',content: '我感覺是好多畫面,一部電影。。。'}]},{comment_id: '1-1-2',content: '我感覺是好多畫面,一部電影。。。'}]}
這樣挺好的,滿足了我們的需求,但 children 中的評論和 comments 中的評論數(shù)據(jù)亢余了。
5.2 扁平化 store
聰明的你一定會(huì)想到,如果 children 中只保存?comment_id?不就好了嗎?展示的時(shí)候只要根據(jù)?comment_id?從 comments 中查詢就行了。這就是設(shè)計(jì) store 的精髓所在了。我們可以將 store 當(dāng)做一個(gè)數(shù)據(jù)庫,store 中的狀態(tài)按照領(lǐng)域(domain)來劃分成一張張數(shù)據(jù)表。不同的數(shù)據(jù)表之間以主鍵來關(guān)聯(lián)。因此上面的 store 可以設(shè)計(jì)成三張表,分別是 questions、answers、comments,以它們的 id 作為 key,增加一個(gè)新的字段來關(guān)聯(lián)子級。
{questions: {'1': {id: '1',content: 'LOL中哪個(gè)英雄最能表達(dá)出你對刺客的想象?',answers: ['1-1']}},answers: {'1-1': {id: '1-1',content: '我就是來提名一個(gè)已經(jīng)式微的英雄的。沒錯(cuò),就是提莫隊(duì)長...',comments: ['1-1-1', '1-1-2']}},comments: {'1-1-1': {id: '1-1-1',content: '言語精煉,每一句話都是一幅畫面,一組鏡頭',children: ['1-1-2']},'1-1-2': {id: '1-1-2',content: '我感覺是好多畫面,一部電影。。。'}}}
你會(huì)發(fā)現(xiàn)數(shù)據(jù)結(jié)構(gòu)變得非常扁平化,避免了數(shù)據(jù)亢余以及嵌套過深的問題。在查找的時(shí)候也可以直接通過 id 來查找,避免了通過索引來查找某一具體項(xiàng)。
6. 推薦閱讀
解析Twitter前端架構(gòu) 學(xué)習(xí)復(fù)雜場景數(shù)據(jù)設(shè)計(jì) JSON數(shù)據(jù)范式化(normalizr) React+Redux打造“NEWS EARLY”單頁應(yīng)用

回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~
點(diǎn)擊“閱讀原文”查看70+篇原創(chuàng)文章

