阅读 311

【THE LAST TIME】从 Redux 源码中学习它的范式

THE LAST TIME

The last time, I have learned

【THE LAST TIME】 一直是我想写的一个系列,旨在厚积薄发,重温前端。

也是给自己的查缺补漏和技术分享。

笔者文章集合详见

TLT往期

前言

范式概念是库恩范式理论的核心,而范式从本质上讲是一种理论体系。库恩指出:按既定的用法,范式就是一种公认的模型或模式

而学习 Redux,也并非它的源码有多么复杂,而是他状态管理的思想,着实值得我们学习。

讲真,标题真的是不好取,因为本文是我写的 redux 的下一篇。两篇凑到一起,才是完整的 Redux

上篇:从 Redux 设计理念到源码分析

本文续上篇,接着看 combineReducersapplyMiddlewarecompose 的设计与源码实现

至于手写,其实也是非常简单,说白了,去掉源码中严谨的校验,就是市面上手写了。当然,本文,我也尽量以手写演进的形式,去展开剩下几个 api 的写法介绍。

combineReducers

从上一篇中我们知道,newState 是在 dispatch 的函数中,通过 currentReducer(currentState,action)拿到的。所以 state 的最终组织的样子,完全的依赖于我们传入的 reducer。而随着应用的不断扩大,state 愈发复杂,redux 就想到了分而治之(我寄几想的词儿)。虽然最终还是一个根,但是每一个枝放到不同的文件 or func 中处理,然后再来组织合并。(模块化有么有)

combineReducers 并不是 redux 的核心,或者说这是一个辅助函数而已。但是我个人还是喜欢这个功能的。它的作用就是把一个由多个不同 reducer 函数作为 valueobject,合并成一个最终的 reducer 函数。

进化过程

比如我们现在需要管理这么一个"庞大"的 state

庞大的 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
}
复制代码

其实就是变了一个 statekey,然后 key 对应的值是这个 Reducer,这个 Reducerstate 是前面取出这个 keystate 下的值。


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 独有的。ExpressKoa等框架,也都有这个概念。只是为解决不同的问题而存在罢了。

ReduxMiddleware 说白了就是对 dispatch 的扩展,或者说重写,增强 dispatch 的功能! 一般我们常用的可以记录日志、错误采集、异步调用等。

其实关于ReduxMiddleware, 我觉得中文文档说的就已经非常棒了,这里我简单介绍下。感兴趣的可以查看详细的介绍: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 的时候,callbacka,就是一个a(b(...args))function,当然,第一次是 afunc1。后面就是无限的叠罗汉了。最终拿到的是一个 func1(func2(func3(...args)))function

总结

所以回头看看,redux 其实就这么些东西,第一篇算是 redux 的核心,关于状态管理的思想和方式。第二篇可以理解为 redux 的自带的一些小生态。全部的代码不过两三百行。但是这种状态管理的范式,还是非常指的我们再去思考、借鉴和学习的。

学习交流

  • 关注公众号【全栈前端精选】,每日获取好文推荐
  • 添加微信号:is_Nealyang(备注来源) ,入群交流
公众号【全栈前端精选】 个人微信【is_Nealyang】
关注下面的标签,发现更多相似文章
评论