【react进阶(二)】:教你正确理解、合理使用Redux及Redux的源码分析

935 阅读12分钟

教你正确理解、合理使用Redux及Redux的源码分析

通过本文你会学到什么

  • 我们为什么要使用Redux
  • Redux应如何使用
  • 从源码分析Redux
  • Redux的弊端
  • Redux优秀实践

一、我们为什么要使用Redux

核心:合理高效的管理我们应用的状态;

如果我们没有Redux会如何进行状态共享
  1. 在React中我们会使用父子组件通过props进行状态传递;
  2. 在React中通过context API进行状态共享;
  3. 或者你直接通过localstorage等本地存储手段进行数据共享;

这些管理方式会带来什么样的麻烦呢?

  1. 组件嵌套层次过深我们来回传递贼鸡儿麻烦;
  2. 可能需要共享状态的组件没有组件层级关系;
  3. context API拿来传递些全局状态比如说主题、字体等还行,复杂的状态也不是能很好的管理;(旧版本的React context API当状态变更还会带来无法rerender的问题,这里就不做展开;)
  4. 状态无法预测,当出现bug的时候比较难定位问题;

二、Redux如何使用

  1. createStore方法创建一个store;(一个store中是一个对象树)
  2. store.subscribe创建变化监听器;(每当 dispatch action 的时候就会执行)
  3. store.dispatch(action)触发action,action是描述发生什么的一个对象;
  4. reducer接收上一次的state(或是初始state)和action,并返回新的 state 的函数;
Redux的三大核心原则
  1. 单一数据源:整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
  2. State是只读的:唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
  3. 使用纯函数修改:为了描述 action 如何改变 state tree ,你需要编写 reducers
代码示例
// reducer
const counter = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}

// 创建store
const store = createStore(counter)

const rootEl = document.getElementById('root')
const render = () => ReactDOM.render(
  <Counter
    value={store.getState()}
    onIncrement={() => store.dispatch({ type: 'INCREMENT' })}
    onDecrement={() => store.dispatch({ type: 'DECREMENT' })}
  />,
  rootEl
)
render()

// 注册监听器
store.subscribe(render)

三、从源码分析Redux

Redux的源码实际上比较少,核心可能就两百行,比较适合初读源码的小伙伴们,下面我们一起通过源码来探究以下Redux的原理。

1. createStore

createStore这个方法接受三个参数:

  1. reducer:接受一个函数(这个函数接受两个参数:state,action,返回新的state树)
  2. preloadedState:初始时的 state(前后端同构的时候也可以起作用)
  3. enhancer:扩展store的功能

返回了一个store对象,这个对象包含了四个值

  1. dispatch: 接受一个plainobject的action,通过传入的Reducer计算出本次action之后的新state树
  2. subscribe: 注册监听:传入listener 存入nextListeners; 返回unsubscribe方法
  3. getState: 获取当前的state状态树
  4. replaceReducer:替换 store 当前用来计算 state 的 reducer。currentReducer =》 newReducer
export default function createStore<
  S,
  A extends Action,
  Ext = {},
  StateExt = never
>(
  // 接受一个函数(这个函数接受两个参数:state,action,返回新的state树)  
  reducer: Reducer<S, A>,
  // 初始时的 state。 在同构应用中,你可以决定是否把服务端传来的 state 水合(hydrate)后传给它,
  // 或者从之前保存的用户会话中恢复一个传给它。如果你使用 combineReducers 创建 reducer,它必须是一个普通对象,与传入的 keys 保持同样的结构  
  preloadedState?: PreloadedState<S> | StoreEnhancer<Ext, StateExt>,
  // 扩展store的功能  
  enhancer?: StoreEnhancer<Ext, StateExt>
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext {
    // 如果存在enhancer且是一个函数(高阶函数)
    if (typeof enhancer !== 'undefined') {
      if (typeof enhancer !== 'function') {
        throw new Error('Expected the enhancer to be a function.')
      }

      return enhancer(createStore)(
        reducer,
        preloadedState as PreloadedState<S>
      ) as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
    }  
 // 存储的是我们reducer函数
  let currentReducer = reducer
  // 目前的state
  let currentState = preloadedState as S
  // 存储监听器
  let currentListeners: (() => void)[] | null = []
  // 这里存储一份Listener是防止有人会在dispatch的过程中进行subscribe/unsubscribe的骚操作
  let nextListeners = currentListeners
  // 是否处于dispatch中
  let isDispatching = false
 
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      // slice: 该方法并不会修改数组,而是返回一个子数组。
      nextListeners = currentListeners.slice()
    }
  }
 // 获取当前的state
  function getState(): S {
    if (isDispatching) {
      throw new Error('简化')
    }

    return currentState as S
  }
 
  // 注册监听:传入listener 存入nextListeners; 返回unsubscribe方法
  function subscribe(listener: () => void) {
    if (isDispatching) {
      throw new Error('简化')
    }

    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      if (isDispatching) {
        throw new Error('简化')
      }

      isSubscribed = false

      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
      currentListeners = null
    }
  }
 
   // 接受一个plainobject的action
  function dispatch(action: A) {
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      isDispatching = true
      // 通过传入的Reducer计算出本次action之后的新state树
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }
  // 此时:nextListeners = currentListeners = listeners
    const listeners = (currentListeners = nextListeners)
    // 遍历执行监听器
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
  // 返回当前传入的action
    return action
  }
 
  // 替换 store 当前用来计算 state 的 reducer。currentReducer =》 newReducer
  function replaceReducer<NewState, NewActions extends A>(
    nextReducer: Reducer<NewState, NewActions>
  ): Store<ExtendState<NewState, StateExt>, NewActions, StateExt, Ext> & Ext {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }

    // TODO: do this more elegantly
    ;((currentReducer as unknown) as Reducer<
      NewState,
      NewActions
    >) = nextReducer

    dispatch({ type: ActionTypes.REPLACE } as A)
    // change the type of the store by casting it to the new store
    return (store as unknown) as Store<
      ExtendState<NewState, StateExt>,
      NewActions,
      StateExt,
      Ext
    > &
      Ext
  }

  // store创建自动dispatch的初始化action,创建我们的初始化state树
  dispatch({ type: ActionTypes.INIT } as A)

  const store = ({
    dispatch: dispatch as Dispatch<A>,
    subscribe,
    getState,
    replaceReducer
  } as unknown) as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
  return store
}

2. combineReducers

此方法就接收一个参数,这参数是一个reducerMap对象,类似{routing: routingReducer};

export default function combineReducers(reducers: ReducersMapObject) {
  // store tree上的key
  const reducerKeys = Object.keys(reducers)
  const finalReducers: ReducersMapObject = {}
  // 遍历拷贝传入的reducersMap到finalReducers
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]

    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  // finalReducerKeys也就是reducersMap的key的集合数组
  const finalReducerKeys = Object.keys(finalReducers)

  let shapeAssertionError: Error
  try {
    // 这里去判断了一下reducers的对应的reducer的合理性
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }
 
  // 返回了一个函数:也就是我们传入到createStore方法中的第一个参数,传入state,action,返回新的state树
  return function combination(
    state: StateFromReducersMapObject<typeof reducers> = {},
    action: AnyAction
  ) {
    if (shapeAssertionError) {
      throw shapeAssertionError
    }
  // 是否需要返回新state的标识
    let hasChanged = false
    const nextState: StateFromReducersMapObject<typeof reducers> = {}
    // 遍历去执行每个reducer,根据对应的key,生成新的nextState
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      const reducer = finalReducers[key]
      const previousStateForKey = state[key] // 这里previousStateForKey存储的是对于变化前的store树上key的state
      const nextStateForKey = reducer(previousStateForKey, action)
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      // nextState:新store tree
      nextState[key] = nextStateForKey
      // 如果有一个store tree上对应一个key的value改变了,那hasChanged就置为true
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    // 如果reducersMap keys不等于 state的keys 的length 也同样 hasChanged 为 true
    // 也就是说默认的state没有传,第一次的时候就会return nextState;nextState的value也就是reducers的默认值
    hasChanged =
      hasChanged || finalReducerKeys.length !== Object.keys(state).length
    return hasChanged ? nextState : state
  }
}

3. applyMiddleware

这个方法很重要,我们通过传入的中间件来达到增强我们store.dispatch方法的目的;

applyMiddleware方法接受一个数组作为参数: middlewares-传入数组<中间件>;

applyMiddleware方法是一个非常典型的函数式编程的写法,为什么这么说呢?

redux作者为啥不直接传入多个参数,还非要将这个函数分解为多个单参数函数进行调用呢?这就是函数式编程的理念,你品,你细品;

export default function applyMiddleware(
  ...middlewares: Middleware[]
): StoreEnhancer<any> {
  // 典型的函数式编程写法
  return (createStore: StoreCreator) => <S, A extends AnyAction>(
    reducer: Reducer<S, A>,
    ...args: any[]
  ) => {
    // 生成store
    const store = createStore(reducer, ...args)
    let dispatch: Dispatch = () => {
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.'
      )
    }

    const middlewareAPI: MiddlewareAPI = {
      getState: store.getState,
      dispatch: (action, ...args) => dispatch(action, ...args)
    }
    // 遍历去执行middleware的方法(传入参数getState、dispatch)
    // middleware返回一个接受next参数的函数
    // 所以chain实际上是存储了很多函数的数组
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 对正常创建的store.dispatch进行增强
    dispatch = compose<typeof dispatch>(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}
自己实现一个简单的Redux Middleware
// 这是我们的一个用来打印action和 dispatch后的state的middleware
function logger({ getState }) {
  return (next) => (action) => {
    console.log('will dispatch', action)

    // 调用 middleware 链中下一个 middleware 的 dispatch。
    let returnValue = next(action)

    console.log('state after dispatch', getState())

    // 一般会是 action 本身,除非
    // 后面的 middleware 修改了它。
    return returnValue
  }
}

4. compose

compose函数精髓所在:funs = [a,b,c,d,e] => return (...args) => a(b(c(d(e(...args)))))

这个方法在koa2的源码中也有体现,具体可看:Koa源码分析(中间件执行机制、Koa2与Koa1比较)

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]
  }
 // compose函数精髓所在:funs = [a,b,c,d,e] => return (...args) => a(b(c(d(e(...args)))))
  return funcs.reduce((a, b) => (...args: any) => a(b(...args)))
}

5. BindActionCreators

  • actionCreateors: : 一个 action creator,或者一个 value 是 action creator 的对象。
  • dispatch: 一个由 Store 实例提供的 dispatch 函数。
export default function bindActionCreators(
  actionCreators: ActionCreator<any> | ActionCreatorsMapObject,
  dispatch: Dispatch
) {
  // 如果传入的actionCreateors就是一个函数直接返回一个dispatch这个action的包装函数
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }
  // Map: key:ActionCreator的函数名,value: 一个dispatch这个action的包装函数
  const boundActionCreators: ActionCreatorsMapObject = {}
  for (const key in actionCreators) {
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

// 传入dispatch 和 actionCreator 返回dispatch(action)的包装函数
function bindActionCreator<A extends AnyAction = AnyAction>(
  actionCreator: ActionCreator<A>,
  dispatch: Dispatch
) {
  return function (this: any, ...args: any[]) {
    return dispatch(actionCreator.apply(this, args))
  }
}
// 代码片段:具体代码可查看https://www.redux.org.cn/docs/api/bindActionCreators.html
this.boundActionCreators = bindActionCreators(TodoActionCreators, dispatch);
console.log(this.boundActionCreators);
// {
//   addTodo: Function,
//   removeTodo: Function
// }
 let action = TodoActionCreators.addTodo('Use Redux');
 dispatch(action);

6. Redux-thunk源码分析

redux默认是同步action,但是作者也提供了一个可以进行异步操作的中间件;

// redux-thunk源码很简单
// 让我们action可以成为一个函数,这个函数可以访问到dispatch, getState这两个方法,
// 最后返回一个plainobject action就好了
function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => (next) => (action) => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;
thunk函数到底是啥

是函数式编程里面的特有名词,主要用于calculation delay,也就是延迟计算。

function wrapper(arg) {
  // 内部返回的函数就叫`thunk`
  return function thunk() {
    console.log('thunk running, arg: ', arg)
  }
}
// 我们通过调用wrapper来获得thunk
const thunk = wrapper('wrapper arg')

// 然后在需要的地方再去执行thunk
thunk()
说一说另一个解决异步操作问题的中间件-redux-saga

这个中间件应用的还是很广泛的,比如说阿里的dva都是集成我们的redux-saga; 核心是通过监听我们的action操作,执行对应的side effects的task,结合我们ES6的generator语法,配合saga提供的工具函数,可以非常方便的处理我们的异步问题;

四、Redux带来的弊端

Redux虽然带来了比较高级的函数式编程思想以及Immutable数据不可变的特性,但是我们普通开发者去很好的践行这些缺很累,为什么累? 我们一般使用react-redux的时候,都会定义很多的样板代码,特点的繁琐,比如说你的组织目录,需要有actions、reducers等等,简单的一个数据传递扭转,你需要修改多处的代码,而且这些代码都可能是差差不多的。

你可能还会带来心智负担,什么心智负担呢?

什么样的数据需要放在我们的store中呢,是否我所有的状态都需要集中管理呢?答案当然是否定的,我们需要将我们需要全局共享的状态放入store中,组件内的状态不需要共享的当然还是组件内的状态使用的好。(这里牵涉到一个性能问题,我们一般在react项目中都会集成react-redux这个库,如果你几乎每个组件都去connect订阅store中的数据,你会获得很多不需要的rerender,性能会大大降低。)

社区中有哪些解决方案

目前社区中为了解决我们Redux的这些弊端,诞生了很多优秀的类库,当然,它们的核心还是redux;

  1. dva
  2. rematch

六、Redux优秀实践

除了上面说的dvarematch,再推荐几个个类库辅助我们进行项目开发:

  1. reselect

帮助你在使用react-redux做的过程中,mapStateToProps方法不需要每次都重复计算state,会帮助你缓存计算的结果(当然,这是在你确实不需要重新计算的情况下,才会使用你缓存的结果);

  1. immutable-js or immer

都可以让你操作javascript的复杂数据类型时,没有心智负担,这里的心智负是指引用类型的副作用,你之前可能会通过深拷贝避免副作; 在redux的reducer中使用Object.assign或者ES6的... 运算符保持我们的reducer函数是纯的,这两个类库都可以方便的让你安全的操作state; 这两个库都是非常优秀的,但是immer是后起之秀,二者实现的原理也有区别,就不展开讨论了。

我们要学会合理的运用社区中优秀的方案解决我们的项目开发中的问题。没有最好的,只有适合的

欢迎共同学习

前端修炼指南

Node.js Koa2全栈实战

Flutter TodoList App开发

微前端实战与思考

typescript+mobx+react,拿走即用脚手架

参考以及优秀相关文章推荐

Redux 中文文档

redux 有什么缺点?

理解redux-thunk

2018 年我们还有什么功能是 Redux 才适合做的吗?

本文使用 mdnice 排版