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()或擴展運算符。 這看起來要乾淨得多了,特別是在狀態嵌套得很深的狀況下咱們不須要把擴展操做符分散到各個的位置。 對象的不可變狀態使代碼可以快速肯定某個狀態是否發生了更改。

剛開始接觸這個概念會有些疑惑,不過在開發過程當中你遇到狀態發生變化而彈出的錯誤時,你就能夠清楚其緣由而有效的修改問題。

相關文章
相關標籤/搜索