原文在這裏: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
首先 immutable是mutable的反義詞-mutable的意思是:變化,修改,能被搞得一團糟.編程
因此若是某個東西是immutable,那麼他就是不能有變化的.redux
極端的例子是,不能使用傳統意義的變量, 你要不斷的建立新值來代替舊的值. JavaScript沒有這麼極端, 可是有些語言根本不容許mutate任何東西(Elixir, Erlang還有ML).數組
Javas不是純粹的函數式語言,它能夠在某種程度上假裝成函數式語言.JS中有些數組操做時immutable(意思是:不修改原始值,而是返回一個新的數組).字符串操做老是immutable的(JS使用改變的字符串建立新的字符串). 同時,你也能夠編寫本身的immutable函數.須要注意的是要遵照一些規則.瀏覽器
如今來看看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===samePerson
爲true
,就是這個緣由.)
若是咱們想讓giveAesomePowers
函數不對person對象做出修改,必需要做出一些改變.首先要讓函數變 pure(變純),由於純函數和immutability緊密相關.
爲了讓函數變純,必需要遵照如下規則:
"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-同一輸入==同一響應
幾個特定的方法會在使用的時候致使數組發生mutate,
注意,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
方法有個小知識點(過去困擾過我), 傳遞給sort
的compareFunction
須要返回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的用例中, 絕對不要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最核心思想之一].
默認狀況下,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
相同嗎?用的是三個等號,===
. 緣由是兩個對象的引用地址都指向同一個對象. 內部發生改變了,可是地址沒有變.
什麼是"引用等於"(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須要保證它的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標準的一部分.
這些例子的編寫出發點是從Redux reducer中返回state. 我會展現輸入的state是什麼樣子, 返回的Sate是什麼樣的.
爲了保持實例代碼簡潔. 我會徹底忽略"action"參數. 假定更新能夠有任何action觸發. 固然在你本身的reducer中, 你可能會私用switch
和case
來針對每一個action執行操做,可是我認爲這會增長本部分理解的噪音.
爲了在簡單的React State中使用這些事例, 須要稍做一些調整.
由於React會 淺融合傳遞進this.setState()
的對象.不須要像Redux同樣展開已有的state.
在Redux reducer中,要這麼寫:
return {
...state,
(updates here)
}
複製代碼
對於簡單 React,能夠這麼寫, 不須要展開操做符:
this.setState({
updates here
})
複製代碼
要記住一點,儘管setState
不會執行淺融合,在更新state內嵌套的屬性時也必需要使用展開操做符(任何比第一層更深的部分).
當你想更新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的-對用簡單的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 // 改變屬性值
}
}
}
複製代碼
在更新深度嵌套的對象時,這個代碼很難閱讀.
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
}
}
}
複製代碼
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;
});
}
複製代碼
這個和上面的工做原理相同, 惟一的區別是,你須要構建一個新的對象,並返回一個想要改變的對象.
數組的.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;
});
}
複製代碼
數組的.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 ]; */
}
複製代碼
咱們可使用.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
. 有更多的技巧.
若是你看看上面的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.
});
}
複製代碼
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的一個很好的特性是,由於他很小,目標聚焦(僅僅返回新state的函數), 很容在已有代碼中添加.
Immer向後兼容已經有的Redux reducers,若是你在Immer的produce
函數中包裝已經存在的switch/case
代碼,全部的reducer測試仍然能夠經過.
以前,我演示過, 傳遞給produce
的更新函數能夠隱式返回undefined
,而且會自動挑選出針對draft
state的變化.我沒有提到的是,更新函數能夠一個全新的對象,只要它沒有對draft
做出任何改變.
這意味着,已經編寫好的返回全新state 的Redux reducer,也能夠用Immer的produce
函數包裝,他們應該保持徹底相同. 在這一點上,你能夠輕鬆一塊,一塊地替換掉很難閱讀的immutable 代碼. 看看官方實例從producers返回不一樣數據的各類方法