Skip to content

Latest commit

 

History

History
292 lines (230 loc) · 9.33 KB

9.md

File metadata and controls

292 lines (230 loc) · 9.33 KB

使用 slice(),concat() 和 ...(展开运算符) 来避免修改数组

对应视频:http://video.sike.io/javascript-redux-avoiding-array-mutations-with-concat-slice-and-spread/

本节使用到的环境:https://jsbin.com/kaboqe/edit?js,console

环境

  • 引入了 Expect, deep-freeze 库。

Expect 我们之前已经用过, 而 deep-freeze 库只提供了一个方法 deepFreeze.

deepFreeze

冻结对象,让整个对象不可修改。

还记得之前使用过的测试驱开发?本节也将使用这个方法。

可变和不可变

我们已经知道,在 Redux 中, state 是只读的,每次要改变只能去计算出一个全新的。

像这种不可修改的对象叫做不可变 (immutable) 对象。

将修改对象称为转变 (mutation).

为了避免无意地转变不可变对象,我们将讨论一些在处理不可变数组时,应该要注意的地方。

多个计数器例子

我们之前已经实现过单个计数器,并使用单个数值表示整个应用的状态。

当我们要写一个"多个独立计数器"应用时(相信你已经玩弄过前面的示例), 我们要用数组去表示多个单独的计数器状态。

添加计数器

我想实现的第一个函数是添加计数器的函数

const addCounter = (list) => {
  // 待实现
};

const testAddCounter = () => {
  const listBefore = [];
  const listAfter = [0];
  expect(
    addCounter(listBefore)
  ).toEqual(listAfter);
};

testAddCounter();
console.log('全部测试通过!');

我们希望在执行 addCounter 后给数组最后添加一个 0

我们可以简单地使用 push 方法将 0 加入数组,而且可以通过测试。

const addCounter = (list) => {
  list.push(0);
  return list;
};

Tests passed screenshot

当然,我们要确保我们没有转变这个数组。

通过 deepFreeze 方法我们可以保护数组不被修改:

deepFreeze(listBefore);

现在,再次运行代码,会报错误,因为不能添加新的属性到一个被“冰冻”的对象。

Screenshot of Array protected by deep-freeze

我将使用数组的 concat 方法,这个方法不会修改原来的数组,而是返回一个新的数组,它是一个纯函数

const addCounter = (list) => {
  return list.concat([0]);
};

测试再一次通过,因为我们没有转变原来的状态。

我们也可以使用 ES6 数组 ...(展开运算符) 来简化代码:

const addCounter = (list) => {
  return [...list, 0];
};

移除计数器

下面要实现的是 removeCounter, 用于移除计数器。

它接受两个参数,一个是数值数组(代表所有计数器的状态),另一个是代表计数器位置的索引 (index).

const removeCounter = (list, index) => {

};

const testRemoveCounter = () => {
  const listBefore = [0, 10, 20];
  const listAfter = [0, 20];

  deepFreeze(listBefore); // 我们直接添加了 deepFreeze.

  expect(
    removeCounter(listBefore, 1)
  ).toEqual(listAfter);
};

如果现在的状态是三个计数器,在调用删除计数器的函数后,我们会删除对应索引的计数器,这里是第二个。(索引为1)

一般来说,我们可以使用 splice 方法去删除数组里面的东西。但是, splice 是一个转变方法,它会修改原有的数组,我们不应该在 Redux 中使用。

const removeCounter = (list, index) => {
  list.splice(index, 1);
  return list;
};

在使用 deepFreeze 之后这样是通过不了的,我们要想出另外一个删除数组项的办法。

我们可以使用一个跟 splice 很像的 slice 方法,但是它跟 splice 不一样,每次调用都会返回一个新的数组。所以我们可以这么用:

// ES5
function removeCounter(list, index) {
  return list.slice(0, index).concat(list.slice(index + 1));
}

// ES6 展开运算符
const removeCounter = (list, index) => {
  return [
    ...list.slice(0, index),
    ...list.slice(index + 1)
  ];
};

我们只要数组的指定索引前和后的部分。

计数器自增

计数器自增应该是这样的:

const incrementCounter = (list, index) => {

};

const testIncrementCounter = () => {
  const listBefore = [0, 10, 20];
  const listAfter = [0, 11, 20];

  deepFreeze(listBefore); // 我们直接添加了 deepFreeze.

  expect(
    incrementCounter(listBefore, 1)
  ).toEqual(listAfter)
};

最直接的方法是直接修改数组对应索引的项,但这也是会使数组发生转变的,因为我们使用了 deepFreeze, 它不会通过测试。

const incrementCounter = (list, index) => {
  list[index]++;
  return list;
};

为了解决这个问题,我们同样可以使用 sliceconcat 来拼接新的数组,使用展开运算符取代 concat 将更直观简洁。

// ES5
function incrementCounter(list, index) {
  return list.slice(0, index).concat(list[index] + 1, list.slice(index + 1));
}

// ES6 展开运算符
const incrementCounter = (list, index) => {
  return [
    ...list.slice(0,index),
    list[index] + 1,
    ...list.slice(index + 1)
  ];
};

总结

  • 避免使用会使原来数组发生转变的方法。
    • 使用 deepFreeze 保护数组不被修改。
  • 常见的转变方法及替代方法:
let array = [1, 2, 3];
let newArray = [];

// 转变方法
newArray = array.push(4);
console.log(newArray); // [1, 2, 3, 4]
console.log(array); // [1, 2, 3, 4]

// 非转变方法
newArray = array.concat(4);
newArray = [...array, 4]; // ES6
console.log(newArray); // [1, 2, 3, 4]
console.log(array); // [1, 2, 3]

// 转变方法
array.splice(1)
newArray = array;
console.log(newArray); // [1, 3]
console.log(array); // [1, 3]

// 非转变方法
newArray = array.slice(0, 1).concat(array.slice(1 + 1))
newArray = [...array.slice(0, 1), ...array.slice(1 + 1)]; // ES6
console.log(newArray); // [1, 3]
console.log(array); // [1, 2, 3]

// 转变方法
array[1] += 1;
newArray = array;
console.log(newArray); // [1, 3, 3]
console.log(array); // [1, 3, 3]

// 非转变方法
newArray = array.slice(0, 1).concat(array[1] + 1, array.slice(1 + 1));
newArray = [...array.slice(0, 1), array[1] + 1, ...array.slice(1 + 1)]; // ES6
console.log(newArray); // [1, 3, 3]
console.log(array); // [1, 2, 3]

思考: (答案将在下一节公布)

  • deepFreeze 的原理是什么,它是怎么保护对象的?

上节解答:

  • 为什么我们不在 Counter 组件里面直接获取当前状态? 相关代码:
const Counter = ({
  value
}) => (
  // ...具体实现
);

const render = () => {
  ReactDOM.render(
    <Counter
      value={store.getState()}
    />,
    document.getElementById('root')
  );
};
  • 这样做会使组件和状态耦合,组件本身不应依赖 store.
  • Counter 是一个展示型组件,后面我们会继续深入讨论。

导航

  1. 不可变的单一状态树
  2. 用动作描述状态变化
  3. 纯函数和非纯函数
  4. reducer 函数
  5. 为计数器编写一个带有测试的 reducer 函数
  6. store 方法:getState,dispatch 和 subscribe
  7. 从头实现 store
  8. React 计数器示例
  9. 使用 slice(),concat() 和 ...(展开运算符) 来避免修改数组
  10. 使用 Object.assign() 和 ...(展开运算符) 来避免修改对象
  11. 编写代办事项列表的 Reducer (添加一个代办事项)
  12. 编写代办事项列表的 Reducer (切换代办事项的完成状态)
  13. “Reducer 组合”和数组
  14. “Reducer 组合”和对象
  15. 使用 combineReducers() 实现 Reducer组合
  16. 从头实现 combineReducer()
  17. React 代办事项列表示例(添加一个代办事项)
  18. React 代办事项列表示例(切换代办事项的完成状态)
  19. React 代办事项列表示例(过滤代办事项)
  20. 提取展示型组件( TODO, TODOLIST )
  21. 提取展示型组件( AddTodo, Footer, FilterLink )
  22. 提取容器组件 ( FILTERLINK )
  23. 提取容器组件 ( VisibleTodoList, AddTodo )
  24. 通过 props 显式地向下传递 store
  25. 通过 context 隐式地向下传递 store
  26. 使用 React Redux 的 传递 store
  27. 使用 React Redux 的 connect() 生成容器 ( VisibleTodoList )
  28. 使用 React Redux 的 connect() 生成容器 ( AddTodo )
  29. 使用 React Redux 的 connect() 生成容器 ( FooterLink )
  30. 提取动作创造器