React和Redux中的不可变性(Immutability)

先问你们一个问题react

this.state.cart.push(item.id);
this.setState({ cart: this.state.cart });
复制代码

这两种写法有区别吗? 基本上全部熟练使用React的同窗都知道要避免直接操做state,可是缘由是什么呢? 今天咱们就来探究一下。算法

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

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(它们会返回一个新数组,而不是修改原始数组)。字符串操做老是不可变的(它们会建立一个包含更改的新字符串)。bash

不可变对象(immutable objects)

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

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

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

  • 直接修改数组或者对象的值,就行前面例子中的同样
  • 修改当前函数外的状态, 好比全局变量、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()或扩展运算符。 这看起来要干净得多了,特别是在状态嵌套得很深的状况下咱们不须要把扩展操做符分散到各个的位置。 对象的不可变状态使代码可以快速肯定某个状态是否发生了更改。

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

相关文章
相关标签/搜索