先问大家一个问题

this.state.cart.push(item.id);
this.setState({ cart: this.state.cart });

这两种写法有区别吗?
基本上所有熟练使用React的同学都知道要避免直接操作state,但是原因是什么呢?
今天我们就来探究一下。

这涉及到Javascript中的一个常规概念Immutability
先看下面这段代码:

const items = [
  { id: 1, label: 'One' },
  { id: 2, label: 'Two' },
  { id: 3, label: 'Three' },
];

return (
  <div>
    <button onClick={() => items.push({id:5})}>Add Item</button>
    {items.map( item => <Item key={item.id} {...item} />)}
  </div>
)

点击按钮触发click事件,想items中添加新的项,但页面并没有重新渲染把加入的项显示在页面上。
这是因为数组是引用类型,当使用push方法修改当前数组的时候,react的状态管理对数组的引用没有改变,所以react在进行脏检查对比这个数组时,无法判断出它已经发生变化,就不会触发react的状态更新,进而更新DOM。
同样,当我们写出这样this.state.something = x 或者这样 this.state = x的代码的时候,就有各种奇怪的bug出现,并且非常影响组件的性能。
一切的原因都是原对象被mutate了。
那么相对的,要避免这种问题,就需要保证对象Immutability.

什么是Immutability

如果你之前没有接触过这个概念,那么你很容易把它和为新值分配变量或重新赋值弄混。
immutable正是和mutable相反,我们知道mutable就是指可变的,可修改的…可以被搅乱。
所以immutable就是指不可以改变数据的内部结构和值。
比如Javascript中的某些数组操作就是immutable(它们会返回一个新数组,而不是修改原始数组)。字符串操作总是不可变的(它们会创建一个包含更改的新字符串)。

不可变对象(immutable objects)

我们要确保我们的对象不可被改变。就需要使用一个方法,它必须返回一个新对象。本质上,我们需要一个称为纯函数的东西。
纯函数具有两个属性:

  • 返回值必须依赖于输入值,且输入值不变,返回值也不会变
  • 它不会作出超出它本身作用域的影响

什么叫做”超出它本身作用域的影响“?
这是一个稍微宽泛的定义,它意味着修改不在当前功能范围内的内容。比如:

  • 直接修改数组或者对象的值,就行前面例子中的一样
  • 修改当前函数外的状态, 比如全局变量、window的属性
  • API请求
  • Math.random()

而像下面这个方法就是一个纯函数:

function add(a, b) {
  return a + b;
}

无论你执行多少遍,只要输入的a和b的值不变,它的返回值永远不会变而且不会造成其它影响。

数组

我们创建一个纯函数来处理数组对象。

function itemAdd(array, item) {
  return [...array, item]
}

我们使用扩展运算符返回一个新的数组,并没有改变原数组的内部结构和数据。我们也可以使用别的方法,先复制一个数组,然后进行操作。

function itemAdd(array, item) {
  const newArr = array.concat();
  // const neArr = array.slice();
  // const newArr = [...array];
  return newArr.push(item);
}
对象

通过使用Object.assign(),创建一个函数,它会返回一个新的对象,而不会改变原对象的内容和结构。

const updateObj = (data, newAttribute) => {
    return {
      Object.assign({}, data, {
        location: newAttribute
    })
  }
}

我们也可以使用扩展运算法:

const updateLocation = (data, newAttribute) => {
  return {
    ...data,
    location: newAttribute
  }
}

React和Redux中如何更新状态

对于典型的React应用,它的状态就是一个对象。而Redux也是使用不可变对象作为应用程序存储的基础。
这是因为如果React无法确定组件的状态已更改,则它将不知道如何更新虚拟DOM。
而对象的不变性使跟踪这些变更成为了可能。React将对象的旧状态与其新状态进行比较,并基于该差异重新渲染组件。
前面我们介绍的数组和对象的处理方法,在React和Redux中同样适用。

React中更新对象

在React中你使用this.setState()方法,它会隐式地将你传入的对象使用Object.assign()方法进行合并,所以对于状态的修改:
在Redux中

return {
  ...state,
  (updates here)
}

而在React中

this.setState({
  updates here
})

但是需要注意的是,尽管setState()方法隐式地合并了对象,但是在更新state中的深度嵌套项(任何深度超过第一级的项)时,需要使用对象(或数组)的扩展运算符方法处理。

Redux中更新对象

当您想更新Redux状态对象中的顶级属性时,用扩展运算符复制现有状态,然后用新的值列出要更改的属性。

function reducer(state, action) {
  /*
    State looks like:

    state = {
      clicks: 0,
      count: 0
    }
  */

  return {
    ...state,
    clicks: state.clicks + 1,
    count: state.count - 1
  }
}
Redux更新嵌套对象

如果要更新的对象是Redux状态中的一层(或多层)结构,则需要对要更新的对象的每个级别制作一个副本。
看下面这个两层结构的例子:

function reducer(state, action) {
  /*
    State looks like:

    state = {
      school: {
        name: "Hogwarts",
        house: {
          name: "Ravenclaw",
          points: 17
        }
      }
    }
  */

  // Two points for Ravenclaw
  return {
    ...state, // copy the state (level 0)
    school: {
      ...state.school, // copy level 1
      house: {         // replace state.school.house...
        ...state.school.house, // copy existing house properties
        points: state.school.house.points + 2  // change a property
      }
    }
  }
Redux按照键值更新对象
function reducer(state, action) {
  /*
    State looks like:

    const state = {
      houses: {
        gryffindor: {
          points: 15
        },
        ravenclaw: {
          points: 18
        },
        hufflepuff: {
          points: 7
        },
        slytherin: {
          points: 5
        }
      }
    }
  */

  // Add 3 points to Ravenclaw,
  // when the name is stored in a variable
  const key = "ravenclaw";
  return {
    ...state, // copy state
    houses: {
      ...state.houses, // copy houses
      [key]: {  // update one specific house (using Computed Property syntax)
        ...state.houses[key],  // copy that specific house's properties
        points: state.houses[key].points + 3   // update its `points` property
      }
    }
  }
Redux中使用map方法更新数组中的项

数组的.map函数将通过调用提供的函数返回一个新数组,传递每个现有项,并使用返回值作为新项的值。

function reducer(state, action) {
  /*
    State looks like:

    state = [1, 2, "X", 4];
  */

  return state.map((item, index) => {
    // Replace "X" with 3
    // alternatively: you could look for a specific index
    if(item === "X") {
      return 3;
    }

    // Leave every other item unchanged
    return item;
  });
}

Immutable.js

上面提到的方法都是常规的Javascript方法,而且可以看到有些稍微复杂的对象处理起来都很繁碎,让人想要放弃。
尤其是深度嵌套的对象更新很难读取、编写,而且很难正确执行。单元测试是必需的,但即使是这些测试也不能使代码更易于读写。
幸运的是我们可以使用一些库来实现:Immutable.js
它提供了性能强大而且丰富的API。
我们通过设计一个TODO组件来看下如何使用它。
我们首先引入

import { List, Map } from "immutable";
import { Provider, connect } from "react-redux";
import { createStore } from "redux";

然后创建一些标签来定义这个组件:

const Todo = ({ todos, handleNewTodo }) => {
  const handleSubmit = event => {
    const text = event.target.value;
    if (event.keyCode === 13 && text.length > 0) {
      handleNewTodo(text);
      event.target.value = "";
    }
  };
return (
    <section className="section">
      <div className="box field">
        <label className="label">Todo</label>
        <div className="control">
          <input
            type="text"
            className="input"
            placeholder="Add todo"
            onKeyDown={handleSubmit}
          />
        </div>
      </div>
      <ul>
        {todos.map(item => (
          <div key={item.get("id")} className="box">
            {item.get("text")}
          </div>
        ))}
      </ul>
    </section>
  );
};

我们使用handleSubmit()方法创建新的待办事项。在本例中,用户将只创建新的待办事项,我们只需要一个操作:

const actions = {
  handleNewTodo(text) {
    return {
      type: "ADD_TODO",
      payload: {
        id: uuid.v4(),
        text
      }
    };
  }
};

然后我们可以继续创建reducer函数并将上面创建的操作传递给reducer函数:

const reducer = function(state = List(), action) {
  switch (action.type) {
    case "ADD_TODO":
      return state.push(Map(action.payload));
    default:
      return state;
  }
};

我们将使用connect创建一个容器组件,以便可以插入到存储区中。然后我们需要传入mapstatetops()和mapsdispatchtoprops()函数来连接组件。

const mapStateToProps = state => {
  return {
    todos: state
  };
};

const mapDispatchToProps = dispatch => {
  return {
    handleNewTodo: text => dispatch(actions.handleNewTodo(text))
  };
};

const store = createStore(reducer);

const App = connect(
  mapStateToProps,
  mapDispatchToProps
)(Todo);

const rootElement = document.getElementById("root");
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
);

我们使用mapStateToProps()为组件提供存储的数据。然后使用mapsDispatchToProps()将动作绑定到组件,使动作创建者可用作组件的道具。
在reducer函数中,我们使用来自Immutable.jsList创建应用程序的初始状态。

const reducer = function(state = List(), action) {
  switch (action.type) {
    case "ADD_TODO":
      return state.push(Map(action.payload));
    default:
      return state;
  }
};

我们把List看作了一个JavaScript数组,所以可以在state上使用.push()方法。用于更新状态的值是一个对象,它表示可以将map识别为一个对象。这样保证了当前状态不会更改,就不需要使用Object.assign()或扩展运算符。
这看起来要干净得多了,特别是在状态嵌套得很深的情况下我们不需要把扩展操作符分散到各个的位置。
对象的不可变状态使代码能够快速确定某个状态是否发生了更改。

刚开始接触这个概念会有些疑惑,不过在开发过程中你遇到状态发生变化而弹出的错误时,你就可以清楚其原因而有效的修改问题。