翻译|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返回不一样数据的各类方法

完!

相关文章
相关标签/搜索