THE LAST TIME
The last time, I have learned
【THE LAST TIME】 一直是我想写的一个系列,旨在厚积薄发,重温前端。
也是给自己的查缺补漏和技术分享。
笔者文章集合详见:
- GitHub 地址:Nealyang/personalBlog
- 公众号:全栈前端精选
TLT往期
前言
范式概念是库恩范式理论的核心,而范式从本质上讲是一种理论体系。库恩指出:按既定的用法,范式就是一种公认的模型或模式。
而学习 Redux
,也并非它的源码有多么复杂,而是他状态管理的思想,着实值得我们学习。
讲真,标题真的是不好取,因为本文是我写的 redux
的下一篇。两篇凑到一起,才是完整的 Redux
。
本文续上篇,接着看 combineReducers
、applyMiddleware
和 compose
的设计与源码实现
至于手写,其实也是非常简单,说白了,去掉源码中严谨的校验,就是市面上手写了。当然,本文,我也尽量以手写演进的形式,去展开剩下几个 api
的写法介绍。
combineReducers
从上一篇中我们知道,newState
是在 dispatch
的函数中,通过 currentReducer(currentState,action)
拿到的。所以 state
的最终组织的样子,完全的依赖于我们传入的 reducer
。而随着应用的不断扩大,state
愈发复杂,redux
就想到了分而治之(我寄几想的词儿)。虽然最终还是一个根,但是每一个枝放到不同的文件 or func
中处理,然后再来组织合并。(模块化有么有)
combineReducers
并不是 redux
的核心,或者说这是一个辅助函数而已。但是我个人还是喜欢这个功能的。它的作用就是把一个由多个不同 reducer
函数作为 value
的 object
,合并成一个最终的 reducer
函数。
进化过程
比如我们现在需要管理这么一个"庞大"的 state
:
let state={
name:'Nealyang',
baseInfo:{
age:'25',
gender:'man'
},
other:{
github:'https://github.com/Nealyang',
WeChatOfficialAccount:'全栈前端精选'
}
}
因为太庞大了,写到一个 reducer
里面去维护太难了。所以我拆分成三个 reducer
。
function nameReducer(state, action) {
switch (action.type) {
case "UPDATE":
return action.name;
default:
return state;
}
}
function baseInfoReducer(state, action) {
switch (action.type) {
case "UPDATE_AGE":
return {
...state,
age: action.age,
};
case "UPDATE_GENDER":
return {
...state,
age: action.gender,
};
default:
return state;
}
}
function otherReducer(state,action){...}
为了他这个组成一个我们上文看到的 reducer
,我们需要搞个这个函数
const reducer = combineReducers({
name:nameReducer,
baseInfo:baseInfoReducer,
other:otherReducer
})
所以,我们现在自己写一个 combineReducers
function combineReducers(reducers){
const reducerKeys = Object.keys(reducers);
return function (state={},action){
const nextState = {};
for(let i = 0,keyLen = reducerKeys.length;i<keyLen;i++){
// 拿出 reducers 的 key,也就是 name、baseInfo、other
const key = reducerKeys[i];
// 拿出如上的对应的 reducer: nameReducer、baseInfoReducer、otherReducer
const reducer = reducers[key];
// 去除需要传递给对应 reducer 的初始 state
const preStateKey = state[key];
// 拿到对应 reducer 处理后的 state
const nextStateKey = reducer(preStateKey,action);
// 赋值给新 state 的对应的 key 下面
nextState[key] = nextStateKey;
}
return nextState;
}
}
基本如上,我们就完事了。
关于 reducer
更多的组合、拆分、使用的,可以参照我 github
开源的前后端博客的 Demo:React-Express-Blog-Demo
源码
export type Reducer<S = any, A extends Action = AnyAction> = (
state: S | undefined,
action: A
) => S
export type ReducersMapObject<S = any, A extends Action = Action> = {
[K in keyof S]: Reducer<S[K], A>
}
定义了一个需要传递给 combineReducers
函数的参数类型。也就是我们上面的
{
name:nameReducer,
baseInfo:baseInfoReducer,
other:otherReducer
}
其实就是变了一个 state
的 key
,然后 key
对应的值是这个 Reducer
,这个 Reducer
的 state
是前面取出这个 key
的state
下的值。
export default function combineReducers(reducers: ReducersMapObject) {
//获取所有的 key,也就是未来 state 的 key,同时也是此时 reducer 对应的 key
const reducerKeys = Object.keys(reducers)
// 过滤一遍 reducers 对应的 reducer 确保 kv 格式么有什么毛病
const finalReducers: ReducersMapObject = {}
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]
if (process.env.NODE_ENV !== 'production') {
if (typeof reducers[key] === 'undefined') {
warning(`No reducer provided for key "${key}"`)
}
}
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
// 再次拿到确切的 keyArray
const finalReducerKeys = Object.keys(finalReducers)
// This is used to make sure we don't warn about the same
// keys multiple times.
let unexpectedKeyCache: { [key: string]: true }
if (process.env.NODE_ENV !== 'production') {
unexpectedKeyCache = {}
}
let shapeAssertionError: Error
try {
// 校验自定义的 reducer 一些基本的写法
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
// 重点是这个函数
return function combination(
state: StateFromReducersMapObject<typeof reducers> = {},
action: AnyAction
) {
if (shapeAssertionError) {
throw shapeAssertionError
}
if (process.env.NODE_ENV !== 'production') {
const warningMessage = getUnexpectedStateShapeWarningMessage(
state,
finalReducers,
action,
unexpectedKeyCache
)
if (warningMessage) {
warning(warningMessage)
}
}
let hasChanged = false
const nextState: StateFromReducersMapObject<typeof reducers> = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
// 上面的部分都是我们之前手写内容,nextStateForKey 是返回的一个newState,判断不能为 undefined
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
// 判断是否改变,这里其实我还是很疑惑
// 理论上,reducer 后的 newState 无论怎么样,都不会等于 preState 的
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
hasChanged =
hasChanged || finalReducerKeys.length !== Object.keys(state).length
return hasChanged ? nextState : state
}
}
combineReducers
代码其实非常简单,核心代码也就是我们上面缩写的那样。但是我是真的喜欢这个功能。
applyMiddleware
说 applyMiddleware
这个方法,其实不得不说,redux
中的 Middleware
。中间件的概念不是 redux
独有的。Express
、Koa
等框架,也都有这个概念。只是为解决不同的问题而存在罢了。
Redux
的 Middleware
说白了就是对 dispatch
的扩展,或者说重写,增强 dispatch
的功能! 一般我们常用的可以记录日志、错误采集、异步调用等。
其实关于Redux
的 Middleware
, 我觉得中文文档说的就已经非常棒了,这里我简单介绍下。感兴趣的可以查看详细的介绍:Redux 中文文档
Middleware 演化过程
记录日志的功能增强
- 需求:在每次修改
state
的时候,记录下来 修改前的state
,为什么修改了,以及修改后的state
。 - Action:每次修改都是
dispatch
发起的,所以这里我只要在dispatch
加一层处理就一劳永逸了。
const store = createStore(reducer);
const next = store.dispatch;
/*重写了store.dispatch*/
store.dispatch = (action) => {
console.log('this state', store.getState());
console.log('action', action);
next(action);
console.log('next state', store.getState());
}
如上,在我们每一次修改 dispatch
的时候都可以记录下来日志。因为我们是重写了 dispatch
不是。
增加个错误监控的增强
const store = createStore(reducer);
const next = store.dispatch;
store.dispatch = (action) => {
try {
next(action);
} catch (err) {
console.error('错误报告: ', err)
}
}
所以如上,我们也完成了这个需求。
但是,回头看看,这两个需求如何才能够同时实现,并且能够很好地解耦呢?
想一想,既然我们是增强 dispatch。那么是不是我们可以将 dispatch 作为形参传入到我们增强函数。
多文件增强
const exceptionMiddleware = (next) => (action) => {
try {
/*loggerMiddleware(action);*/
next(action);
} catch (err) {
console.error('错误报告: ', err)
}
}
/*loggerMiddleware 变成参数传进去*/
store.dispatch = exceptionMiddleware(loggerMiddleware);
// 这里额 next 就是最纯的 store.dispatch 了
const loggerMiddleware = (next) => (action) => {
console.log('this state', store.getState());
console.log('action', action);
next(action);
console.log('next state', store.getState());
}
所以最终使用的时候就如下了
const store = createStore(reducer);
const next = store.dispatch;
const loggerMiddleware = (next) => (action) => {
console.log('this state', store.getState());
console.log('action', action);
next(action);
console.log('next state', store.getState());
}
const exceptionMiddleware = (next) => (action) => {
try {
next(action);
} catch (err) {
console.error('错误报告: ', err)
}
}
store.dispatch = exceptionMiddleware(loggerMiddleware(next));
但是如上的代码,我们又不能将 Middleware 独立到文件里面去,因为依赖外部的 store
。所以我们再把 store
传入进去!
const store = createStore(reducer);
const next = store.dispatch;
const loggerMiddleware = (store) => (next) => (action) => {
console.log('this state', store.getState());
console.log('action', action);
next(action);
console.log('next state', store.getState());
}
const exceptionMiddleware = (store) => (next) => (action) => {
try {
next(action);
} catch (err) {
console.error('错误报告: ', err)
}
}
const logger = loggerMiddleware(store);
const exception = exceptionMiddleware(store);
store.dispatch = exception(logger(next));
以上其实就是我们写的一个 Middleware
,理论上,这么写已经可以满足了。但是!是不是有点不美观呢?且阅读起来非常的不直观呢?
如果我需要在增加个中间件,调用就成为了
store.dispatch = exception(time(logger(action(xxxMid(next)))))
这也就是 applyMiddleware
的作用所在了。
我们只需要知道有多少个中间件,然后在内部顺序调用就可以了不是
const newCreateStore = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware)(createStore);
const store = newCreateStore(reducer)
手写 applyMiddleware
const applyMiddleware = function (...middlewares) {
// 重写createStore 方法,其实就是返回一个带有增强版(应用了 Middleware )的 dispatch 的 store
return function rewriteCreateStoreFunc(oldCreateStore) {
// 返回一个 createStore 供外部调用
return function newCreateStore(reducer, initState) {
// 把原版的 store 先取出来
const store = oldCreateStore(reducer, initState);
// const chain = [exception, time, logger] 注意这里已经传给 Middleware store 了,有了第一次调用
const chain = middlewares.map(middleware => middleware(store));
// 取出原先的 dispatch
let dispatch = store.dispatch;
// 中间件调用时←,但是数组是→。所以 reverse。然后在传入 dispatch 进行第二次调用。最后一个就是 dispatch func 了(回忆 Middleware 是不是三个括号~~~)
chain.reverse().map(middleware => {
dispatch = middleware(dispatch);
});
store.dispatch = dispatch;
return store;
}
}
}
解释全在代码上了
其实源码里面也是这么个逻辑,但是源码实现更加的优雅。他利用了函数式编程的 compose
方法。在看 applyMiddleware
的源码之前呢,先介绍下 compose 的方法吧。
compose
其实 compose
函数做的事就是把 var a = fn1(fn2(fn3(fn4(x))))
这种嵌套的调用方式改成 var a = compose(fn1,fn2,fn3,fn4)(x)
的方式调用。
compose
的运行结果是一个函数,调用这个函数所传递的参数将会作为compose
最后一个参数的参数,从而像'洋葱圈'似的,由内向外,逐步调用。
export default function compose(...funcs: Function[]) {
if (funcs.length === 0) {
// infer the argument type so it is usable in inference down the line
return <T>(arg: T) => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args: any) => a(b(...args)))
}
哦豁!有点蒙有么有~ 函数式编程就是烧脑?且直接。所以爱的人非常爱。
compose
是函数式编程中常用的一种组合函数的方式。
方法很简单,传入的形参是 func[],如果只有一个,那么直接返回调用结果。如果是多个,则funcs.reduce((a, b) => (...args: any) => a(b(...args)))
.
我们直接啃最后一行吧
import {componse} from 'redux'
function add1(str) {
return 1 + str;
}
function add2(str) {
return 2 + str;
}
function add3(a, b) {
return a + b;
}
let str = compose(add1,add2,add3)('x','y')
console.log(str)
//输出结果 '12xy'
dispatch = compose<typeof dispatch>(...chain)(store.dispatch)
applyMiddleware 的源码最后一行是这个。其实即使我们上面手写的 reverse 部分。
reduce 是 es5 的数组方法了,对累加器和数组中的每个元素(从左到右)应用一个函数,将其减少为单个值。函数签名为:arr.reduce(callback[, initialValue])
所以如若我们这么看:
[func1,func2,func3].reduce(function(a,b){
return function(...args){
return a(b(...args))
}
})
所以其实就非常好理解了,每一次 reduce
的时候,callback
的a
,就是一个a(b(...args))
的 function
,当然,第一次是 a
是 func1
。后面就是无限的叠罗汉了。最终拿到的是一个 func1(func2(func3(...args)))
的 function
。
总结
所以回头看看,redux
其实就这么些东西,第一篇算是 redux
的核心,关于状态管理的思想和方式。第二篇可以理解为 redux
的自带的一些小生态。全部的代码不过两三百行。但是这种状态管理的范式,还是非常指的我们再去思考、借鉴和学习的。
学习交流
- 关注公众号【全栈前端精选】,每日获取好文推荐
- 添加微信号:is_Nealyang(备注来源) ,入群交流
公众号【全栈前端精选】 | 个人微信【is_Nealyang】 | |
---|---|---|
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。