深入理解redux之reducer为什么是纯函数

3,989 阅读4分钟

reducer是redux的三个核心概念之一,它指定了应用状态的变化如何响应 actions 并发送到 store,需要由开发人员自己定义,提起reducer,最常想到的一个准则是reducer要是纯函数,那么这其中是什么原因呢,如果不是纯函数的话会导致redux不可用吗?接下来就来一步步分析下这个问题

reducer响应actions的原理

  我们定义的reducer负责接收action,并返回一个新的state,但在react组件开发中,我们也仅仅定义了而从未实际调用过reducer,所有reducer作用的原理是什么呢?

  实际上,组件中我们会手动调用dispatch方法发送action来响应事件,而这里的发送action不止发送action那么简单,还包括了调用reducer,更新状态为reducer的处理结果,触发订阅事件等一系列操作,都是在dispatch的方法内实现的,到这里可以来通过源码的处理流程来了解一些这个过程~

function dispatch(action) {
    ...
    try {
      //将flag置为true,表明处于分发逻辑中
      isDispatching = true 
      
      //currentReducer即为传入的reducer函数,这里会自动调用currentReducer函数,并将返回值赋给currentState
      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
}

  可以看到,dispatch处理中reducer方法的调用无疑是很关键的一步,经过它的处理之后才生成了新的状态数据   在redux中,保持reducer是一个纯函数非常重要,保证reducer是纯函数要达到以下几点:

  • 不得修改传入的参数
  • 不得调用非纯函数,如Date.now()
  • 不得执行有副作用的操作,如API请求和路由跳转

那么为什么reducer函数要遵守这几点规则呢,如果不遵守的话又会怎么样呢

纯函数条件之一:不得修改传入的参数

function reuderfunc(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return ...
    default:
      return state
  }
}

  reducer接受的参数由两个,一个是action,一个是state,其中action只是用来传递信息的,完全没有修改的必要,那么state呢,既然reducer是用来返回一个新的state的,在这个过程中直接基于传入的state来改,然后再返回,貌似也可以达到效果?   实际上,虽然在reducer处理过程中改变了传入的state,有可能redux还会正常运转,但像时间旅行,录制和回放这类依赖于历史状态的功能则无法实现了,要知道这可是redux当时的设计初衷之一~   特别注意的是,如果改变了传入的参数initState,或该对象底层的任意一个key值,都有可能在应用于react组件时,导致react任为该状态无变化,而不更新组件,因为redux源代码中将oldState和newState(reducer返回的结果)做比较,如果某一级state指定的引用相等,则会导致此结果,这样做是牺牲一点计算性能(生成新对象)来保证页面刷新。

纯函数条件之二:不得调用非纯函数,如 Date.now() 或 Math.random()

redux的核心提供可预测化的状态管理,即无论何时特定的action触发的行为永远保持一致,试想如果reducer中有Date.now()等非纯函数,即使同样的action,那么reducer处理过程中也是有所不同的,不再能保证可预测性

纯函数条件之二:执行有副作用的操作

  首先,执行有副作用的操作,如api和路由跳转,因为设置到后台的处理,会带来和上一节同样的问题,同样的action触发后的处理过程可能有所不同(依据后台处理或路由跳转而定),失去了可预测性   其次因为reducer函数的返回值是要作为下一个状态值被返回的,那么试想当reducer中有api调用时,api是会向后台请求数据的异步函数,往往希望后台的请求结果数据会应用于新的state,但是这时候会发现,这个异步函数卸载reducer中没有办法影响到state的更新,因为在异步请求处理完成时,reducer函数已经被返回(函数的返回是同步的),所以说在redcuer中调用api也完全没有意义了.

api请求该如何执行

  刚才提到在reducer中不能执行api请求操作,但是很明显api操作是不可避免的,因为总要向后台请求数据,那么api请求应该如何做呢?这里有两个办法

  • 在dispatch方法之前进行api请求:在dispatch之外先进行api异步请求,当收到请求结果后,根据结果的不同选择dispatch不同的action
  • 应用redux-thunk,redux-promise等中间件,就可以在dispatch函数中直接执行api请求等异步操作了。