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

2,153 阅读6分钟

Flux架构

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

  • View(视图层):UI界面
  • Action(动作):View会触发的一系列事件动作
  • Dispatcher(分发器):对Action进行分发
  • Store(数据层):存储应用的数据状态,store的变化最终会映射到View上。 单向数据流的优势在于:对于数据装填的变化是可预测的。如果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:

const store = createStore(reducer, initState, applyMiddleware(Middleware1, Middleware2...))

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"。

总结

本文主要介绍了Redux的核心原理。如果你觉得本文有帮助,还请点赞关注支持一下~