Redux框架reducer对状态的处理

前言

在react+redux项目里,关于reducer处理state的方式,在redux官方文档中有这样一段描述:

不要修改 state。 使用 Object.assign() 创建了一个副本。不能这样使用Object.assign(state, {visibilityFilter: action.filter }),因为它会改变第一个参数的值。你必须把第一个参数设置为空对象。你也可以开启对ES7提案对象展开运算符的支持,从而使用 { ...state, ...newState }达到相同的目的。

对此,我们可能会产生以下一些疑问:

  • 为什么要创建副本state?
  • 怎样创建副本state才是合理的?
  • 外部插件直接更新state是否合理?

为什么要创建副本state

在redux-devtools中,我们可以查看到redux下所有通过reducer更新state的记录,每一条记录都对应着内存中某一个具体的state,使得用户可以追溯到每一次历史操作产生与执行的状态,这也是使用redux管理状态的重要优势之一。

若不创建副本,而是直接修改state,则redux的所有操作都将指向内存中的同一个state,因而无法获得每次操作的历史状态。

创建副本也是为了保证向下传入的this.props与nextProps能得到正确的值,以便我们能够利用前后props的改变情况决定如何render组件。

怎样创建副本state才是合理的?

既然创建副本是为了保留更改历史,那么,原则上原state所有被改动过的属性都应该被创建副本。我们可以看一下官方示例:

  function todoApp(state = initialState, action) {
      switch (action.type) {
            case SET_VISIBILITY_FILTER:
                 return Object.assign({}, state, {
                      visibilityFilter: action.filter
            })
            default:
                 return state
      }
  }

示例中的state结构较为简单,而实际项目中的业务需求可能远比示例中更为复杂。若visibilityFilter是下面这样的结构:

visibilityFilter: {
  a:{
    c:1
  },
  b:{
    d:2
  }
}

而我们需要改动的是visibilityFilter.b.d。则可选的方案包括:

方案1

将todoApp这个reducer拆分为更细化的reducer,以保证visibilityFilter属性中嵌套对象b的属性d能得到正确更新。

方案2

采用官方实例中Object.assign方法,但需要将visibilityFilter中未更新的对象用原state中的对象进行手动赋值:

function todoApp(state = initialState, action) {
  switch (action.type) {
      case SET_VISIBILITY_FILTER:
          return Object.assign({}, state, {
              visibilityFilter: {
                  state.visibilityFilter.a,
                  b:{
                    d:action.filter
                  }
              }
          })
      default:
          return state
  }
}

或采用对象展开运算符:

function todoApp(state = initialState, action) {
  switch (action.type) {
      case SET_VISIBILITY_FILTER:
        return {
        ...state, 
        visibilityFilter:{
          ...state.visibilityFilter, 
          b:{
            ...state.visibilityFilter.b,
            d:action.filter
          }
        }
      }
      default:
          return state
  }
}
方案3

将state进行深度对象克隆后,再进行更新。这里直接采用lodash的cloneDeep方法:

import cloneDeep from 'lodash/cloneDeep'
function todoApp(state = initialState, action) {
  switch (action.type) {
      case SET_VISIBILITY_FILTER:
            const newState = cloneDeep(state)
      newState.visibilityFilter.b.d = action.filter
            return newState
      default:      return state
  }
}
方案4

采用官方提供的Immutability Helper工具中update()方法进行数据更新:

import update from 'react/lib/update'
function todoApp(state = initialState, action) {
  switch (action.type) {
      case SET_VISIBILITY_FILTER:
            return update(state, {
                visibilityFilter:{
                   d:{$set: action.filter}
                }
            })
      default:
            return state
  }
}
方案小结
  • 在结构更复杂时,方案1会产生更多细化的reducer,很多reducer其实没有必要进行如此深层次的细化拆分。
  • 在方案2中,我们需要将原对象中所有没有变更的对象手动赋值给副本对象,并确保副本对象的结构完整性与原对象相同。相比方案1,方案2的优势在于更少的代码量。
  • 方案3是上述方案中最为简便且不易出错的方案,但深度复制会为整个被复制的对象创建一个完整的副本。与方案1、2中只创建变更部分的副本相比,将消耗更多内存,执行效率明显低于前面的方案。
  • 方案4不存在方案3的性能问题,而相比方案2而言,创建副本的方式更为简单,所以本文更为推荐采用此方案创建副本。
错误示例!

由于官方示例采用Object.assign方法创建副本,有时候我们为了书写简便,可能会出现这样的副本创建方式:

function todoApp(state = initialState, action) {
  switch (action.type) {
      case SET_VISIBILITY_FILTER:
            const newState = Object.assign({}, state)
            newState.visibilityFilter.b.d = action.filter      
            return newState  
      default:
            return state
  }
}

或者:

function todoApp(state = initialState, action) {
  switch (action.type) {
      case SET_VISIBILITY_FILTER:
          state.visibilityFilter.b.d = action.filter
          return Object.assign({}, state)
      default:
          return state
  }
}

此处,我们对Object.assign方法进行一个小测试:

    const x = {
      a1: {a2: 1},
      b1: {
        b2: {b4: 2},
        b3: {b5: 3}
      },
      c1:4
    }
    const y = Object.assign({}, x)
    y.b1.b3.b5 = 8
    y.c1 = 9
    console.log(x); //=> {a1:{a2:1}, b1:{b2:{b4:2}, b3:{b5:8}}, c1:4}
    console.log(y); //=> {a1:{a2:1}, b1:{b2:{b4:2}, b3:{b5:8}}, c1:9}
    console.log(x == y); //=> false
    console.log(x.a1 == y.a1); //=> true
    console.log(x.b1 == y.b1); //=> true
    console.log(x.b1.b2 == y.b1.b2); //=> true
    console.log(x.b1.b3 == y.b1.b3); //=> true
    console.log(x.b1.b3.b5 == y.b1.b3.b5); //=> true

y通过对x进行Object.assign后获得。当对x和y的c1值进行修改时,确实各不相同。这是因为c1在对象中以值的形式存在,体现为两份不同的拷贝。然而,在对b1对象的b3.b5进行修改时,则x和y的值同时发生了改变,即在x和y内部,其在内存中是同一个引用地址。也就是说,这种assign来复制对象的方式并没有做到真正的不变!

外部插件直接更新state是否合理?

我目前接触较多的外部插件为redux-form。此处暂以redux-form更新state的方式进行一些探讨。

redux-form

当组件采用redux-form进行监听后,内部form表单里的对象都将被放入redux的state中进行管理,并由redux-form自身发起action进行更新删除等操作。

问题在于,每次表单的更新,redux-form都会发起一次action,这意味着我们在一个input框里输入一句简单的"hello world",默认情况下将会有11个state副本产生。显然,这种方式并不合理。

首先,就创建副本而言,本身是一种性能消耗。至于创建副本的目的是为了追溯历史操作与更改,则类似redux-form这样短时间高频率的更改state的方式,产生的大量细碎历史,或许并没有必要?

其次,若外部插件直接更新state,由于处理方式大多封装在其内部,若插件自身对创建state副本的方式没有深入的考虑,其高频率的更新state,可能会对整个项目的运行效率产生较为严重的影响。

小结

就redux-form而言,在一些场景中,能明显感受到输入操作存在顿挫感。显然,当我们在选择外部插件时,需要合理考虑其对state的处理方式。外部插件直接更新state可能会使一些业务状态更方便管理,但其对整个项目的性能影响却需要我们慎重评估,谨慎使用。

原文发布于微信公众号 - 逸言(YiYan_OneWord)

原文发表时间:2016-10-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Python中文社区

Python云计算框架:OpenStack源码分析之RabbitMQ(二)

之前发布的文章因为在编辑后代码部分在手机上看不清已被及时删除,本文重新编辑好之后再发布一次,带来不便请谅解! 專 欄 ❈ ZZR,Python中文社区专栏作者...

2899
来自专栏iOS 开发杂谈

iOS多线程之一:基本概念

进程:就是一个正在执行的程序。 线程:是执行程序最基本的单元,它有自己栈和寄存器。

671
来自专栏顶级程序员

Linux中高效编写Bash脚本的10个技巧

Linux开源社区(微信号:cn_linux) 英文:Aaron Kili,翻译:Linux中国/ch-cn 链接:linux.cn/article-8618...

3375
来自专栏有趣的django

35.Django2.0文档

第四章 模板  1.标签 (1)if/else {% if %} 标签检查(evaluate)一个变量,如果这个变量为真(即,变量存在,非空,不是布尔值假),系...

57010
来自专栏Golang语言社区

Golang工程经验(上)

作为一个C/C++的开发者而言,开启Golang语言开发之路是很容易的,从语法、语义上的理解到工程开发,都能够快速熟悉起来;相比C、C++,Golang语言更简...

4392
来自专栏杨建荣的学习笔记

一个细小的空间问题触发的报警(r11笔记第68天)

今天有一个数据库服务器报警,报警信息是来自于一个异机备库。可以看到这台服务器空间只有300多G,而剩余空间只剩下了不到30G.所以这样一个问题就很奇怪了...

3637
来自专栏点滴积累

Fish Shell

今天看到阮一峰同学的一篇博客(Fish shell 入门教程),讲述的非常详细、清楚,有兴趣的可以直接转去查看此文,本文仅提供一下个人使用心得。 一、fish ...

3576
来自专栏owent

pbc的proto3接入

Protobuf 的 proto3发布也有挺长一段时间了。现在很多新项目慢慢转变用proto3来开发。这篇文章主要记录一下我在给pbc写对proto3支持时的一...

1541
来自专栏技术小讲堂

WCF中操作的分界于调用顺序和会话的释放操作分界实例停止

操作分界 在WCF操作契约的设计中,有时会有一些调用顺序的业务,有的操作不能最先调用,有的操作必须最后调用,比如在从一个箱子里拿出一件东西的时候,必须先要执行打...

3346
来自专栏信安之路

ring3层恶意代码实例汇总

之前一期我们学习了 IAT 的基本结构,相信大家对 C++ 有了一个基本的认识,这一期放点干货,我把 ring3 层恶意代码常用的编程技术给大家整理了一下,所有...

1270

扫码关注云+社区