Immer

[[toc]]

React中浅层次拷贝的问题

const detail = {name:'和振峰',school:{loc:'shijaizhuang'}}
const copy = Object.assign({},detail);
copy.school.loc ="北京";
//此时你会发现我们的detail.school.loc也变成了"北京"了
copy.school === detail.school
//此时copy.school和detail.school指向同一个对象,引用相同,一个值被修改那么另一个同样被修改

要解决上面的问题,一定要深克隆,而不是浅层次的拷贝

React中引用类型导致组件不更新

栗子

class Index extends Component {
state = {
ff: {
a: 1, b: 2, c: 3, school: {
location: "jing999.cn",
name: "和振峰"
}
}
};
add = () => {
const {ff} = this.state;
// ff.school.name = 11; //[1]

this.setState({
ff: {
...ff,
school: {
location: "jing999.cn",
name: 11
}
}
}); //[2]

// let draftFF = produce(ff, draft => {
// draft.school.name = 11;
// });
// this.setState({
// ff: draftFF
// }) // [3]
};

shouldComponentUpdate(nextProps, nextState, nextContext) {
//[1]这里永远是true ,因为内存地址是完全一样的
//[2]这里永远是false ,因为克隆后地址一直在变
//[3]只有第一次是false,后面不会变,值变化后跟之前的一样,性能最高
console.log(nextState.ff === this.state.ff);
if (nextState.ff === this.state.ff) {
return false
}
return true;
}
render() {
return (
<>
<button onClick={this.add}>点击</button>
{this.state.ff.school.name}
</>
);
}
}
export default Index

简介不可变数据

React在减少重复渲染方面确实是有一套独特的处理办法,那就是虚拟DOM,但显然在首次渲染的时候React绝无可能超越原生的速度,或者一定能将其它的框架比下去。
但是每次数据变动都会执行render,大大影响了性能,特别是在移动端。

JavaScript 中的对象一般是可变的(Mutable),因为使用了引用赋值,新的对象简单的引用了原始对象,改变新的对象将影响到原始对象。如 foo={a: 1}; bar=foo; bar.a=2 你会发现此时 foo.a 也被改成了 2。虽然这样做可以节约内存,但当应用复杂后,这就造成了非常大的隐患,Mutable 带来的优点变得得不偿失。为了解决这个问题,一般的做法是使用 shallowCopy(浅拷贝)或 deepCopy(深拷贝)来避免被修改,但这样做造成了 CPU 和内存的浪费。

不可变数据就是一旦创建,就不能再被直接更改的数据。对Immutable 对象的任何修改或添加删除操作都会返回一个新的Immutable对象。
Immutable 实现的原理是 Persistent Data Structure(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,
Immutable 使用了Structural Sharing(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。

Object.freeze() 方法可以冻结一个对象,冻结的对象不能添加新的属性,不能修改其已有属性的值,不能删除已有属性,以及不能修改该对象已有属性的可枚举性、可配置性、可写性。尝试修改会静默失败或抛出TypeError类型的错误。相关函数还包括:
Object.isExtensible()
Object.seal()Object.defineProperty()均为ES5中定义的方法

immerJS

Immer是mobx的作者写的一个immutable库,核心实现是利用 ES6 的 proxy,几乎以最小的成本实现了js的不可变数据结构

使用方式

import produce from "immer"

const baseState = [
{
todo: "Learn typescript",
done: true
},
{
todo: "Try immer",
done: false
}
]

const nextState = produce(baseState, draftState => {
draftState.push({todo: "Tweet about it"})
draftState[1].done = true
})
/**
* 带深度合并的经典React.setState
*/
onBirthDayClick1 = () => {
this.setState(prevState => ({
user: {
...prevState.user,
age: prevState.user.age + 1
}
}))
}

/**
* ...但是,由于setState接受函数,
* 上面相当于所有的东西都会重新渲染,用immerjs优化后
*/
onBirthDayClick2 = () => {
this.setState(
produce(draft => {
//这里只有age发生的变化,state里面的值,他们的引用不会发生变化,不会引起不必要的更新
draft.user.age += 1
})
)
}

图解

上图可以:immer.js的方法修改了对象的某一个属性的时候,该属性的所有的父级属性的引用都会发生改变,而其他属性的引用都是共享

nextState = produce(currentState, draftState => {
...
});

主要思想就是先在currentState基础上生成一个代理draftState,之后的所有修改都会在draftState上进行,避免直接修改currentState,而当修改结束后,再从draftState基础上生成nextState。

Immer内部使用Object.freeze()方法,只冻结nextState跟currentState相比修改的部分

  • 优点:
    • 降低了 Mutable 带来的复杂度
    • 节省内存
    • Undo/Redo (因为每次数据都是不一样的,只要把这些数据放到一个数组里储存起来,想回退到哪里就拿出对应数据即可,很容易开发出撤销重做这种功能。)
    • 拥抱函数式编程 (纯函数式编程比面向对象更适用于前端开发。因为只要输入一致,输出必然一致,这样开发的组件更易于调试和组装。)
    • 写法更加优雅
  • 对比ImmutableJS
    • 增加了资源文件大小(压缩后代码大小16K,immer是4k)
    • 需要使用者学习它的数据结构操作方式,没有 Immer 提供的使用原生对象的操作方式简单、易用;
    • 它的操作结果需要通过toJS方法才能得到原生对象,这使得在操作一个对象的时候,时刻要主要操作的是原生对象还是 ImmutableJS 的返回结果,稍不注意,就会产生意想不到的 bug。

示例

immutable.js

看看Immutable怎么使用的,谁能保证以后一定不用这个

import Immutable from "immutable"

let map1 = Immutable.Map({a:1, b:2, c:3,school:{
location:"shijiazhuang",
name :"和振峰"
}});
let map2 = map1.set('b', 50);//我这里仅仅设置了b的值,那么其他的值都会共享

console.log(map1) //返回的是一个Map的对象,不能直接在原数据更改

console.log('引用相等',map1===map2); //immutable.js中每次返回的引用都是不一样的,此处返回false

console.log('school的引用没有变化',map2.school===map1.school);//immutable.js中没有变化的对象将会共享,所以此处返回true

let map3 = {a:1,b:2,c:3}
let map4 = map3; //map4拿到的是map3的指针,所以一个变化后另外一个也会变化,但是变化的是值,引用本身是不变化的,所以map3===map4返回true
map4.c =4;
console.log('map3===map4',map3===map4);

Immer.js

import produce from "immer"
let map1 = {
a: 1, b: 2, c: 3, school: {
location: "shijiazhuang",
name: "和振峰"
}
};
let map2 = produce(map1, draft => {
draft.b = 50
});
console.log(map2); //跟源原对象一样的类型
// console.log(map2.a = 100); // Cannot assign to `read only` property 'a' of object 要保持跟源对象一致,值类型不能被修改
console.log(map1 === map2); //false 此处跟immutable一致,每次返回的引用都是不一样的
console.log(map1.school === map2.school); //没有变化的对象将会共享
console.log(map2.school.name = 1); //school的引用地址没有发生改变,可以修改,双方发生变化 不建议修改

实践

为什么你要在React.js中使用Immutable Data熟悉React.js的都应该知道,React.js是一个UI = f(states)的框架,为了解决更新的问题,React.js使用了virtual dom,virtual dom通过diff修改dom,来实现高效的dom更新。听起来很完美吧,但是有一个问题。
当state更新时,如果子组件数据没变,你也会去做virtual dom的diff,这就产生了浪费。

  1. 与 React 搭配使用,Pure Render
    熟悉 React 的都知道,React 做性能优化时有一个避免重复渲染的大招,就是使用 shouldComponentUpdate(),但它默认返回 true,即始终会执行 render() 方法,然后做 Virtual DOM 比较,并得出是否需要做真实 DOM 更新,这里往往会带来很多无必要的渲染并成为性能瓶颈。

当然我们也可以在 shouldComponentUpdate() 中使用使用 deepCopy 和 deepCompare 来避免无必要的 render(),但 deepCopy 和 deepCompare 一般都是非常耗性能的

搭配

shouldComponentUpdate、PureComponent、memo、useMemo
memo(xxxx, (prevProps, nextProps) => prevProps.data === nextProps.data);

  1. React 建议把 this.state 当作 Immutable 的,因为之前修改前需要做一个 deepCopy,显得麻烦

  2. 与 Redux 搭配使用

参考

官方文档

Immutable 详解及 React 中实践

seamless-immutable

Immer.js简析