传播运算符与 immutable.js

Posted

技术标签:

【中文标题】传播运算符与 immutable.js【英文标题】:spread operator vs immutable.js 【发布时间】:2019-04-25 13:11:27 【问题描述】:

似乎在将 redux 与 react 结合使用时,immutable.js 几乎已成为行业标准。 我的问题是,当我们使用扩展运算符时,我们不是对我们的 redux 状态进行不变的更改吗?例如,

const reducer = (state=initialState, action) => 
    switch(action.type)
        case actionType.SOME_ACTION:
            return 
                ...state,
                someState: state.someState.filter(etc=>etc)
            
    

我用 redux 设置状态的方式不是不可变的吗?使用 immutable.js OVER 扩展运算符使对象不可变有什么好处?

抱歉,如果有人问过这个问题,但我找不到令我满意的答案。我了解不可变对象的好处,但不了解使用 immutable.js 库而不是点运算符的重要性。

【问题讨论】:

【参考方案1】:

简答

是的! ES6 扩展运算符可以完全替代 immutable.js,但有一个主要警告,您必须始终保持态势感知。

很长的答案

您和其他开发人员将 100% 负责维护不变性,而不是让 immutable.js 为您处理。下面详细介绍了如何使用 ES6 的“扩展运算符”以及它的各种函数(如 filtermap)自行管理不可变状态。

下面将探讨以不可变和变异的方式向数组或对象中删除和添加值。我在每个示例中注销了initialStatenewState,以证明我们是否已经改变了initialState。这很重要,因为如果 initialStatenewState 完全相同,Redux 不会指示 UI 重新渲染。

注意:如果您尝试以下任何变异解决方案,Immutable.js 会使应用程序崩溃。

从数组中移除元素

不可变方式

const initialState = 
  members: ['Pete', 'Paul', 'George', 'John']

const reducer = (state, action) => 
  switch(action.type)
case 'REMOVE_MEMBER':
  return 
    ...state,
    members: state.members.filter(
      member => member !== action.member
    )
  
  

const newState = reducer(
  initialState,
  type: 'REMOVE_MEMBER', member: 'Pete'
);

console.log('initialState', initialState);
console.log('newState', newState);

变异方式

const initialState = 
  members: ['Pete', 'Paul', 'George', 'John']

const reducer = (state, action) => 
  switch(action.type)
case 'REMOVE_MEMBER':
  state.members.forEach((member, i) => 
    if (member === action.member) 
      state.members.splice(i, 1)
    
  )
  return 
    ...state,
    members: state.members
  
  

const newState = reducer(
  initialState,
  type: 'REMOVE_MEMBER', member: 'Pete'
);

console.log('initialState', initialState);
console.log('newState', newState);

向数组添加元素

不可变方式

const initialState = 
  members: ['Paul', 'George', 'John']

const reducer = (state, action) => 
  switch(action.type)
case 'ADD_MEMBER':
  return 
    ...state,
    members: [...state.members, action.member]
  
  

const newState = reducer(
  initialState,
  type: 'ADD_MEMBER', member: 'Ringo'
);

console.log('initialState', initialState);
console.log('newState', newState);

变异方式

const initialState = 
  members: ['Paul', 'George', 'John']

const reducer = (state, action) => 
  switch(action.type)
case 'ADD_MEMBER':
  state.members.push(action.member);
  return 
    ...state,
    members: state.members
  
  

const newState = reducer(
  initialState,
  type: 'ADD_MEMBER', member: 'Ringo'
);

console.log('initialState', initialState);
console.log('newState', newState);

更新数组

不可变方式

const initialState = 
  members: ['Paul', 'Pete', 'George', 'John']

const reducer = (state, action) => 
  switch(action.type)
case 'UPDATE_MEMBER':
  return 
    ...state,
    members: state.members.map(member => member === action.member ? action.replacement : member)
  
  

const newState = reducer(
  initialState,
  type: 'UPDATE_MEMBER', member: 'Pete', replacement: 'Ringo'
);

console.log('initialState', initialState);
console.log('newState', newState);

变异方式

const initialState = 
  members: ['Paul', 'Pete', 'George', 'John']

const reducer = (state, action) => 
  switch(action.type)
case 'UPDATE_MEMBER':
  state.members.forEach((member, i) => 
    if (member === action.member) 
      state.members[i] = action.replacement;
    
  )
  return 
    ...state,
    members: state.members
  
  

const newState = reducer(
  initialState,
  type: 'UPDATE_MEMBER', member: 'Pete', replacement: 'Ringo'
);

console.log('initialState', initialState);
console.log('newState', newState);

合并数组

不可变方式

const initialState = 
  members: ['Paul', 'Ringo']

const reducer = (state, action) => 
  switch(action.type)
case 'MERGE_MEMBERS':
  return 
    ...state,
    members: [...state.members, ...action.members]
  
  

const newState = reducer(
  initialState,
  type: 'MERGE_MEMBERS', members: ['George', 'John']
);

console.log('initialState', initialState);
console.log('newState', newState);

变异方式

const initialState = 
  members: ['Paul', 'Ringo']

const reducer = (state, action) => 
  switch(action.type)
case 'MERGE_MEMBERS':
  action.members.forEach(member => state.members.push(member))
  return 
    ...state,
    members: state.members
  
  

const newState = reducer(
  initialState,
  type: 'MERGE_MEMBERS', members: ['George', 'John']
);

console.log('initialState', initialState);
console.log('newState', newState);

对于经验丰富的开发人员来说,上述改变数组的示例可能看起来像是明显的坏习惯,但对于刚入门的人来说却很容易陷入困境。我们希望任何 Mutated way 代码 sn-ps 都会在代码审查中陷入困境,但情况并非总是如此。 让我们稍微谈谈对象,在您自己处理不变性时比较麻烦。

从对象中移除

不可变方式

const initialState = 
  members: 
paul: 
  name: 'Paul',
  instrument: 'Guitar'
,
stuart: 
  name: 'Stuart',
  instrument: 'Bass'

  

const reducer = (state, action) => 
  switch(action.type)
case 'REMOVE_MEMBER':
  let  [action.member]: _, ...members  = state.members
  return 
    ...state,
    members
  
  

const newState = reducer(
  initialState,
  type: 'REMOVE_MEMBER', member: 'stuart'
);

console.log('initialState', initialState);
console.log('newState', newState);

变异方式

const initialState = 
  members: 
paul: 
  name: 'Paul',
  instrument: 'Guitar'
,
stuart: 
  name: 'Stuart',
  instrument: 'Bass'

  

const reducer = (state, action) => 
  switch(action.type)
case 'REMOVE_MEMBER':
  delete state.members[action.member]
  return 
    ...state,
    members: state.members
  
  

const newState = reducer(
  initialState,
  type: 'REMOVE_MEMBER', member: 'stuart'
);

console.log('initialState', initialState);
console.log('newState', newState);

更新对象

不可变方式

const initialState = 
  members: 
paul: 
  name: 'Paul',
  instrument: 'Guitar'
,
ringo: 
  name: 'George',
  instrument: 'Guitar'

  

const reducer = (state, action) => 
  switch(action.type)
case 'CHANGE_INSTRUMENT':
  return 
    ...state,
    members: 
      ...state.members,
      [action.key]: 
        ...state.members[action.member],
        instrument: action.instrument
      
    
  
  

const newState = reducer(
  initialState,
  type: 'CHANGE_INSTRUMENT', member: 'paul', instrument: 'Bass'
);

console.log('initialState', initialState);
console.log('newState', newState);

变异方式

const initialState = 
  members: 
paul: 
  name: 'Paul',
  instrument: 'Guitar'
,
ringo: 
  name: 'George',
  instrument: 'Guitar'

  

const reducer = (state, action) => 
  switch(action.type)
case 'CHANGE_INSTRUMENT':
  state.members[action.member].instrument = action.instrument
  return 
    ...state,
    members: state.members
  
  

const newState = reducer(
  initialState,
  type: 'CHANGE_INSTRUMENT', member: 'paul', instrument: 'Bass'
);

console.log('initialState', initialState);
console.log('newState', newState);

如果你做到了这一点,恭喜!我知道这是一篇冗长的文章,但我觉得展示所有变异方式很重要,您需要在没有 Immutable.js 的情况下防止自己发生这种情况。使用 Immutable.js 的一大优势,除了可以防止您编写糟糕的代码之外,还有帮助方法,例如 mergeDeepupdateIn

不可变的.JS

合并深度

const initialState = Immutable.fromJS(
  members: 
    paul: 
      name: 'Paul',
      instrument: 'Guitar'
    ,
    ringo: 
      name: 'George',
      instrument: 'Guitar'
    
  
)
const reducer = (state, action) => 
  switch (action.type) 
    case 'ADD_MEMBERS':
      return state.mergeDeep(members: action.members)
  

const newState = reducer(
  initialState,
  
    type: 'ADD_MEMBERS',
    members: 
      george:  name: 'George', instrument: 'Guitar' ,
      john:  name: 'John', instrument: 'Guitar' 
    
  
);

console.log('initialState', initialState);
console.log('newState', newState);
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.2/immutable.min.js"></script>

更新在

const initialState = Immutable.fromJS(
  members: 
    paul: 
      name: 'Paul',
      instrument: 'Guitar'
    ,
    ringo: 
      name: 'George',
      instrument: 'Guitar'
    
  
)
const reducer = (state, action) => 
  switch (action.type) 
    case 'CHANGE_INSTRUMENT':
      return state.updateIn(['members', action.member, 'instrument'], instrument => action.instrument)
  

const newState = reducer(
  initialState,
  type: 'CHANGE_INSTRUMENT', member: 'paul', instrument: 'Bass'
);

console.log('initialState', initialState);
console.log('newState', newState);
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.2/immutable.min.js"></script>

【讨论】:

是的,但是扩展语法不是实现了 immutable.js 试图实现的目标吗?我很困惑为什么 immutable.js 背后有这么多的炒作和支持,而使用扩展运算符可以实现类似的东西 @YoungMoon,查看我的更新答案,详细了解如何使用纯 ES6 实现不变性。 忘记感谢您提供最棒的答案。谢谢【参考方案2】:

我用 Redux 设置状态的方式不是不可变的吗?

在您的示例代码中(假设传递给 filter 的真实函数没有进行任何突变),是的。

使用 immutable.js OVER 扩展运算符方式使对象不可变有什么好处?

两个主要原因:

    意外地改变不可变集合对象是不可能的,因为公共 API 不允许这样做。而对于内置的 JS 集合,它是。深度冻结(递归调用Object.freeze)可以对此有所帮助。

    高效* 使用内置集合的不可变更新可能具有挑战性。 Immutable.js 在内部使用 tries 来使更新比原生集合的原生集合更有效。

如果您想使用内置集合,请考虑使用 Immer,它为不可变更新提供了更好的 API,同时还冻结了它创建的对象,有助于缓解第一个问题(但不是第二个问题)。

* Efficient 表示时间复杂度,例如由于对象流失增加,对象构造和 GC 运行。

【讨论】:

以上是关于传播运算符与 immutable.js的主要内容,如果未能解决你的问题,请参考以下文章

打字稿和传播运算符?

类型的打字稿传播运算符[重复]

Netty的ChannelPipline传播源码解析

不传播的原因是啥(es 6传播运算符)javascript错误对象[重复]

反应传播运算符(...休息)的替代品?

如何从传播运算符中删除属性?