原文在这里: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返回不一样数据的各类方法