展开运算符 vs Immutable.js

11

看起来在使用redux和react时,immutable.js几乎已成为行业标准。我的问题是,当我们使用spread操作符时,我们不是在以不可变的方式对我们的redux状态进行更改吗?例如,

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

我使用redux设置状态的方式不是不可变的吗?使用immutable.js相较于使用展开运算符创建不可变对象的好处是什么?

如果此问题已有答案,对不起,但我找不到让我满意的答案。我了解不可变对象的好处,但不理解为什么要使用immutable.js库而不是点运算符。

2个回答

24

简短回答

是的!ES6 spread运算符可以完全替代immutable.js,但有一个重大的限制,您必须时刻保持情况感知。

非常长的回答

您和其他开发人员将100%负责维护不变性,而不是让immutable.js为您处理。下面是如何使用ES6 'spread运算符'及其各种函数(例如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);

对于经验丰富的开发者来说,上述可变动数组的示例可能似乎是显而易见的不良做法,但对于新手来说,这是一个容易陷入的陷阱。我们希望任何“已改变的方式”代码片段在代码审查中都能被捕捉到,但并非总是如此。

让我们谈一下对象,当你自己处理不可变性时,它们更加繁琐。

从对象中删除

不可变的方式

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这样的帮助方法。

Immutable.JS

mergeDeep

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>

updateIn

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>


1
是的,但不是扩展语法实现了Immutable.js试图实现的目标吗?我很困惑为什么Immutable.js有如此多的炒作和支持,当使用类似于扩展操作符可以实现相似的功能。 - Young Moon
@YoungMoon,请查看我的更新答案,其中详细介绍了如何使用纯ES6实现不可变性。 - AnonymousSB
2
忘了感谢你最棒的答案了。谢谢。 - Young Moon

2
“Isn't the way I am setting the state with Redux immutable?”的意思是:“我使用Redux设置状态的方式不是不可变的吗?”
在您的示例代码中(假设传递给filter的真实函数不进行任何突变),是的。
“使用immutable.js相比使用spread操作符使对象不可变有什么好处?”的意思是:“使用immutable.js相比使用spread操作符使对象不可变有什么好处?”
两个主要原因:
1. 不易意外地突变Immutable集合对象,因为公共API不允许这样做。而使用内置JS集合就会发生这种情况。深度冻结(递归调用Object.freeze)可以在一定程度上解决这个问题。
2. 使用内置集合进行不可变更新的效率*可能具有挑战性。Immutable.js在内部使用tries使更新比使用原生集合的平凡用法更有效。
如果您想使用内置集合,请考虑使用 Immer,它提供了更好的 API 用于不可变更新,同时还会冻结它创建的对象,有助于缓解第一个问题(但不是第二个问题)。
* “高效”指的是例如对象构造和 GC 运行的时间复杂度,由于增加了对象翻转。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接