Rematch源碼解析及思考

Rematch源碼解析及思考

27 人贊了文章

Rematch

rematch在redux的基礎上構建並減少了樣板代碼和執行了一些最佳實踐

本文版本是 1.0.0-alpha.6

[1.0.0-beta.3] - 2018-06-23 - BreakingChange: dispatch和 getState被移動到了init函數裡面,但是無礙本文閱讀,整體思想和架構都是不變的,可以install 1.0.0-alpha.6 debug

在2018年,react生態圈已經有了許多的狀態管理類庫,譬如:

- Redux--(Redux is a predictable state container for JavaScript apps)

- Dva--(React and redux based, lightweight and elm-style framework. )

- Mobx--(Simple, scalable state management)

- Mobx-state-tree--(Opinionated, transactional, MobX powered state container combining the best features of the immutable and mutable world for an optimal DX )

- Rxjs---(Reactive Extensions Library for JavaScript)

相信絕大多數的同學都會在狀態管理庫的選擇上犯難,上面列出來的,還不包括redux生態下的redux-saga, redux-promise, redux-observable等等膠水中間件類庫。

本文不會去討論狀態管理庫的選擇,相信每個有經驗的開發者,都會按照團隊,業務複雜度,難易度綜合做個標準選擇最合適的類庫,同時也不會去討論Rematch的基礎用法

@rematch/core API?

rematch.gitbooks.io

因此,本文想通過研讀Rematch的源碼進行學習設計思想,並且看看是如何實現一個更好用的redux。

源碼以typescript編寫,如果有小夥伴不是很熟悉,可以結合文章,自己debug進行閱讀哈~記得install 1.0.0-alpha.6版本的,因為我目前看到的beta 2的版本,source是被壓縮的。

目錄結構

.src├── plugins │ ├── dispatch.ts // 生成dispatch函數對象│ ├── effects.ts // 處理非同步action的插件├── utils // 工具函數 ├── index.ts // 入口文件 ├── pluginFactory.ts // 讓插件對象從根工廠函數裡面繼承 ├── redux.ts // 基礎redux├── rematch.js // Rematch基類└── typings.d.ts

在閱讀源碼開始之前,來看看我們在實際的組件當中怎麼實例化store,

示例:

import React from react;import { Provider } from react-reduximport { init } from @rematch/coreconst store = init({ models: { count, }})ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById(root));

通過實際場景中的業務代碼,我們很快的就可以確定,Rematch的初始化函數是從@rematch/core當中index.ts暴露出來的init函數,所以我們的目標先鎖定在index.ts當中

按照慣例先從index開始閱讀

首先,index.ts會返回幾個主要的函數,暴露給外部使用,同時我們的焦點也主要集中在以下的三個API當中:

export default { dispatch, getState, init,}

index.ts:

// 全局storesconst stores = {}// 全局dispatches,根據以modelName為key分const dispatches = {}// 全局Dispatch:從stores當中調用store.dispatchexport const dispatch = (action: R.Action) => { // ...}// 全局getState:返回所有在stores當中的store。export const getState = () => { // ..}// 全局init:初始化應用,初始化rematch storeexport const init = (initConfig: R.InitConfig = {}): R.RematchStore => { // ..}// 省略兩個函數getDispatch,createModel

三個函數核心函數職責

dispatch

import { dispatch } from @rematch/coreexport const dispatch = (action: R.Action) => { for (const storeName of Object.keys(stores)) { stores[storeName].dispatch(action) }}

由於在index.ts主文件當中,聲明了const dispatches = {}這個全局變數,同時在後面的函數執行當中,把所有的dispatch按照modelName進行存儲,所以dispatch函數可以在全局任何位置進行調用:dispatch.storeName[reducer/effect]

getState

getState(): { [storeName]: state }

import { getState } from @rematch/coreexport const getState = () => { const state = {} for (const name of Object.keys(stores)) { state[name] = stores[name].getState() } return state}

dispatch的基礎原理相同,由於在init函數當中,把store都按照name的key-value形式存儲在了全局變數const stores = {}當中,所以源碼的函數通過遍歷得到了所有的state狀態,以供開發者調用:getState()

Init

init(config)

import { init } from @rematch/coreexport const init = (initConfig: R.InitConfig = {}): R.RematchStore => { /** * initConfig: {} init函數的第一個參數 **/ // 如果不指定name,則以迭代下標形式當做name(「0」, "1"...) const name = initConfig.name || Object.keys(stores).length.toString() /** config:{} 配置的對象,通過utils下的mergeconfig函數合併,返回出 { models: { count: {...} }, name: "0", plugins: [], redux: {... redux接入的各種屬性,如enhancers, middlewares, reducers, rootReducers等}, } **/ const config: R.Config = mergeConfig({ ...initConfig, name }) /** 核心類:Rematch Store,最終會被init函數執行後返回 **/ const store = new Rematch(config).init() /** 全局store的存儲 **/ stores[name] = store for (const modelName of Object.keys(store.dispatch)) { if (!dispatch[modelName]) { dispatch[modelName] = {} } for (const actionName of Object.keys(store.dispatch[modelName])) { // 如果不存在監聽者模式(在一個model裡面監聽另外一個model的reducers: ) // example:在modelA 中的reducer聲明 modelB/changeModelState: (state, payload) => {} if (!isListener(actionName)) { const action = store.dispatch[modelName][actionName] if (!dispatches[modelName]) { dispatches[modelName] = {} } if (!dispatches[modelName][actionName]) { dispatches[modelName][actionName] = {} } dispatches[modelName][actionName][name] = action // 給 @rematch/core 暴露出來 dispatch調用 的 /** * dispatch.gameModel.updateReducer = () => { * 執行全局stores裡面對應storeName下的dispatch中modelName(gameModel)的action(updateReducer)。 * } */ dispatch[modelName][actionName] = (payload: any, meta: any) => { for (const storeName of Object.keys(dispatches[modelName][actionName])) { stores[storeName].dispatch[modelName][actionName](payload, meta) } } } } } return store}

init()函數是整個Rematch的工廠,是初始化所有reducer, effect的核心工具函數,init的職責包括了

1. 設置stores name

2. 把stores name合併外部傳入的config對象,並返回內部所需的config對象

3. *通過new Rematch(config).init()返回所有初始化的store。(後文解讀)

4. 遍歷初始化store當中的dispatch,執行對應的全局dispatches存儲過程

從上面幾個函數職責的分析,我們可以很清楚知道在index.ts當中,核心的三個函數都做了一些什麼事情,同時可以從中發現一些異於redux而且便於使用者更好的開發的設計

  • 不僅僅是注入型的dispatch,而是具備依託init函數賦值dispatches全局的dispatch函數
  • 所有的dispatches都有對應的命名空間,合理地分隔了models以及reducers

梳理了最外層的函數職責之後,進入到new Rematch(config)基類,也就是返回整個store的類當中,再看看Remacth基類的init函數又做了一些什麼(此處的init和上面的init不是同一個)

rematch.ts

export default class Rematch { protected config: R.Config protected models: R.Model[] private plugins: R.Plugin[] = [] private pluginFactory: R.PluginFactory constructor(config: R.Config) { // 注入從index.ts 當中的 config對象 this.config = config // 通過pluginFactory函數傳遞config參數,返回 { validate: func(validations), create: func(plugin) } this.pluginFactory = pluginFactory(config) // 遍歷核心插件(dispatchPlugin, effectsPlugins)以及config當中的plugins for (const plugin of corePlugins.concat(this.config.plugins)) { /** 通過插件工廠pluginFactory.create生成plugins 數組,存儲形式 [ "0": {onModel: func, onStoreCreated: func} // dispatchPlugin "1": {onModel: func, middleware: func} // effectsPlugin ] */ this.plugins.push(this.pluginFactory.create(plugin)) } // preStore: middleware, model hooks this.forEachPlugin(middleware, (middleware) => { this.config.redux.middlewares.push(middleware) }) } public forEachPlugin(method: string, fn: (content: any) => void) { for (const plugin of this.plugins) { if (plugin[method]) { fn(plugin[method]) } } } public getModels(models: R.Models): R.Model[] { return Object.keys(models).map((name: string) => ({ name, ...models[name], reducers: models[name].reducers || {}, })) } public addModel(model: R.Model) { validate([ [!model, model config is required], [typeof model.name !== string, model "name" [string] is required], [model.state === undefined, model "state" is required], ]) // run plugin model subscriptions this.forEachPlugin(onModel, (onModel) => onModel(model)) } public init() { // collect all models this.models = this.getModels(this.config.models) for (const model of this.models) { this.addModel(model) } // create a redux store with initialState // merge in additional extra reducers const redux = createRedux.call(this, { redux: this.config.redux, models: this.models, }) const rematchStore = { name: this.config.name, ...redux.store, // dynamic loading of models with `replaceReducer` model: (model: R.Model) => { this.addModel(model) redux.mergeReducers(redux.createModelReducer(model)) redux.store.replaceReducer(redux.createRootReducer(this.config.redux.rootReducers)) redux.store.dispatch({ type: @@redux/REPLACE }) }, } this.forEachPlugin(onStoreCreated, (onStoreCreated) => onStoreCreated(rematchStore)) rematchStore.dispatch = this.pluginFactory.dispatch return rematchStore }}

rematch.ts - Rematch constructor(config: R.Config)

constructor(config: R.Config) { // 注入從index.ts 當中的 config對象 this.config = config // 通過pluginFactory函數傳遞config參數,返回 { validate: func(validations), create: func(plugin) } this.pluginFactory = pluginFactory(config) // 遍歷核心插件(dispatchPlugin, effectsPlugins)以及config當中的plugins for (const plugin of corePlugins.concat(this.config.plugins)) { /** 通過插件工廠pluginFactory.create生成plugins 數組,存儲形式 [ "0": {onModel: func, onStoreCreated: func} // dispatchPlugin "1": {onModel: func, middleware: func} // effectsPlugin ] */ this.plugins.push(this.pluginFactory.create(plugin)) } // preStore: middleware, model hooks // 執行一遍middleware,因為middleware本質是一個非同步,所以進入到了effectPlugins的插件對象中 this.forEachPlugin(middleware, (middleware) => { // 把effectsPlugin當中的middlware添加到this.config.redux.middlewares的數組當中 this.config.redux.middlewares.push(middleware) })}

在實例化Rematch的時候,首先去執行構造函數,主要的行為都是處理初始化的plugin賦值行為。主要分為兩類plugin的賦值執行操作

1. this.plugins:普通的核心插件,dispatchPlugin以及effectsPlugin,插入到

2. this.config.redux.middlewares:中間件插件,effectsPlugin當中的middleware以及外部業務config對象里傳入的middlwares。

有了構造函數的初始化plugin之後,就可以繼續執行init的函數了。

rematch.ts - Rematch init()

先瀏覽一下整體代碼,下文有執行解析

public init() { // collect all models // 通過getModels函數,收集所有的models。 this.models = this.getModels(this.config.models) for (const model of this.models) { this.addModel(model) } // create a redux store with initialState // merge in additional extra reducers const redux = createRedux.call(this, { redux: this.config.redux, models: this.models, }) const rematchStore = { name: this.config.name, ...redux.store, // dynamic loading of models with `replaceReducer` model: (model: R.Model) => { this.addModel(model) redux.mergeReducers(redux.createModelReducer(model)) redux.store.replaceReducer(redux.createRootReducer(this.config.redux.rootReducers)) redux.store.dispatch({ type: @@redux/REPLACE }) }, } this.forEachPlugin(onStoreCreated, (onStoreCreated) => onStoreCreated(rematchStore)) rematchStore.dispatch = this.pluginFactory.dispatch return rematchStore}

首先會先去執行所有的models的收集,把config當中的models都扔給this.getModels函數去執行,並且返回一個以

{ name: "count", reducers: {}, effects: {}, state: {}}

這樣的形式返回給this.model,然後再去遍歷this.model執行this.addModel中的this.forEachPlugin(onModel, (onModel) => onModel(model)),按照dispatchPlugin和effectPlugin兩個plugin去執行(或其他存在onModel的plugins)。

  • dispatchPlugin只會讓model.reducer進入邏輯代碼

const dispatchPlugin: R.Plugin = { exposed: { storeDispatch(action: R.Action, state: any) { // 這裡的代碼是避免整個store還沒完全執行完的替代函數,會拋出異常 console.warn(Warning: store not yet loaded) }, storeGetState() { // 這裡的代碼是避免整個store還沒完全執行完的替代函數,會拋出異常 console.warn(Warning: store not yet loaded) }, /** * dispatch * * both a function (dispatch) and an object (dispatch[modelName][actionName]) * @param action R.Action */ dispatch(action: R.Action) { // 當整個store init完成之後,這裡的storeDispatch最終會指向最外部暴露給開發者的dispatchhan數 return this.storeDispatch(action) }, /** * createDispatcher * * genereates an action creator for a given model & reducer * @param modelName string * @param reducerName string */ createDispatcher(modelName: string, reducerName: string) { return async (payload?: any, meta?: any): Promise<any> => { const action: R.Action = { type: `${modelName}/${reducerName}` } // 創建action type: {} // { type: count/increment } if (typeof payload !== undefined) { action.payload = payload } if (typeof meta !== undefined) { action.meta = meta } // 判斷是否是effect類型 if (this.dispatch[modelName][reducerName].isEffect) { // ensure that effect state is captured on dispatch // to avoid possible mutations and warnings return this.dispatch(action) } return this.dispatch(action) } }, }, // 在Store完全創建完之後,進行對應的賦值,指向外部的暴露的dipatch和getState onStoreCreated(store: any) { this.storeDispatch = store.dispatch this.storeGetState = store.getState }, // generate action creators for all model.reducers onModel(model: R.Model) { this.dispatch[model.name] = {} if (!model.reducers) { return } // 僅執行model.reducers的遍歷 for (const reducerName of Object.keys(model.reducers)) { this.validate([ [ !!reducerName.match(//.+//), `Invalid reducer name (${model.name}/${reducerName})`, ], [ typeof model.reducers[reducerName] !== function, `Invalid reducer (${model.name}/${reducerName}). Must be a function`, ], ]) this.dispatch[model.name][reducerName] = this.createDispatcher.apply( this, [model.name, reducerName] ) } },}

onModel函數,會遍歷所有的reducers,同時根據modelName -> reducerName -> 的鏈路生成對應的dispatcher函數,同時dispatcher函數返回一個async function最終再返回出dispatch的執行結果。也就是執行store當中的reducer。

  • effectsPlugin只會讓model.effects進入邏輯代碼

onModel(model: R.Model): void { if (!model.effects) { return } const effects = typeof model.effects === function ? model.effects(this.dispatch) : model.effects for (const effectName of Object.keys(effects)) { this.validate([ [ !!effectName.match(///), `Invalid effect name (${model.name}/${effectName})`, ], [ typeof effects[effectName] !== function, `Invalid effect (${model.name}/${effectName}). Must be a function`, ], ]) this.effects[`${model.name}/${effectName}`] = effects[effectName].bind( this.dispatch[model.name] ) // add effect to dispatch // is assuming dispatch is available already... that the dispatch plugin is in there this.dispatch[model.name][effectName] = this.createDispatcher.apply( this, [model.name, effectName] ) // tag effects so they can be differentiated from normal actions this.dispatch[model.name][effectName].isEffect = true }},

effectsPlugin的onModel和dispatchPlugins 的onModel也差不多。主要區別在標記is effect = true,同樣最終執行dispatch 一個內置創建的action type,然後去觸發models裡面的effect對應的邏輯函數改變。

至此,我們絕大多數的邏輯調用已經梳理完成了,但是還有臨門的一腳,變更的state的reducer在哪裡處理呢?其實在rematch.tsinit函數中還有一個重要的角色代碼

rematch.ts

import createRedux from ./redux// rematch.ts -> init函數內const redux = createRedux.call(this, { redux: this.config.redux, models: this.models, })

而此處的代碼是源自./redux這個文件的,

redux.ts

import * as Redux from reduximport * as R from ./typingsimport isListener from ./utils/isListenerconst composeEnhancersWithDevtools = (devtoolOptions: R.DevtoolOptions = {}): any => { // redux dev-tool}export default function({ redux, models,}: { redux: R.ConfigRedux, models: R.Model[],}) { const combineReducers = redux.combineReducers || Redux.combineReducers const createStore: Redux.StoreCreator = redux.createStore || Redux.createStore const initialState: any = typeof redux.initialState !== undefined ? redux.initialState : {} this.reducers = redux.reducers // combine models to generate reducers this.mergeReducers = (nextReducers: R.ModelReducers = {}) => { // merge new reducers with existing reducers this.reducers = { ...this.reducers, ...nextReducers } if (!Object.keys(this.reducers).length) { // no reducers, just return state return (state: any) => state } // 把redux當中的reducers以及this.reducers進行合併 return combineReducers(this.reducers) } this.createModelReducer = (model: R.Model) => { const modelReducers = {} for (const modelReducer of Object.keys(model.reducers || {})) { // 為每個reducer創建命名空間,並賦值到modelReducers當中 const action = isListener(modelReducer) ? modelReducer : `${model.name}/${modelReducer}` modelReducers[action] = model.reducers[modelReducer] } // 給this.reducers 插入相應的rematch reducers,並且按照reducers當中傳入的state進行返回state // 給store this.reducers[model.name] = ( state: any = model.state, action: R.Action ) => { // handle effects if (typeof modelReducers[action.type] === function) { return modelReducers[action.type](state, action.payload, action.meta) } return state } } // initialize model reducers for (const model of models) { // 執行創建model的Reducer this.createModelReducer(model) /** this.reducers[count/increment] = (state) => state */ } this.createRootReducer = ( rootReducers: R.RootReducers = {} ): Redux.Reducer<any, R.Action> => { const mergedReducers: Redux.Reducer<any> = this.mergeReducers() if (Object.keys(rootReducers).length) { return (state, action) => { const rootReducerAction = rootReducers[action.type] if (rootReducers[action.type]) { return mergedReducers(rootReducerAction(state, action), action) } return mergedReducers(state, action) } } // 返回combineReducers(this.reducers) return mergedReducers } const rootReducer = this.createRootReducer(redux.rootReducers) // 被合併的rootReducer。 const middlewares = Redux.applyMiddleware(...redux.middlewares) const enhancers = composeEnhancersWithDevtools(redux.devtoolOptions)( ...redux.enhancers, middlewares ) this.store = createStore(rootReducer, initialState, enhancers) // 最後通過createStore,完成對reducer, initialState, enhancers的組裝,返回redux store return this}

上述代碼非常簡單,基本上就是利用外部傳入配置中models以及redux選項進行合併,構造出一個redux store,並返回這個函數本身的this對象。現在基本可以確定,rematch 是基於redux封裝了各種樣板代碼,還有非同步action的集合體了。

總結

通過解讀rematch的源碼,我們看出了這個庫是如何優雅的封裝了redux的一系列繁瑣樣板代碼

  • 便利性更高的全局dispatch函數。
  • 內置action type, action creator 解決大量樣板代碼。
  • 內置async await處理非同步action。
  • 合理區分reducer action / effect action,不再為非同步行為犯難

最後,如果文中有解析不合理的地方歡迎提出。


推薦閱讀:

Flux架構模式
React-Redux源碼分析
集成 React 和 Datatables - 並沒有宣傳的那麼難
Redux中的reducer到底是什麼,以及它為什麼叫reducer?
構建離線優先的 React 應用

TAG:React | Redux | 前端開發 |