一次redux改造计划

284 阅读4分钟

为什么会有这个idea

  • 一个reducer需要太多样板式代码,你创建相关的actionsconstantsreducers或许更多目录,并管理它们,这导致每新增一个reducer都需要往这些文件夹中添加新的reducer相关定义

  • 普遍的reducerifelse ifelseswitch,丑陋且语义不够

  • 不想学习redux toolkit,正好想学习 redux and react-redux

  • reducer随着时间增加,每一次触发reducer,都会触发无关的reducerdefault,我认为这些无关的default返回值是无用的,只是在初始化redux state时有用,非初始化时这些default的返回值已在redux中保存了一份

如何解决1,2问题?

// 伪代码

const count = new CreateReducer(1).addAction('add',(state,action)=>{
     return state + action.count
})

const store = createStore(combineReducers({ count: count.finish() }))

store.getState()  // { count: 1 }

// 该有类型提示
count.dispatcher('add',{ count: 1 })

store.getState()  // { count: 2 }

我们应该实现一个CreateReducer class

他能保存所有actionHandler,并且能返回一个reducer,同时还能在dispatcher时更新状态

export type Reducer<S = any, A extends Action = AnyAction> = (state: S | undefined, action: A) => S

actionHandler应该接受一个actionType和一个接受stateactionPayload并返回statehandler

并保存在一个reducerContainer

//                                      actionType (state,actionPayload) => state
const count = new CreateReducer(1).addAction('add',(state,action)=>{
     // 必须返回和state类型相同的value
     return state + action.count
})
export class CreateReducer<S, A> {
  private reducerObj: object
  private state: S
  constructor(state: S) {
    this.state = state // reducer 默认state,redux初始化调用该reducer时会传入undefined,此时state就会是我们定义的默认值
    this.reducerObj = {}
  }

  addAction(action: L, handler: (state: S, action: Partial<Action & A>) => S): this {
  // 如果reduxObj中存在相关action的handler则报错,否则将相关handler保存至容器,在reducer调用时取出
    if (Reflect.has(this.reducerObj, action)) throw new Error('The action exists')
    this.actionList.push(action)
    Reflect.set(this.reducerObj, action, handler)
    return this
  }
  finish(): Reducer<S, Action & A> {
  // 返回一个reducer
  // 该reducer会在reducerObj内找到actionType相同的handler,并调用返回成为新的state
  // 如果没有找到就返回传入的state, 初始化时redux的state为undefine,所以返回的state = this.state
    const ret = (State: S = this.state, action: Action & A) => 
      const fn = Reflect.get(this.reducerObj, action.type)
      if (typeof fn === 'function') return fn.call(null, State, action)
      return State
    }
    return ret
  }
}

如果你的reducercombineReducers包裹,那么你的reducer会在这里得到调用

否则会在这里被执行

现在我们完成了reducer的生成,那该怎么触发更新呢?这是一个难题,我们应该怎么在class里面获得reduxdispatch函数并触发更新?

或许我们可以破坏reducer的定义...

export type Reducer<S = any, A extends Action = AnyAction> = (state: S | undefined, action: A, dispatch?: Dispatch<A>) => S

我们在redux调用reducer时多传入一个dispatch函数进去

redux createStore内部改造后的reducer调用

currentState = currentReducer(currentState, action, dispatch as Dispatch)

redux combineReducers内部改造后的reducer调用

// previousStateForKey 为state 初始化时候为空
// action 初始化时为redux内部的常量,用于获取default值
// dispatch 我们传入的dispatchHandler 
const nextStateForKey = reducer(previousStateForKey, action, dispatch)

改造一下我们的CreateReducer


type stateHandlerType<S, A> = (state: S) => A

export class CreateReducer<S, A> {
  private reducerObj: object
  private state: S
  private dispatch: Dispatch<Action & A> | undefined
  constructor(state: S) {
    this.state = state
    this.reducerObj = {}
  }

  addAction(action: L, handler: (state: S, action: Partial<Action & A>) => S): this {
    if (Reflect.has(this.reducerObj, action)) throw new Error('The action exists')
    Reflect.set(this.reducerObj, action, handler)
    return this
  }
  private setDispatch(dispatch: Dispatch<Action & A>) {
    this.dispatch = dispatch
  }
  finish(): Reducer<S, Action & A> {
    const ret = (State: S = this.state, action: Action & A, dispatch?: Dispatch<Action & A>) => {
     // dispatch为我们传入的redux内部的dispatch function
     // 保存起来在dispatcher函数中使用
      dispatch && this.setDispatch(dispatch)
      const fn = Reflect.get(this.reducerObj, action.type)
      if (typeof fn === 'function') return fn.call(null, State, action)
      return State
    }
    return ret
  }
// actions actionType
// states 也就是你传入的action data
  dispatcher(actions: L, states?: Partial<A> | stateHandlerType<S, Partial<A>>): void {
    if (!this.dispatch) {
      throw new Error('dispatch does not exist')
    }
    if (!Reflect.has(this.reducerObj, actions)) {
      throw new Error(`Did you add the ${actions} action?`)
    }
    let stateHandler: undefined | A
    if (typeof states === 'function')
      stateHandler = states(this.state)
    else stateHandler = states
   // 通过redux提供的 bindActionCreators 函数传入action和dispatch 来帮助更新
    bindActionCreators(() => ({ type: actions, ...stateHandler }), this.dispatch as any)()
  }
}
// 完整的调用方式,带上泛型,当然如果不带也会被自动推导出来
const count = new CreateReducer<number,{ count: number }>(1).addAction('add',(state,action)=>{
     return state + action.count
})

const store = createStore(combineReducers({ count: count.finish() }))

store.getState()  // { count: 1 }

// 该有类型提示
count.dispatcher('add',{ count: 1 })

store.getState()  // { count: 2 }

解决重复更新

当你使用combineReducers时你的每一个reducer在更新时都会被循环传入action调用

为什么会被循环调用?

在初始化时,传入redux内部指定的actions,命中你的reducer default分支获取默认值,并保存在redux的中,这没有问题

但问题是当你的reducer变多时,每一次更新都会命中一些无用reducerdefault分支,除非你的default分支会有一些计算逻辑,否则default命中就是多余的

如何解决

首先我们可以将所有的actionType保存在reducer中,形成一个数组

并在reducer调用时判断传入的action是否在此次的reducer actionTypeList

如果本次的actionType与此次reducer的更新无关可直接跳过

所以我们又要改造一下我们的CreateReducer class


type stateHandlerType<S, A> = (state: S) => A

export class CreateReducer<S, A, L extends string> {
  private reducerObj: object
  private state: S
  private dispatch: Dispatch<Action & A> | undefined
  private actionList: string[]
  constructor(state: S) {
    this.state = state
    this.reducerObj = {}
    this.actionList = []
  }

  addAction(action: L, handler: (state: S, action: Partial<Action & A>) => S): this {
    if (Reflect.has(this.reducerObj, action)) throw new Error('The action exists')
    // 将传入的action保存到actionList中
    this.actionList.push(action)
    Reflect.set(this.reducerObj, action, handler)
    return this
  }
  // 获取actionList
  getActionList() {
    return this.actionList
  }
  private setDispatch(dispatch: Dispatch<Action & A>) {
    this.dispatch = dispatch
  }
  finish(): Reducer<S, Action & A> {
    const ret = (State: S = this.state, action: Action & A, dispatch?: Dispatch<Action & A>) => {
      dispatch && this.setDispatch(dispatch)
      const fn = Reflect.get(this.reducerObj, action.type)
      if (typeof fn === 'function') return fn.call(null, State, action)
      return State
    }
    // 在reducer的prototype中添加一条关于actionList的属性,保存了该reducer的所有action
    ret.prototype.getActionList = this.getActionList()
    return ret
  }
  dispatcher(actions: L, states?: Partial<A> | stateHandlerType<S, Partial<A>>): void {
    if (!this.dispatch) {
      throw new Error('dispatch does not exist')
    }
    if (!Reflect.has(this.reducerObj, actions)) {
      throw new Error(`Did you add the ${actions} action?`)
    }
    let stateHandler: undefined | A
    if (typeof states === 'function')
      stateHandler = states(this.state)
    else stateHandler = states
    bindActionCreators(() => ({ type: actions, ...stateHandler }), this.dispatch as any)()
  }
}

并在combineReducers函数中添加一段代码

// 是否是第一次初始化
let mount = true

combineReducers 的返回的reducer中添加这样一段逻辑

for (let i = 0; i < finalReducerKeys.length; i++) {
      // 获取每个reducer的key
      const key = finalReducerKeys[i]
      // 在reducerlist中找出对应的reducer handler
      const reducer = finalReducers[key]

      if (!mount) {
        // 在非初始化时才会进入的逻辑
        // 当前action不存在该reducer的所有action中, 表示该reducer与这次update无关,直接跳过
        if (!(reducer.prototype.getActionList as string[]).includes(action.type)) continue
      }
      // 找出该reducer对应的state
      const previousStateForKey = state[key]
      // 带上该state 调用reducer
      const nextStateForKey = reducer(previousStateForKey, action, dispatch)
      // 再次判断reducer的返回值
      if (typeof nextStateForKey === 'undefined') {
        const actionType = action && action.type
        throw new Error(
          `When called with an action of type ${actionType ? `"${String(actionType)}"` : '(unknown type)'}, the slice reducer for key "${key}" returned undefined. ` +
            `To ignore an action, you must explicitly return the previous state. ` +
            `If you want this reducer to hold no value, you can return null instead of undefined.`
        )
      // 初始化对应reducer的value
      nextState[key] = nextStateForKey
      // 通过判断当前reducer的返回值是否等于传入的state判断是否change
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
mount = false

大功告成

源码地址 内含redux的关键逻辑注释

已封装成包