Redux的核心原理,其实很简单

Monday, Mar 01, 2021

Flux 架构

Redux 是基于 Facebook 提出的 Flux 架构设计出来的。Flux 不是一个框架或者库,可以认为 Redux 是 Flux 的一种实现形式。Flux 架构强调数据应该是单向的数据流。

Flux 架构将应用拆分成四个部分:

flux

  • View(视图层):UI 界面
  • Action(动作):View 会触发的一系列事件动作
  • Dispatcher(分发器):对 Action 进行分发
  • Store(数据层):存储应用的数据状态,store 的变化最终会映射到 View 上。

flux

单向数据流的优势在于:对于数据装填的变化是可预测的。如果 store 中的状态发生了变化,那么一定是因为 dispatch 了某个 action。所以在 Redux 官网映入眼帘的就是:

Redux:A Predictable State Container for JS Apps

了解了 Flux 架构之后再来看 Redux 就会更容易理解。

Redux 核心原理

Redux 核心组成有三部分:

  • Store:存储数据状态的容器
  • Action:动作
  • Reducer:一个函数,接受两个参数,第一个参数是当前的 state,第二个参数是 action。reducer 负责根据 action 对状态进行处理。为什么叫 Reducer,是因为 Reducer 函数可以作为 Array 的 reduce 函数的参数。

Flux 的 dispatch 哪去了?Redux 并不是严格遵循 Flux 架构设计的,dispatch 在 Redux 中被整合到了 Store 中。

Redux 整个工作过程中,数据流是严格单向的,只能通过 dispatch action 的方式触发数据状态的修改。Action 会进入对应的 Reducer 进行处理最终得到新的状态 State,然后进一步的触发 View 的数据更新。

我们使用 Redux 最重要的一步就是通过 createStore 创建一个 store:

createStore 接受三个参数:

  • reducer
  • 初始状态 initState
  • enhancer, 对 createStore 能力进行增强的函数,如 applyMiddleware,添加一些中间件

具体做了什么事情?下面我简化了 createStore 方法的逻辑(去掉了边界 case 相关的代码)并对每一步进行了注释:

export default function createStore(reducer, preloadedState) {
  // 保存reducer的变量
  let currentReducer = reducer
  // 保存state的变量
  let currentState = preloadedState
  // 订阅状态的改变,在state改变之后会触发里面的监听事件
  let currentListeners = []

  // 获取当前的state
  function getState() {
    return currentState
  }

  // 订阅函数
  function subscribe(listener: () => void) {
    let isSubscribed = true
    // 将订阅函数放到listeners队列中,state更新后会一次调用里面的函数
    currentListeners.push(listener)
    // 返回一个取消订阅的函数
    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      isSubscribed = false

      const index = currentListeners.indexOf(listener)
      currentListeners.splice(index, 1)
    }
  }

  // 分发action的函数
  function dispatch(action) {
    // 执行reducer,根据action生成新的state保存到currentState
    currentState = currentReducer(currentState, action)

    const listeners = currentListeners
    // 依次触发listeners中订阅的函数,更新UI
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }

  // 这里触发一个dispatch,不会命中reducer里面的任何逻辑,
  // 所以直接走default,返回初始的state,达到设置初始默认值的目的
  dispatch({ type: ActionTypes.INIT })
  // 闭包
  const store = {
    dispatch: dispatch,
    subscribe,
    getState,
    // ...
  }
  return store
}

redux 中间件

redux 中间件使用方式是通过 applyMiddleware 方法,applyMiddleware 接受任意个数个中间件作为参数,在一开始介绍 createStore 的时候用到了applyMiddleware 方法。

applyMiddleware 函数实际的作用是对 store 的 dispatch 方法进行增强。下面对 applyMiddleware 方法每一步进行了注释:

export default function applyMiddleware(
  ...middlewares
) {
  // 返回一个函数,接受参数是createStore方法。
  return (createStore) => <S, A extends AnyAction>(
    reducer: Reducer<S, A>,
    preloadedState?: PreloadedState<S>
  ) => {
    // 调用createStore,创建一个store
    const store = createStore(reducer, preloadedState)
    let dispatch: Dispatch = () => {
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.'
      )
    }
    // middleware接受的参数,一个middleware实际上就是一个函数
    // 参数包含两个属性,getState和dispatch,所以一个redux的中间件需要接受并使用这两个方法
    const middlewareAPI: MiddlewareAPI = {
      getState: store.getState,
      dispatch: (action, ...args) => dispatch(action, ...args)
    }
    // 遍历middleware数组,给middleware数组传递上面的middlewareAPI参数,得到一个新的函数数组
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 得到一个新的dispatch方法,替换原有store的dispatch方法。
    // 新的dispatch方法通过compose方法将上一步得到的函数数组组合成一个函数,具体如何做到的,下面会描述。
    dispatch = compose<typeof dispatch>(...chain)(store.dispatch)
    // 返回一个新的store对象,dispatch是通过compose函数得到的新的dispatch方法
    return {
      ...store,
      dispatch
    }
  }
}

业务代码中调用的 dispatch 实际上会将传过来的 action 经过各个 Middleware 处理后调用 createStore() 返回的真正的 dispatch。

applyMiddleware 最终最需要搞清楚的就是 compose 函数是如何将 middleware 函数数组组合成一个函数的?函数合成(compose)并不是 Redux 的专利,它是函数式编程的一个概念,在 Koa 中也可以看到一个 compose 函数是相同的事情。compose 函数的代码非常精简:

export default function compose(...funcs: Function[]) {
  // 处理数组为空的边界case
  if (funcs.length === 0) {
    return (arg) => arg
  }
  // 处理数组为1的情况,这种情况下也不需要组合,直接返回第一个元素就行
  if (funcs.length === 1) {
    return funcs[0]
  }
  // 有多个函数,通过 array的reduce方法来组合
  return funcs.reduce(
    (a, b) =>
      (...args: any) =>
        a(b(...args))
  )
}

如果你还不了解reduce 方法,那么这是一个绝好的机会去真正认识到 reduce 函数的威力。reduce 函数的特点就是会执行 reduce 函数参数的逻辑,将数组中的数组组合成一个结果。

假设我们执行 compose(f1,f2,f3) ,得到的结果就是:

;(...args) => f1(f2(f3(...args)))

通过 compose 函数,我们就可以将多个函数整合成一个函数。

那 redux 的中间件又是一个什么结构呢? 这里我们找一个 redux 最常见的中间件redux-thunkredux-thunk 是 redux 中经典的一步 action 解决方案。redux-thunk 非常简洁,只有几行代码:

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) =>
    (next) =>
    (action) => {
      // 当action是一个函数的时候,调用这个函数,传递dispatch、getState给action
      // 在action函数中去处理异步逻辑,调用dispatch
      if (typeof action === 'function') {
        return action(dispatch, getState, extraArgument)
      }
      // 如果不是函数,就是一个正常的同步action,直接dispatch
      return next(action)
    }
}

const thunk = createThunkMiddleware()
thunk.withExtraArgument = createThunkMiddleware

export default thunk

看到createThunkMiddleware函数的返回函数接收的参数是不是非常熟悉?这里就是 applyMiddleware中向 middleware 传递的 middlewareAPI。

一个 redux middleware 的结构就是这样的:

;({ dispatch, getState }) =>
  (next) =>
  (action) => {
    /*  */
  }

compose 函数最终会将函数合并成:

;(...args) => middleware1(middleware2(middleware3(args)))

applyMiddleware 中给 compose 合成的函数传递的参数是 store.dispatch,所以({ dispatch, getState }) => (next) => (action) => { /* */ }中的这个 next 参数就是store.dispatch。所以中间件最终会调用到 store 的 dispatch ,完成 action 的分发,中间件的作用是对 dispatch 的能力进行增强(最终还是要靠 dispatch 方法),比如redux-thunk使得dispatch可以处理异步逻辑。

当然你可能并不需要 Redux

任何技术设计、思想框架,与其说他们的优缺点,不如用“适合不适合”某类场景来表达更加准确、客观。

作为 Redux 的 creators 之一的 Dan Abramov 在很久之前也说过 “You might not need Redux”。Redux 的官网中也有关于 When should I use Redux 的版块。对于一个拥有复杂状态更新频繁的 App 使用 redux 可能会带来一些维护上的优势,但是对于轻量简单的应用来讲,Redux 的使用就不是那么必要的了。使用 Redux,开发者必须要写很多模板代码,这种重复和繁琐可能不是开发者所能忍受的。在 React 中,React hooks 的出现可能是对 redux 的一次降维打击,对于是否使用 Redux,还是需要根据具体业务场景进行一次"trade-off"。