关于Redux框架,Reducer中state处理方式的探讨

前言



在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管理状态的重要优势之一.

若不创建副本,redux的所有操作都将指向内存中的同一个state,我们将无从获取每一次操作前后,state的具体状态与改变,若没有副本,redux-devtools列表里所有的state都将被最后一次操作的结果所取代.我们将无法追溯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进行深度对象克隆后,再进行更新,可以用原生js去实现,但这里直接采用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

由此可见Object.assign操作后,x,y的区别是其自身的引用地址和属性c1所引用的数值不同,而属性a1,b1所引用的对象及其内部子对象在内存中是同一个引用地址,这就意味着,改变y.b1.b3.b5的值实际上同时也改变了x.b1.b3.b5的值,

这会导致redux中state历史混乱以及之后components所调用的this.props与nextProps无法得到正确的值

外部插件直接更新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创建副本的目的是为了追溯历史操作与更改,所以我们应该考虑,类似redux-form这样短时间高频率的更改state的方式,产生大量细碎的输入历史,我们是否应该避免这样的更新方式?

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

小结

就redux-form使用体验而言,在一些输入场景中,其会导致输入操作产生明显的顿挫感,可以猜测这是由于其工作方式导致的性能问题造成的结果.

所以,外部插件直接更新state可能会使一些业务状态更方便于管理,但其对整个项目的性能影响情况,可能需要我们去慎重评估.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容