前言

react-redux在实现react前端项目中使用还是比较频繁的,虽然用了比较多次。但是对原理的理解还是不够深入,所以今天回顾下redux。

redux

redux是什么,为什么要用redux,这应该是许多初步接触redux开发者的疑问 这里摘抄官方文档的说明

what

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。

why

随着 JavaScript 单页应用开发日趋复杂,JavaScript 需要管理比任何时候都要多的 state。(状态)state 在什么时候,由于什么原因,如何变化已然不受控制。

redux通过一些原则限制状态更新发生的时间和方式,让这些变化变得可测

三大原则

  • 单一数据源
  • state是只读
  • 使用纯函数来执行(reducer修改state状态,reducer是纯函数)

数据流

严格的单向数据流是 Redux 架构的设计核心。 redux 应用中数据的生命周期(数据流动)

  • 调用 store.dispatch(action)
  • Redux store 调用传入的 reducer 函数。
  • 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
  • Redux store 保存了根 reducer 返回的完整 state 树。

redux源码阅读

function dispatch(action) {
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }

    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant?'
      )
    }

    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }
export default function createStore(reducer, preloadedState, enhancer){
    let currentReducer = reducer;
    ....
}

dispatch 将 currentState传入 currentReducer,执行currentReducer函数, currentReducer是createStore时候传入的reducer
所以可以看出dispatch是这里将action传递给reducer

疑问:dispatch的匹配原理(或者说action与reducer的匹配原理)是什么?

开始我理解的就是,每次dispatch一个action,会只走到对应reducer中,去匹配对应的action执行相应代码,然后我将官方例子的代码修改了一下,去验证是否对的

let id = 1000;// 增加的
const todos = (state = [], action) => {
    console.log('todos',state)
    switch (action.type) {
      case 'ADD_TODO':
        return [
          ...state,
          {
            id: action.id,
            text: action.text,
            completed: false
          }
        ]
      case 'TOGGLE_TODO':
        return state.map(todo =>
          (todo.id === action.id) 
            ? {...todo, completed: !todo.completed}
            : todo
        )
      //修改的
      default:
        return [
          ...state,
          {
            id: id++,
            text: `default ${id++}`,
            completed: false
          }
        ]
    }
  }
  
  export default todos

结果就是dispatch非 ADD_TODO or TOGGLE_TODO的时候,也会增加todo任务 就证明了我的猜想是错误的,dispatch给reducer后,reducer会将这个action匹配给它下面所有子reducer,这也说明了为什么action type不能重复命名的原因 还有规范的开发中,需要用actionType.js,用常量定义actiontype,方便协同开发避免出错

疑问:数据流的第三步,“根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。” 这里怎么理解,store tree是怎么形成的

阅读源码 combineReducer

....
export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  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]
    }
  }
  const finalReducerKeys = Object.keys(finalReducers)

  let unexpectedKeyCache
  if (process.env.NODE_ENV !== 'production') {
    unexpectedKeyCache = {}
  }

  let shapeAssertionError
  try {
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

  return function combination(state = {}, action) {
    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 = {}
    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)
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    return hasChanged ? nextState : state
  }
}

可以看到combineReducers将reducers的每个子节点(key)都获取出来 reducerKeys 中途校验子节点,最后得到 finalReducerKeys 最终的一个节点列表。 然后循环这个节点列表,构造state tree,以下这两句就是构造state tree的过程
const nextState = {}nextState[key] = nextStateForKey 但是你会发现,在createStore后,执行store.getState(),也能输出完整的state tree为何呢 查看源码发现在createStore.js中有 dispatch({ type: ActionTypes.INIT }),完成初始化的过程

总结

这次回顾redux,收获的不仅是对redux原理的理解,还有学习源码的方式,因为很多源码晦涩难懂 而且读着读着就走偏,不是所有都是你关心的内容,所以认识到带着问题去阅读源码是比较高效的一种方式 下面是我这次源码阅读的思路

问题驱动型阅读源码方式

在重新阅读redux 中文文档的时候,有了以下的疑问,发现文档理也没有深入说明所以就带着疑问驱动去阅读框架的源码

  • 理解redux的数据流提出疑问
  • 对store tree 形成的好奇探究 => combineReducer
  • 对dispatch 匹配规则的深究 => createStore.dispatch