传播运算符与 immutable.js

spread operator vs immutable.js

好像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 spread operator 方法使对象不可变有什么好处?

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

简答

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

很长的回答

您和您的开发伙伴将 100% 负责维护不变性,而不是让 immutable.js 替您处理。下面详细介绍了如何使用 ES6 'spread operator' 及其各种函数(如 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 代码片段都能在代码审查中被发现,但情况并非总是如此。 稍微说说对象,自己处理不可变性比较麻烦

从对象中删除

不可变方式

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);

如果您已经走到这一步,恭喜您!我知道这是一个长篇大论 post,但我觉得证明所有 变异方式 很重要,你需要在没有 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>

Isn't the way I am setting the state with Redux immutable?

在你的示例代码中(假设传递给 filter 的真实函数没有做任何改变),是的。

What is the benefit of using immutable.js OVER spread operator way of making objects immutable?

两大原因:

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

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

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

* 高效意味着时间复杂度例如由于对象流失增加,对象构造和 GC 运行。