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

THE LAST TIME

The last time, I have learnedhtml

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

也是给本身的查缺补漏和技术分享。git

笔者文章集合详见github

TLT往期

前言

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

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

讲真,标题真的是很差取,由于本文是我写的 redux 的下一篇。两篇凑到一块儿,才是完整的 Redux后端

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

本文续上篇,接着看 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】