上期回顾

首先我们先来回顾一下上一篇文章我们总结的几个痛点:

  • 低入侵

  • 异步 Action

  • 跨组件获取 Store

  • 模块化

  • 辅助函数

然后我们针对以上痛点,一步步实现对 Redux 的封装。

API 设计

在开始编码之前,容我们先花一点时间按照痛点来定一下 API 应该是什么样子的。

首先,Redux 的注入应该是非常简单的,不入侵我们业务代码的,类似 Dva 那种彻底改变应用启动方式的方式不是首选方案。所以,采用修饰器是相对来说比较稳妥的方案,不用在原来业务代码上做太多的修改。

// 根组件注入 Redux
@reduxRoot({ store })
class Index extends PureComponent {
  // xxx
}
// 子组件注入 Redux (为了子组件可以直接获取整颗 store 树,并提供辅助函数)
@reduxChild()
class Child extends PureComponent {
    // xxx
}

再者,我们还需优化原生 Redux 将 Reducer、Action 等概念散落在不同文件且区分不明确的方式。

// 用 object 组织 store 树
export default {
  state: {}, // 状态集合
  reducers: { // 真正修改 state 的同步函数集合
    ADD(state, payload) {
      // xxx
    }
  },
  actions: { // 触发 reducers 的函数集合,支持异步
    add({ dispatch, commit, state }, payload) {
      dispatch(/* xxx */)
      // or
      commit(/* xxx */)
    },
    asyncAdd({ dispatch, commit, state }, payload) {
      setTimeout(() => {
        dispatch(/* xxx */)
      }, 1000)
    },
  },
  modules: {}, // 同样结构嵌套的模块集合
}

最后,需要统一一下调用 Action 和 Reducer 的方式,为了区分这两种,Action 用 dispatch 方法触发,Reducer 用 commit 方法触发,两者调用方式类似

// 调用 Action
this.props.dispatch('add', 1, state => {})
this.props.dispatch('add', 1).then(state => {})
this.props.dispatch('asyncAdd', 1).then(state => {})
this.props.dispatch({ type: 'asyncAdd', payload: 1 }).then(state => {})
this.props.dispatch('moduleA/add', 1, state => {}) // 模块化 提交
// 调用 Reducer
this.props.commit('ADD', 1)
this.props.commit({ type: 'ADD', payload: 1 })
this.props.commit('moduleA/ADD', 1) // 模块化 提交

接下来我们来看代码实现

嵌套模块化的实现

export default {
  state: {},
  reducers: {},
  actions: {},
  modules: {},
}

当 modules 里嵌套多层的时候,我们可以将整颗 store 树当做一个叉树来看,需要完成整合所有state,根据路径获取 module 中的值,根据路径更新某 module 的 state 等功能。

export default class Module {
  constructor(rawModule) {
    this.state = null
    this._rawModule = rawModule
    this.collectionState([], rawModule)
  }
  // 分割路径
  getPath(pathString) {
    return pathString.split('/').slice(0, -1)
  }
  // 根据路径获取具体 state
  getState(path) {
    return path.reduce((state, key) => state[key], this.state)
  }
  // 根据路径获取具体 rawModule
  getRawModule(path) {
    return path.reduce((module, key) => {
      if (!module.modules) return null
      return module.modules[key]
    }, this._rawModule)
  }
  // 获取 module 中的 actions 列表
  getActions(module) {
    return {
      ...(module.actions || {}),
    }
  }
  // 更新整棵树
  updateState(path, nextState) {
    if (path.length === 0) return this.state = nextState
    return chainDefine(this.state, path, nextState)
  }
  // 递归整颗树,获取 state 集合
  collectionState(path, rawModule) {
    if (path.length === 0) {
      this.state = rawModule.state
    } else {
      const parent = this.getState(path.slice(0, -1))
      parent[path[path.length - 1]] = rawModule.state
    }
    if (rawModule.modules) {
      objForEach(rawModule.modules, (rawChildModule, key) => {
        this.collectionState(path.concat(key), rawChildModule)
      })
    }
  }
}
// 遍历对象
export function objForEach(obj, callback) {
  Object.keys(obj).forEach(key => callback(obj[key], key))
}
// 链式赋值
export function chainDefine(map, path, value) {
  if (path.length === 1) return map[path[0]] = value
  return chainDefine(map[path[0]], path.slice(1), value)
}

ReduxRoot + ReduxChild

完成了基础类再来完成最关键的 Redux 注入部分


import { createStore } from 'redux'
// 为根组件注入 Redux
function reduxRoot({
  store,
}) {
  return OriginComponent => {
    const reactReduxStore = createStore(() => {
      // 实现处理不同 reducer 的逻辑
    })

    class RootStoreComponent extends PureComponent {
      getChildContext() {
        return {
          commit: this.commit,
          dispatch: this.dispatch,
          originStore: reactReduxStore, // Redux 原始对象,可调用 getState 等方法
        }
      }

      commit() {
        // 用于提交 Reducers
      }

      dispatch() {
        // 用于提交 Actions
      }

      render() {
        return (
          <OriginComponent />
        )
      }
    }

    const ConnectComponent = connect(state => ({ state }))(RootStoreComponent)

    class ProviderComponent extends PureComponent {
      render() {
        return (
          <Provider store={reactReduxStore}>
            <ConnectComponent />
          </Provider>
        )
      }
    }

    return ProviderComponent
  }
}
// 为子组件注入 Redux
function reduxChild(options = {}) {
  return OriginComponent => {
    class ChildStoreComponent extends PureComponent {
      static contextTypes = {
        commit: PropTypes.func,
        dispatch: PropTypes.func,
        originStore: PropTypes.object,
      }
      render() {
        return (
          <OriginComponent {...this.props} />
        )
      }
    }
    return connect(state => ({ state }))(ChildStoreComponent)
  }
}

整体上的思路就是通过装饰器,在根组件上使用 Provider组件 和 connect方法,初始化 Redux,在子组件上通过 context 将跟组件的 commit、dispatch 等方法挂载到子组件,并通过 connect 将 state 注入到子组件中。

Dispatch、Commit 及 异步 Action

其实异步 Action 的实现相对简单,因为在原生 Redux 中 Action 更像是一个常量,如果我们将 Action 看做一个函数,在 Action 函数执行完毕后,再真正调用 this.props.dispatch 触发原生 reducer 即可实现异步 Action。

// ...
commit() {
  const formattedArguments = normalizeArguments(args)
  // 此处的 dispatch 为 原生 Redux 的方法,提交后,全部走到 createStore 的回调函数中处理相关逻辑
  this.props.dispatch({
    type: formattedArguments.type,
    payload: formattedArguments.payload,
  })
}
dispatch(...args) {
  return new Promise(resolve => {
    // 格式化参数,支持载荷和对象两种提交方式
    const formattedArguments = normalizeArguments(args)
    const type = formattedArguments.type
    const payload = formattedArguments.payload
    const callback = formattedArguments.callback
    // 等待 action 执行完成再 向 redux dispatch 真实 action,实现异步 action
    const splitArray = type.split('/')
    const actionFuncName = splitArray[splitArray.length - 1]
    const path = StoreModules.getPath(type)
    const rawModule = StoreModules.getRawModule(path)

    function dispatchHelper(...argsArr) {
      // argsArr  ['reducerName', payload]
      // 子模块提交需要修正 reducerName
      let fixedReducerName = argsArr[0]
      if (splitArray.length > 1) fixedReducerName = splitArray.slice(0, splitArray.length - 1).concat([fixedReducerName]).join('/')

      // 触发真正的 reducer
      this.commit(fixedReducerName, argsArr[1])
      resolve(this.getState())
      callback(this.getState())
    }

    const actionsCollect = StoreModules.getActions(rawModule)

    // 将相应参数传入到 Action 函数中以便调用
    return actionsCollect[actionFuncName]({
      commit: dispatchHelper.bind(this), // 在 Action 中执行完异步操作,触发 commit 函数,才会触发 store 的更新
      state: StoreModules.getState(path),
      dispatch: this.dispatch,
    }, payload)
  })
}
// ...
// 工具函数
// 将参数格式化成统一格式
function normalizeArguments(args) {
  let type
  let payload
  let callback
  if (typeof args[0] === 'object') {
    type = args[0].type
    payload = args[0].payload
    callback = args[1] || function () {}
  } else {
    type = args[0]
    if (typeof args[1] === 'function') {
      callback = args[1]
    } else {
      payload = args[1]
      callback = args[2] || function () {}
    }
  }
  return {
    type,
    payload,
    callback,
  }
}

dispatch 和 commit 的主要思路是 dispatch 用于执行 store 中定义的 action 函数,在 action 中可以用 commit 方法触发原生的 reducer,在原生 reducer 中触发 store 中定义的各种 reducer 函数,来达到更新 state 的目的,接下来我们看 createStore 中的回调函数如何实现分发 reducer。

import { createStore, applyMiddleware, compose } from 'redux'
// 支持 Redux DevTools
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const reactReduxStore = createStore((...args) => {
  const subscriber = args[1]

  // 过滤 redux 的初始化事件
  if (/@@/.test(subscriber.type)) return StoreModules.state
  const path = StoreModules.getPath(subscriber.type)
  const splitArray = subscriber.type.split('/')
  const reducerFuncName = splitArray[splitArray.length - 1]
  const rawModule = StoreModules.getRawModule(path)
  const rawModuleState = StoreModules.getState(path)
  if (!rawModule) return StoreModules.state
  if (!rawModule.reducers || !rawModule.reducers[reducerFuncName]) {
     // reducer func 不存在
    return StoreModules.state
  }

  // 调用 store 中定义的 reducer 函数,来获取新的 state
  const nextState = rawModule.reducers[reducerFuncName](rawModuleState, subscriber.payload)
  // 以下一个状态更新当前 state 树
  StoreModules.updateState(path, nextState)
  // 返回全新的 state,来达到更新的目的
  return {
    ...StoreModules.state,
  }

  // 处理 Redux 中间件逻辑,在 reduxRoot 初始化是可注入多个 middlewares
}, composeEnhancers(applyMiddleware(...middlewares)))
// xxx

到这里,我们上面提到的痛点大部分都已解决,以前需要理解多个概念,多写很多代码的 “学院派” Redux,已经离我们而去,剩下的是代码一看即懂的 “简洁版” Redux。

还能更方便更简洁嘛?当然。

辅助函数

在真实的业务场景中,我们并不用将所有的 state/action/reducer 都注入到组件的 props 上,所以我们可以实现 mapStateToProps、mapActionsToProps、mapReducersToProps 这样的辅助函数来简化我们的组件属性。

// 辅助函数的调用
@reduxRoot({
  store,
  mapStateToProps: { // 可以对 state 中的值进行二次计算再返回
    index: state => state.index,
    childIndex: state => state.child.index, // 子 module
  },
  mapReducersToProps: ['ADD', 'child/ADD'],
  mapActionsToProps: ['asyncAdd', 'add', 'child/add', 'child/asyncAdd'],
})
class Index extends PureComponent {
  // xxx
}
// 或者
@reduxRoot({
  store,
  mapStateToProps(state) {
    return {
      index: state.index
    }
  },
  mapReducersToProps: {
    SELF_ADD: 'ADD'
  },
  mapActionsToProps: {
    selfAdd: 'add'
  }
})
class Index extends PureComponent {
  // xxx
}

不管是对象形式还是函数形式,最终都是会返回一个对象,在 render 过程中,可以将三个辅助函数的计算结果通过解构传入到组件中,以便组件中直接使用。

// xxx
render() {
  const mapStateResult = mapStateToPropsHelper(mapStateToProps, this.getState())
  const mapActionsResult = mapActionsToPropsHelper(mapActionsToProps, this.dispatch)
  const mapReducersResult = mapReducersToPropsHelper(mapReducersToProps, this.commit)
  return (
    <OriginComponent {...this.props}
      {...mapStateResult}
      {...mapActionsResult}
      {...mapReducersResult}
      dispatch={this.dispatch}
      commit={this.commit}
      store={{
        state: this.getState(),
        dispatch: this.dispatch,
        commit: this.commit,
        originStore: reactReduxStore,
      }}
    />
  )
}
// xxx
// 子组件同理,不再赘述

至此,一个 “简洁版” Redux 已经可以在生产环境使用,当然,还有一些边界情况没有在伪代码中反映出来,比如 Action、Reducer 不存在时的提示等。