阅读 346

浅析 Rematch 源码

前言

入职后公司用的技术栈还是 react,但状态管理由我原本熟悉的 redux 进化成了在 redux 基础上封装而成的 rematch。用起来也着实方便不少,减少了很多样板代码的编写,同时也不用引入中间件来管理异步的 action 了,但在用起来的过程中难免就会引起了我对 rematch 的一些小疑问:

  • 它是怎么封装 诸如action creator这些以往在 redux 中繁琐的“样本”代码的?
  • 它是如何区分 reducer action 以及 effect action 的?
  • 等等

带着上面的这些疑问,正好北京的夏天着实让我白天毫无出去瞎逛的动力,只有晚上才能出去跑个步啥的,因此闲来无事,我便翻开了 rematch 的源码开始读了起来。

一、总览

打开 rematch 的文件目录,来到 src 文件夹下,可以看到其文件的主要层级如下:

.src
├── plugins     
│    ├── dispatch.ts    //  用于处理 reducer action
│    ├── effects.ts  //  用于处理 effect action
├── typings   //  类型约束文件
├── utils   //  工具函数 
├── index.ts    //  入口文件      
├── pluginFactory.ts  
├── redux.ts    //  基础redux
├── rematch.ts  //  Rematch基类
复制代码

然后打开 index.ts 文件,rematch 的 index 文件非常精简,现版本只存在两个具有实际应用价值的函数:

// 为给定对象创建model,然后返回作为参数接收的对象
export function createModel<S = any, M extends R.ModelConfig<S> = any>(model: M) {
	return model
}

let count = 0

export const init = (initConfig: R.InitConfig = {}): R.RematchStore => {
	// 如果不指定 name,就将其迭代的次数作为 name
	const name = initConfig.name || count.toString()
	count += 1
	// 配置的对象,在这里使用 mergeConfig 来合并
	const config: R.Config = mergeConfig({ ...initConfig, name })
	// 在这里会先将 config 的信息传入 Rematch 函数中,然后会被 init 函数会执行,而它的结果也在此被返回,也就是我们新生成的 store
	return new Rematch(config).init()
}

export default {
	init,
}
复制代码

index 文件中最为核心的就是 init 函数了,它主要做了以下工作:

  • 初始化 store 的 name
  • 将 name 与传入的 config 对象,并返回新的 config 对象
  • 把新的 config 对象作为参数传入,返回 new Rematch(config).init()

二、Rematch

上面的 index 文件到了 new Rematch(config).init() 就截然而止了,虽然我们知道他已经在此过程中,完成了一个 store 的创建,但这个过程我却并不知晓,因此接下来就是要去翻阅 rematch.ts 文件,首先扫一眼这个文件的大概情况,为后面的阅读做个铺垫:

import pluginFactory from './pluginFactory'
import dispatchPlugin from './plugins/dispatch'
import effectsPlugin from './plugins/effects'
import createRedux from './redux'
import * as R from './typings'
import validate from './utils/validate'

const corePlugins: R.Plugin[] = [dispatchPlugin, effectsPlugin]

/**
 * Rematch class
 *
 * an instance of Rematch generated by "init"
 */
export default class Rematch {
	protected config: R.Config
	protected models: R.Model[]
	private plugins: R.Plugin[] = []
	private pluginFactory: R.PluginFactory

	constructor(config: R.Config) {
		
	}
	public forEachPlugin(method: string, fn: (content: any) => void) {
		
	}
	public getModels(models: R.Models): R.Model[] {
		
	}
	public addModel(model: R.Model) {
		
	}
	public 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) {
	    // 这里的 config 就是从 index.ts 里传入的 config
		this.config = config
		this.pluginFactory = pluginFactory(config)
		// 遍历 corePlugins 以及 config 中的 plugins
		// 对其中的每个 plugins 通过 pluginFactor.create 生成 plugins 数组
		for (const plugin of corePlugins.concat(this.config.plugins)) {
			this.plugins.push(this.pluginFactory.create(plugin))
		}
		// preStore: middleware, model hooks
		// 将 middleware 执行一遍,并将 middleware 添加到 this.config.redux.middlewares 这个数组中
		this.forEachPlugin('middleware', (middleware) => {
			this.config.redux.middlewares.push(middleware)
		})
	}
	... ... 
}
复制代码

通过上面的代码我们可以发现,当我们去实例化 Rematch 时,首先会去执行这里的构造函数。这个构造函数主要是为了处理 plugin,并对两类不同的 plugin 分别进行处理:

  • 一种是 corePlugin,也就是核心插件 dispatchPlugin 以及 effectsPlugin ,这里会将他们 push 到 this.plugin 数组中存储起来。
  • 而对于中间件插件,由于中间件插件本身都是“不纯”的,因此本身就属于 effectsPlugin ,这里会将 effectsPlugin 中的 middleWares push 到 this.config.redux.middlewares 中去进行存储。

接下来就是 rematch 中定义的三个方法了:

    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))
    }
复制代码

从这三个方法的名字我们就不难看出这是哪个方法的具体作用了,这三个方法主要是为了协助上面的构造函数以及下面 init() ,因此在此就不一一赘述了。下面就来重头戏 init() :

public init() {
		// collect all models
		// 通过 getModels 获取所有的 models
		this.models = this.getModels(this.config.models)
		// 遍历所有的 models 执行 addModels 
		for (const model of this.models) {
			this.addModel(model)
		}
		// create a redux store with initialState
		// merge in additional extra reducers
		// 这里就是更新 state 的 reducer 了,后面具体会有分析
		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) => {
			const returned = onStoreCreated(rematchStore)
			// if onStoreCreated returns an object value
			// merge its returned value onto the store
			if (returned) {
				Object.keys(returned || {}).forEach((key) => {
					rematchStore[key] = returned[key]
				})
			}
		})

		return rematchStore
	}
复制代码

init() 会先执行 getModels 从而获取所有的 models ,并返回给 this.model, 然后通过遍历 this.model,对其中的每个 models 都执行 addModel ,然后就会去调用 forEachPlugin。这块的执行逻辑稍微有点深,但其实本质上就是为了让所有的 models 都这么执行一次:

plugin.onModel(model)
复制代码

同时这里也会根据 model 的不同的情况,去执行两种plugin:dispatchPlugin 和 effectPlugin。

其中 dispatchPlugin 的 onModel 处理如下:

// dispatch.ts
onModel(model: R.Model) {
		this.dispatch[model.name] = {}
		if (!model.reducers) {
			return
		}
		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`,
				],
			])
			// 根据 model Name 和 reducer Name 生成相应的 dispatch 函数
			this.dispatch[model.name][reducerName] = this.createDispatcher.apply(
				this,
				[model.name, reducerName]
			)
		}
	}
复制代码

我们可以看到 onModel 函数会遍历所有的 reducer,然后生成相应的 dispatch 函数(如何实现后面讨论)

而 effectsPlugin 的 onModel 则是这样的:

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
		}
	},
复制代码

这两者的 onModel 其实都差不多,最大的却别就是 effectsPlugin 的 onModel 在最后标记了 isEffect 为 true 。然后我们就可以来看 this.createDispatcher 到底做了什么:

/**
		 * 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}` }
				if (typeof payload !== 'undefined') {
					action.payload = payload
				}
				if (typeof meta !== 'undefined') {
					action.meta = meta
				}
				return this.dispatch(action)
			}
		}
复制代码

createDispatcher 函数的作用注释里说的很清楚,为 model 和 reducer 生成相应的 action creator,其内部实现是返回一个 async 函数,内部有由 model name 以及 reducer name 组成的套路化的 action type,然后再返回 dispatch(action) 的执行结果。这个 dispatch 又会到哪去呢?让我们将目光回到 rematch.ts 中的 init() 中去,我在上面的代码中有提到这么一段代码:

// 这里就是更新 state 的 reducer 了,后面具体会有分析
const redux = createRedux.call(this, {
	redux: this.config.redux,
	models: this.models,
})
复制代码

实际上我们这段代码就相当于我们在 redux 中的 reducer ,他会接收到 dispatch(action) 从而变动 state。它的详情代码在 redux 中:

import * as Redux from 'redux'
import * as R from './typings'
import isListener from './utils/isListener'

const composeEnhancersWithDevtools = (
	devtoolOptions: R.DevtoolOptions = {}
): any => {
	const { disabled, ...options } = devtoolOptions
	/* istanbul ignore next */
	return !disabled &&
		typeof window === 'object' &&
		window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
		? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__(options)
		: Redux.compose
}

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
		// 将已经新的 reducers 和 存在的 reducer 合并
		this.reducers = { ...this.reducers, ...nextReducers }
		// 如果没有 reducers 就直接返回 state
		if (!Object.keys(this.reducers).length) {
			// no reducers, just return state
			return (state: any) => state
		}
		//  执行合并操作
		return combineReducers(this.reducers)
	}

	this.createModelReducer = (model: R.Model) => {
		const modelBaseReducer = model.baseReducer
		const modelReducers = {}
		// 遍历 model.reducers ,为其中的每个 reducer 创造一个命名空间,并将其赋值到 modelReducer 中去
		for (const modelReducer of Object.keys(model.reducers || {})) {
			const action = isListener(modelReducer)
				? modelReducer
				: `${model.name}/${modelReducer}`
			modelReducers[action] = model.reducers[modelReducer]
		}
		const combinedReducer = (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
		}

		this.reducers[model.name] = !modelBaseReducer
			? combinedReducer
			: (state: any, action: R.Action) =>
					combinedReducer(modelBaseReducer(state, action), action)
	}
	// initialize model reducers
	// 创建 model 的 reducer
	for (const model of models) {
		this.createModelReducer(model)
	}

	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)
			}
		}
		return mergedReducers
	}

	const rootReducer = this.createRootReducer(redux.rootReducers)

	const middlewares = Redux.applyMiddleware(...redux.middlewares)
	const enhancers = composeEnhancersWithDevtools(redux.devtoolOptions)(
		...redux.enhancers,
		middlewares
	)
    // 创建一个 redux store,并返回 this 对象
	this.store = createStore(rootReducer, initialState, enhancers)

	return this
}
复制代码

三、小结

总的来说,rematch 其实就是在 redux 的基础上进行了封装,将我们本来在 redux 中要写的诸如action creator等诸多样本代码给予封装,只需要关心 model 的划分以及写出合理的 reducers 以及 effects 即可。因此简单回答上面我看源码时所带的那两个问题:

  1. 如何区分reducer action以及 effect action

    rematch 在内部有两种 plugin,一种是 dispatchPlugin,一种是 effectPlugin,前者只会让 reducers 进入逻辑代码,后者只会让 effects 进入逻辑代码,并且会标记 isEffect = true

  2. 它是怎么封装 诸如action creator这些以往在 redux 中繁琐的“样本”代码的?

    rematch 在内部的 createDispatch 函数内部会根据 model name 以及 reducer name 创建相应的 action.type,以及对应的 action.payload,然后 dispatch 到 reducer 中去即可,同时也在这个函数内部内置的 async 来支持异步的 action。

扩展阅读

图片拍摄于 冈仁波齐 原文地址:

关注下面的标签,发现更多相似文章
评论