翻譯|Immutability in React and Redux: The Complete Guide

原文在這裏:Immutability in React and Redux: The Complete Guidejavascript

Immutability(不可突變性,一下直接使用英文)是一個使人困惑的話題,整體上在React,Redux和Javascript出現的地方都會有他的身影浮現.html

在React組件沒有自動渲染的時候,你可能碰到了一個bug,即便是你知道已經修改了props,而且有人會提醒你,應該要作immutable state更新.或許你或者同事之一常常寫出mutate(與immutable對應,爲可突變,一下沿用英文單詞)state的 Redux Reducer.你不得不常常糾正他們(reducers,或者同事).java

這一點有點詭異,也十分的微妙,尤爲是你不肯定要到底要注意什麼.坦率講,若是你沒有認識到Immutable的重要性,就很難關注它.react

這個教程會解釋什麼是immutability以及如何在應用中編寫immutable代碼.一下是涵蓋的內容:git

{{TOC}}github

什麼是Immtablity?

首先 immutable是mutable的反義詞-mutable的意思是:變化,修改,能被搞得一團糟.編程

因此若是某個東西是immutable,那麼他就是不能有變化的.redux

極端的例子是,不能使用傳統意義的變量, 你要不斷的建立新值來代替舊的值. JavaScript沒有這麼極端, 可是有些語言根本不容許mutate任何東西(Elixir, Erlang還有ML).數組

Javas不是純粹的函數式語言,它能夠在某種程度上假裝成函數式語言.JS中有些數組操做時immutable(意思是:不修改原始值,而是返回一個新的數組).字符串操做老是immutable的(JS使用改變的字符串建立新的字符串). 同時,你也能夠編寫本身的immutable函數.須要注意的是要遵照一些規則.瀏覽器

用於Mutation的實例代碼

如今來看看mutality是如何工做的. 從整個person對象開始:

let person = {
	firstName: "Bob",
	lastName: "Loblaw",
	address: {
		street: "123 Fake St",
		city: "Emberton",
		state: "NJ"
	}
}
複製代碼

接着假設寫一個函數賦予person超凡的力量:

function giveAwesomePowers(person) {
	person.specialPower = "invisibility";
	return person;
}
複製代碼

好了,每一個人都得到了超集能力. 隱身(invisibility)是很膩害的技術

如今讓咱們給Mr.Loblaw其餘一些特別的能力

// Initially, Bob has no powers :(
console.log(person);

// Then we call our function...
let samePerson = giveAwesomePowers(person);

// Now Bob has powers!
console.log(person);
console.log(samePerson);

// He's the same person in every other respect, though.
console.log('Are they the same?', person === samePerson); // true
複製代碼

這個函數giveAwesomePowers mutate 了傳遞進入的person對象. 運行這個代碼,你會看到第一次打印出的person,Bob沒有specialPower屬性.可是接下來,第二次,他忽然就有了specialPower能力.

問題在於,由於這個函數修改了傳遞進入的person,咱們不再知道以前的對象是什麼樣子.這個對象永遠被改變了.

giveAwesomePowers函數返回的對象和咱們傳遞進的對象是同一個對象,可是在對象的內部已經亂套了.屬性已經發生改變. 所以對象被mutate了(突變了).

我想要再次重申一下,由於這一點很重要:對象的內在 已經發生改變,可是對象的引用沒有變[^譯註:在內存中的地址空間沒變].從對象外部看是同一個對象(全等於檢查例如person===samePersontrue,就是這個緣由.)

若是咱們想讓giveAesomePowers函數不對person對象做出修改,必需要做出一些改變.首先要讓函數變 pure(變純),由於純函數和immutability緊密相關.

Immutability的原則

爲了讓函數變純,必需要遵照如下規則:

  1. 相同的輸入老是有相同的返回值.
  2. 純函數不能有反作用(side effect) [^譯註:老是以爲這個反作用不太明瞭,用附帶做用是否是更好理解一點?]

那麼什麼是"Side Effect(反作用)"?

"Side effects"是一個寬泛的術語,可是本質上,意味着此刻調用的函數還修改了做用域以外的內容.看看一些side effect的例子...

  • 突變/修飾了輸入的參數,像giveAwesomePowers函數所作的

  • 修改任何函數之外的其餘state,例如修改了全局變量,或者document.(anything)或者window.(anything)

  • 執行API調用

  • console.log()

  • Math.random()

    API調用可能讓你以爲很迷糊.畢竟調用API,例如fetch('/users') 好像徹底沒有改變UI中的任何東西.

    可是在深究一下:若是你調用fetch('/users'),能改變其餘的東西嗎?甚至是在UI以外?

    很是明確.API調用會產生一條瀏覽器的網絡日誌.也會建立(有可能最終會關閉)一個指向服務器的網絡鏈接. 一旦調用命中服務器,一切都有可能發生. 服務器能夠作任何想作的事,包括繼續調用其餘的服務,做出更多的mutation操做. 最終,API調用會在某個地方生成一個日誌文件(生成日誌文件是正正整整的mutation操做).

    因此想我說的同樣,"side effect"的確是涵蓋寬泛的術語. 下面是一個沒有side effect的函數:

function add(a, b) {
  return a + b;
}
複製代碼

你調用一次和調用一百萬次一個樣, 世界上其餘地方的東西不會發生任何改變. 我意思是,從技術角度,嚴謹一點,在你調用這個函數時,世界上其餘的東西會改變的. 時間會流逝...強大帝國會衰落...可是調用這個函數不會直接的致使外接其餘事物發生變化.這一點知足規則2-沒有side effect

再者, 沒有調用這個函數,例如 add(1,2),你老是會獲得相同的返回結果.無論你調用多少次. 這一點知足規則1-同一輸入==同一響應

JS 數組方法會致使Mutate

幾個特定的方法會在使用的時候致使數組發生mutate,

  • push(在一個數組末尾添加一個元素)
  • pop(從數組的末尾移除一個元素)
  • shift(從數組的開始處移除一個元素)
  • unshift(在數組的開始處添加一個元素)
  • sort
  • reverse
  • splice

注意,JS 數組的sort操做是mutable的,它會在原內存地址空間上進行排序操做(in place,或者叫原位操做).要改成immutable操做([^譯註:這一點,彷佛原做者沒有明確表達?]).能夠拷貝一份,而後針對拷貝進行操做.可使用一下的幾個方法進行操做:

let a = [1, 2, 3];
let copy1 = [...a];
let copy2 = a.slice();
let copy3 = a.concat();
複製代碼

因此,若是你想對一個數組進行immutable的排序操做, 能夠這麼操做

let sortedArray = [...originalArray].sort(compareFunction);
複製代碼

關於sort方法有個小知識點(過去困擾過我), 傳遞給sortcompareFunction須要返回0,1或者-1.不能是布爾值.下次編寫比較函數時要留意這一點.

純函數只能調用其餘的純函數

一個可能出問題的地方就是在純函數中調用了不純的函數.純度是能夠變化的.要麼有要麼就沒了.你能夠寫一個完美的純函數,可是若是你最後點用了一個其餘的函數,這些函數又調用了setState,dispatch,亦或者其餘的side effect操做, 純函數就不存在了.

如今有一些幾個特例的side effect是能夠"接受的".使用console.log輸出日誌是能夠接受的. 是的,從技術角度上講, 這是一個side effect,可是它不會影響任何其餘內容.

純函數版的 giveAwesomePowers

如今謹記純函數的原則,重寫這個函數

function giveAwesomePowers(person) {
  let newPerson = Object.assign({}, person, {
    specialPower: 'invisibility'
  })

  return newPerson;
}
複製代碼

如今稍微有點不一樣,並無修改person對象,咱們建立了一個 new person對象.

若是以前你沒見過Object.assign,它的用法是把一個對象的屬性複製到另外一個對象中. 你能夠傳遞多個對象,Ojecct.assign會把多個對象按照從左到右的方向合併成一個單一對象,所以會覆蓋重複的屬性.(說到從左至右,我意思是執行Object.assign(result,a,b,c),)會把a拷貝進result,接着是b,接着是c)

可是Object.assign()不會執行深度融合操做-只有每一個參數對象的的直接子代屬性纔可以被移動.也就是時候, 很是重要的一點,這個操做不會拷貝或者克隆參數對象的屬性. 它會按照原來的樣子分配, 引用不會動.

所用上面的代碼所作的是建立了一個空對象,接着把全部的person的屬性複製到空對象,接着把specialPower屬性也複製的空對象中.另外一種能夠執行相同操做的方法是對象在展開操做(spread operator):

function giveAwesomePowers(person) {
  let newPerson = {
    ...person,
    specialPower: 'invisibility'
  }

  return newPerson;
}
複製代碼

對象展開操做能夠這麼理解:"建立一個新對象,以後從person插入屬性person,接着插入另外一個屬性specialPower".上面的寫法裏的對象展開語法是JavaScript規範ES2018的正式組成部分.

返回全新對象的純函數

如今咱們可使用新的純函數版的giveAwesomePowers來從新運行以前的實例代碼.

// Initially, Bob has no powers :(
//打印原始對象
console.log(person);

// 執行純函數版的對象修改操做
var newPerson = giveAwesomePowers(person);

// Now Bob's clone has powers!
console.log(person);
console.log(newPerson);

//  newPerson 是一個全新的對象了
console.log('Are they the same?', person === newPerson); // false
複製代碼

最大的不一樣點是,person對象沒有被修改. Bob沒有改變. 函數用一樣的屬性建立一個Bob的克隆版本,此外還具備了隱身的屬性.

這就是函數式編程很另類的地方. 對象不斷的被建立和銷燬. 咱們不能修改Bob;只能建立克隆,修改克隆,而後用克隆版本替代Bob.真的有點殘酷.若是你看過電影 致命魔術(The Prestige),有點相似(若是沒看多,就當我沒說).

React優先考慮Immutability

在React的用例中, 絕對不要mutate state或者props是很重要的.無論是函數式組件或者類組件都要遵循這一原則. 若是你準編寫相似這樣的代碼this.state.something=...或者this.props.something=...,不要這麼作了吧, 試試看更好的方法.

要修改state,惟一的方法就是使用this.setState.若是你很好奇爲何要這麼作,能夠看看這篇文章why not to modify state directly.

至於props,是單向流動的.Props輸入進組件.Props不是雙向通道,至少不能經過mutate操做把props設定爲新的值.

若是你必需要發送一些值返回到父組件中,或者要觸發父組件中的某些操做, 能夠以props的形式傳遞函數來實現,以後在須要的時候經過在子組件內調用函數來和父組件通信. 下面是就是回調prop的實例:

//子組件
function Child(props) {
  // 若是,點擊按鈕
  // 會調用從父組件經過props傳遞的函數.
  return (
    <button onClick={props.printMessage}> Click Me </button>
  );
}
//父組件
function Parent() {
  //①父組件中定義一個函數
  function printMessage() {
    console.log('you clicked the button');
  }

  // ②父組件經過props向子組件傳遞一個函數
  // 注意!!!: 傳遞的是函數名,不是調用結果
  // 是printMessage, 不是 printMessage()
  return (
    <Child onClick={printMessage} /> ); } 複製代碼

[^譯註:這個示例代碼若是不太明白,要反覆的看,這是Redux最核心思想之一].

Immutability 對於PureComponents相當重要.

默認狀況下,React組件(函數式組件或者經過繼承React.component的類組件)在他們的父組件從新渲染時也會從新渲染,或者在組件內部經過setState修改內部state時也會從新渲染.

從性能角度考慮,優化React組件最簡單的辦法是聲明一個類,繼承React.PureComponent,不要繼承React.Component.這樣作,只有在組件的props或者state改變時纔會從新渲染. 不再會在父組件從新渲染時,沒頭沒腦的跟着從新渲染了.只有在本身的props發生變化時才執行重渲染. 這裏是React依賴immutability的緣由:若是你要向PureComponent傳遞props,必需要確保這些props是經過immutability的方式更新的.意思是說.若是props是對象或者數組,必定要用新的(修改過的)對象或者數組來替換整個props值.像以前對Bob作的同樣-把他殺掉,而後用克隆頂替.

若是你經過修改屬性,或者添加新的項目來修改對象或者數組的內部元素,甚至是修改數組元素內部的結構- 修改以後的對象或者數組會引用全等於舊的自身,PureComponent就不會注意到props的變化,不會從新渲染. 怪異的渲染問題就會接踵而來.

還記得第一個實例中的Bob和giveAwesomePowers函數嗎? 還記得由函數返回的對象如何與person相同嗎?用的是三個等號,===. 緣由是兩個對象的引用地址都指向同一個對象. 內部發生改變了,可是地址沒有變.

JavaScript中引用全等因而如何工做的?

什麼是"引用等於"(referentially equal)?好吧,有點離題,可是理解這個概念很是重要.

JavaScript的對象和數組都存儲在內存中(如今,你應該馬上點頭,不然就很難解釋下去了).

咱們假設內存像一個盒子,變量名"指向"這個盒子, 盒子裏放的是實際的值.

在JavaScript中,這些盒子(實際就是內存地址)是沒有名字,或者不爲人所知的. 你不會知道一個變量指向的內存地址(在某些語言中,例如C語言,你能夠實際查看一個變量的內存地址,看看他們的生存狀況.)

若是你聲明一個變量,它會指向新的內存地址.

若是你mutate了變量的內部結構, 它仍然指向同一個地址.

有點相似於扒掉了房子中的一切東西,從新修了牆,廚房,起居室,游泳池等等--- 房子的地址沒有改變.

‌關鍵點: 當咱們用===比較兩個對象或者數組時,JavaScript實際比較的是他們指向的內存地址-也就是引用(references).JS甚至根本都不看對象.它只比較引用. 這就是"引用等於"(referential equality)的意思.

因此,若是你接收一個對象,修改它時,修改的是內容,可是不會改變它的引用.

另外一點是,在你把一個對象賦值給另外一個對象(或者做爲函數參數傳遞,這麼作更高效),其餘的對象僅僅是指向第一個對象的地址.有點想巫毒娃娃.你在第二個對象上作的事會直接影響到第一個對象.

下面的代碼讓你更清楚的認識到這個問題.

// 建立變量 `crayon`,指向一個盒子 (無名),
// 盒子承載了對象 `{ color: 'red' }`
let crayon = { color: 'red' };

// 改變 `crayon` 的屬性 不會改變他的指向 
crayon.color = 'blue';

// 把對象或數組賦值給一個新的變量
// 新變量不會改變舊變量指向的盒子
let crayon2 = crayon;
console.log(crayon2 === crayon); // true.二者指向同一個盒子
// 任何針對 `crayon2`變量的修改 也會影響到變量 `crayon1`
crayon2.color = 'green';
console.log(crayon.color); //變爲綠色!
console.log(crayon2.color); //也是綠色了!

// 由於這兩個變量指向同一個內存地址
console.log(crayon2 === crayon);
複製代碼

爲何不作深等於檢查?

在聲明兩個對象以前檢查兩個對象的內部,看起來更合乎情理.這是事實,可是這樣作速度很慢.

到底有多慢? 這要看你須要比較的對象.比較有10,000個子屬性和孫子屬性的對象確定比2個屬性的對象慢.時間沒法預測.

引用等於的的時間,計算機科學家成爲"時間常數"(constant time). 時間常數也成爲 O(1),意思是操做的花費時間老是相同,不用考慮輸入值有多大.

深度等檢查,成爲線性時間(linear time), O(n).意思是花費的時間和對象中的鍵成比例. 一般來講, 線性時間老是比時間常數慢.

這樣來思考:假設JS每次比較兩個值例如a===b要花費0.5秒時間.如今你是願意進行引用檢查仍是深刻兩個對象比較每對屬性?聽起來幾很慢.

在實際計算中,等檢查比時間要遠遠低於1秒,可是盡肯能的少作工做在這裏也是適用的.其餘條件相同,有限考慮性能. 在試圖找到應用的瓶頸時,這會節省大量時間.若是你留心一一點,剛開始就不會慢.

const會阻止改變嗎?

簡短的回答是:不能阻止. let,const,var都不會阻止你改變對象的內部結構.全部這三種聲明方式都容許你mutate對象或數組的內部結構.

"可是它不是叫作const嗎"? 難道意思不是 constant(恆定)?

好吧! const 只會阻止你從新賦值引用,可是不會阻止你改變對象內部結構. 實例以下:

const order = { type: "coffee" }

// const will allow changing the order type...
order.type = "tea"; // this is fine

// const will prevent reassigning `order`
order = { type: "tea" } // this is an Error
複製代碼

下次遇到const要留點心.

我喜歡使用const提醒我本身一個對象或者數組不該該被mutate(大多數狀況下),若是我在編寫代碼時,我肯定要修改某個對象或者數組,我會用let聲明. 這像是一個傳統(像其餘傳統同樣,若是你時不時的打破約定,就不太好了).

怎麼更新 Redux的State

Redux須要保證它的reducer是純函數. 意味着你不能直接修改state-必須基於舊的對象建立一個新的state,正如咱們上面對Bob作的那樣(若是你不太肯定,能夠看看這篇文章 what a reducer is,介紹了reducer名字的來歷)

編寫代碼對state做出immutable更新有點棘手. 下面你會看大一下常見的模式

無論是在瀏覽器終端,仍是實際的應用中親自嘗試一下. 尤爲要注意嵌套對象的更新,實踐中也是如此. 我發現嵌套對象是最麻煩的.

全部這些模式對於React state一樣也是適用的.因此在這個教程中學到的東西能夠用於Redux,沒有Redux的應用也能夠用.

在最後部分,會看到使用Immer庫讓操做更簡單. 可是不要直接跳到最後一部分.理解普通的編寫方式對於明白具體的工做原理大有好處.

... 展開操做符

這些事例大量使用了展開操做符針對數字和對象進行操做. 下面是具體的工做方式

...放在對象或者數組以前,它解開內部的子元素,插入到右邊的變量中

// For arrays:
let nums = [1, 2, 3];
let newNums = [...nums]; // => [1, 2, 3]
nums === newNums // => false! 新的數組對象

// For objects:
let person = {
  name: "Liz",
  age: 32
}
let newPerson = {...person};
person === newPerson // => false! 新的對象


// 內部屬性不動 :
let company = {
  name: "Foo Corp",
  people: [
    {name: "Joe"},
    {name: "Alice"}
  ]
}
let newCompany = {...company};
newCompany === company // => false! 不是同一個對象 object
newCompany.people === company.people // => true! 內部屬性相同
複製代碼

像上面同樣使用, 展開操做符使得建立包含相同內容的數組和對象變得更容易.在建立一個對象/數組的拷貝是很是有用,接着咱們能夠重寫須要改變的部分:

let liz = {
  name: "Liz",
  age: 32,
  location: {
    city: "Portland",
    state: "Oregon"
  },
  pets: [
    {type: "cat", name: "Redux"}
  ]
}

//使Liz年齡增長一歲,其餘的都不動
let olderLiz = {
  ...liz,
  age: 33
}
複製代碼

展開操做符是ES2018標準的一部分.

更新State的方法

這些例子的編寫出發點是從Redux reducer中返回state. 我會展現輸入的state是什麼樣子, 返回的Sate是什麼樣的.

爲了保持實例代碼簡潔. 我會徹底忽略"action"參數. 假定更新能夠有任何action觸發. 固然在你本身的reducer中, 你可能會私用switchcase來針對每一個action執行操做,可是我認爲這會增長本部分理解的噪音.

在React中更新State

爲了在簡單的React State中使用這些事例, 須要稍做一些調整.

由於React會 淺融合傳遞進this.setState()的對象.不須要像Redux同樣展開已有的state.

在Redux reducer中,要這麼寫:

return {
  ...state,
  (updates here)
}
複製代碼

對於簡單 React,能夠這麼寫, 不須要展開操做符:

this.setState({
  updates here
})
複製代碼

要記住一點,儘管setState不會執行淺融合,在更新state內嵌套的屬性時也必需要使用展開操做符(任何比第一層更深的部分).

Redux:更新一個對象

當你想更新Redux state的頂層屬性時,用...state拷貝存在的state,以後列出要更新的屬性和對應的修改值

function reducer(state, action) {
  /* State 相似這樣: state = { clicks: 0, count: 0 } */

  return {
    ...state,
    clicks: state.clicks + 1,
    count: state.count - 1
  }
}
複製代碼

Redux:更新對象內的對象

(這一部分並非專門針對Redux的-對用簡單的React state通用適用 看這裏,如何使用).

當你想更新的對象在Redux內部一層,或更底層,須要拷貝每一層,直至包含了須要更新的對象部分.這裏是第一層實施:

function reducer(state, action) {
  /* State像這樣: state = { house: { name: "Ravenclaw", points: 17 } } */

  // Ravenclaw加2分
  return {
    ...state, // 拷貝(level 0)
    house: {
      ...state.house, // 拷貝嵌套的 (level 1)
      points: state.house.points + 2
    }
  }
複製代碼

另外一個例子, 有兩層深度:

function reducer(state, action) {
  /* State looks like: state = { school: { name: "Hogwarts", house: { name: "Ravenclaw", points: 17 } } } */

  // Two points for Ravenclaw
  return {
    ...state, // 拷貝 (level 0)
    school: {
      ...state.school, // 拷貝 level 1
      house: {         // 替換
      state.school.house...
        ...state.school.house, // 拷貝存在屬性 points: state.school.house.points + 2 // 改變屬性值
      }
    }
  }
複製代碼

在更新深度嵌套的對象時,這個代碼很難閱讀.

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,
  // 變量存儲鍵名
  const key = "ravenclaw";
  return {
    ...state, // copy state
    houses: {
      ...state.houses, // copy houses
      [key]: {  //利用計算屬性修改鍵值
        ...state.houses[key],  // copy that specific house's properties
        points: state.houses[key].points + 3   // update its `points` property
      }
    }
  }
複製代碼

Redux: 在數組前添加元素

mutable的方法是使用數組的.unshift函數在數組以前添加元素. Array.prototype.unshift mutate數組, 不是咱們想要的結果.

這裏是如何用immutable的方法在數組前添加一個元素的方法,適用於Redux:

function reducer(state, action) {
  /* State looks like: state = [1, 2, 3]; */

  const newItem = 0;
  return [    // 新的數組
    newItem,  // 添加的第一個元素
    ...state  // 在最後展開數組
      ];
複製代碼

Redux:給一個數組添加項目

mutable的方法是使用數組的.push函數,在數組的末尾添加一個項目.可是這會mutate數組.

immutably的方法:

function reducer(state, action) {
  /* State looks like: state = [1, 2, 3]; */

  const newItem = 0;
  return [    // a new array
    ...state, // explode the old state first
    newItem   // then add the new item at the end
  ];
複製代碼

也可使用.slice方法拷貝數組,以後mutate拷貝:

function reducer(state, action) {
  const newItem = 0;
  const newState = state.slice();

  newState.push(newItem);
  return newState;
複製代碼

使用map方法更新數組的項目

數組的.map 函數調用你提供的函數,傳遞的參數是數組的每一個項目,返回一個新的數組,使用每一個新項目的返回值做爲新數組的項目.

換句話說,若是你的數組有N個項目,須要返回的數組也是N條,就要使用.map函數.能夠在一次傳遞替換更新一個或者多個項目.

若是數組N條,結束時比N少,可使用.filter. 參見Remove an item form an array.

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;
  });
}
複製代碼

Redux:更新數組中的一個對象

這個和上面的工做原理相同, 惟一的區別是,你須要構建一個新的對象,並返回一個想要改變的對象.

數組的.map 函數經過對數組每一個條目調用函數返回一個新的數組,用函數返回值做爲新數組的元素.

換句話說,若是你的數組有N條項目, 新的數組也須要N條項目, 就用.map. 能夠更新一條或者多條項目.

在這個實例中,咱們有一個數組包含了用戶email地址的數組. 其中一我的改變了email地址,因此咱們須要更新它. 這裏演示的是如何用action的用戶ID和新的email執行更新,你也可使用其餘的途徑來執行更新.

function reducer(state, action) {
  /* State looks like: state = [ { id: 1, email: 'jen@reynholmindustries.com' }, { id: 2, email: 'peter@initech.com' } ] Action contains the new info: action = { type: "UPDATE_EMAIL" payload: { userId: 2, // Peter's ID newEmail: 'peter@construction.co' } } */

  return state.map((item, index) => {
    // Find the item with the matching id
    if(item.id === action.payload.userId) {
      // Return a new object
      return {
        ...item,  // copy the existing item
        email: action.payload.newEmail  // replace the email addr
      }
    }

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


複製代碼

Redux:在一個數組中間插入一個條目

數組的.splice函數會在數組中插入一個項目,可是它會mutate一個數組.

由於咱們並不想mutate原始的數組, 因此能夠先作一下拷貝(.slice),以後使用.splice插入項目

其餘的方法比包括拷貝新元素以前的全部元素,接着插入新的,而後拷貝以後的元素. 可是這麼作很容易出錯.

提示:要作單元測試. 這裏很是容易出錯.

function reducer(state, action) {
  /* State looks like: state = [1, 2, 3, 5, 6]; */

  const newItem = 4;

  // make a copy
  const newState = state.slice();

  // insert the new item at index 3
  newState.splice(3, 0, newItem)

  return newState;

  /* // You can also do it this way: return [ // make a new array ...state.slice(0, 3), // copy the first 3 items unchanged newItem, // insert the new item ...state.slice(3) // copy the rest, starting at index 3 ]; */
}
複製代碼

根據數組元素的index來更新數組

咱們可使用.map方法返回一個特定索引(index)的新值,保持其餘的值不變.

function reducer(state, action) {
  /* State looks like: state = [1, 2, "X", 4]; */

  return state.map((item, index) => {
    // Replace the item at index 2
    if(index === 2) {
      return 3;
    }

    // Leave every other item unchanged
    return item;
  });
}
複製代碼

使用filter從數組中刪除項目

數組的.filter函數調用你提供的函數,逐個傳遞進每一個項目,返回的新數組的元素是條目輸入時,函數返回值爲true的條目.若是函數返回值爲false,就從數組中刪除.

若是你的數組有N條, 你須要返回的條目等於或者少於N,就可使用.filter函數

function reducer(state, action) {
  /* State looks like: state = [1, 2, "X", 4]; */

  return state.filter((item, index) => {
    // Remove item "X"
    // alternatively: you could look for a specific index
    if(item === "X") {
      return false;
    }

    // Every other item stays
    return true;
  });
}
複製代碼

查看Redux 文檔Immutable Update Patterns. 有更多的技巧.

用Immer 使更新更爲簡單

若是你看看上面的immutable state更新代碼,想退縮.我不會責怪你.

深度嵌套對象的更新很難閱讀, 很難書寫,也很可貴到正確的結構. 單元測試是命令式的,可是即便這樣也不會讓代碼更容易閱讀和編寫.

謝天謝地, 有一個庫能幫上忙, 使用由 Michael Weststrate編寫的Immer,可讓你編寫你知道並喜歡的[].push,[].pop還有=編寫mutable代碼-Immer會接受這些代碼,生成完美的immutable代碼,像魔法同樣.

贊! 來看開具體的工做

首先安裝Immer(3.9kb gzipped,)

Yarn add immer
複製代碼

以後,導入produce函數,只要這一個函數, 就完成一切工做了.簡單,明瞭

Import produce from 'immer';

複製代碼

順便講一句,叫作"produce"是由於它產出一個新的值, 名字某種意義上和reduce相反, 這裏是對名字的討論issue on Immer's Github.

從如今起,你可使用produce函數構建一個極佳的mutable練習場所,你全部的mutations都會被具備魔法的JS 代理對象(Proxies )處理. 這裏的先後對比實例使用了純JS版本的reducer和Immer 版本,對比一下更新嵌套對象的過程.

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

function plainJsReducer(state, action) {
  // 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
      }
    }
  }
}

function immerifiedReducer(state, action) {
  const key = "ravenclaw";

  // produce takes the existing state, and a function
  // It'll call the function with a "draft" version of the state
  return produce(state, draft => {
    // Modify the draft however you want
    draft.houses[key].points += 3;

    // The modified draft will be
    // returned automatically.
    // No need to return anything.
  });
}
複製代碼

在React State上使用Immer

Immer也能夠針對setState形式的對象更新形式.

你可能已經知道了React的setState有函數式的形式,接收一個函數,並傳遞當前值, 函數返回新的值:

onIncrementClick = () => {
  // The normal form:
  this.setState({
    count: this.state.count + 1
  });

  // The functional form:
  this.setState(state => {
    return {
      count: state.count + 1
    }
  });
}
複製代碼

Immer的produce函數能夠被插入到state更新函數中.你會注意到,調用produce的調用方式只傳遞了單個參數-也就是更新函數-並非兩個參數(state,draft=>{})

onIncrementClick = () => {
  // The Immer way:
  this.setState(produce(draft => {
    draft.count += 1
  });
}
複製代碼

這是由於Immer的produce函數設置返回的是一個柯里化函數,只有一個參數. 在這個例子中返回的函數已經準備接受state做爲參數,使用draft調用你的更新函數

漸進採用Immer

Immer的一個很好的特性是,由於他很小,目標聚焦(僅僅返回新state的函數), 很容在已有代碼中添加.

Immer向後兼容已經有的Redux reducers,若是你在Immer的produce函數中包裝已經存在的switch/case代碼,全部的reducer測試仍然能夠經過.

以前,我演示過, 傳遞給produce的更新函數能夠隱式返回undefined,而且會自動挑選出針對draftstate的變化.我沒有提到的是,更新函數能夠一個全新的對象,只要它沒有對draft做出任何改變.

這意味着,已經編寫好的返回全新state 的Redux reducer,也能夠用Immer的produce函數包裝,他們應該保持徹底相同. 在這一點上,你能夠輕鬆一塊,一塊地替換掉很難閱讀的immutable 代碼. 看看官方實例從producers返回不一樣數據的各類方法

完!

相關文章
相關標籤/搜索