Immutable总结|原理篇
30 秒速记
- 核心判断:状态更新是否立刻可见取决于调度边界和批处理,不应简单记成“同步”或“异步”
- 原理主线:围绕 「一、前言」、「二、什么是 Immutable Data」、「三、为什么要在React.js中使用Im」 建立输入、状态变化与输出之间的因果关系
- 文章范围:介绍了在React开发中使用Immutable数据结构的原理、优势及其对组件性能优化的作用,涵盖了setState、shouldComponentUpdate、深拷贝与Immutable.js等相关内容,帮助开发者理解如何高效管理和优化前端
- 边界与代价:
React 18自动批处理范围扩大,读取旧值、连续更新和强制同步提交需要分别讨论 - 工程落地:依赖前值时使用函数式更新;只有确有 DOM 读取时才考虑同步提交,并评估阻塞代价
Immutable Data 的价值是更新后返回新对象,同时保留旧数据不变,并用结构共享控制复制成本。 某个深层节点变化时,只重建该节点及其父链,未变化的分支继续复用,因此比每次 deep clone 更适合频繁状态更新。对 React 来说,引用变化可配合浅比较更快判断是否需要重渲染,也方便撤销、重做和状态回溯。代价是要学习新的 Map、List 等 API,还要处理与原生对象混用及资源体积问题。
这篇文章不要按 API 清单来背。先用上面的 Mind Map 建立全局结构,再通过交互 DEMO 观察正常路径和边界路径如何改变状态;阅读正文时重点核对每一步的输入、负责执行的参与者、产生的中间状态以及最终可观察结果。遇到版本敏感结论,要把“历史实现”“当前行为”和“工程兼容策略”分开说明;遇到性能或架构取舍,则用实际指标、失败现象和验证手段支撑判断。
版本校准: 原理文章中的代码代表特定实现与写作时间。应用到当前项目时,应先确认浏览器、框架或工具的主版本,再区分稳定的规范语义、可变化的内部实现和项目自身约束。
# 一、前言
从问题说起:熟悉
React组件生命周期的话都知道:调用setState方法总是会触发render方法从而进行vdom re-render相关逻辑,哪怕实际上你没有更改到Component.state
this.state = {count: 0}
this.setState({count: 0});// 组件 state 并未被改变,但仍会触发 render 方法
- 为了避免这种性能上的浪费,
React提供了一个shouldComponentUpdate来控制触发vdom re-render逻辑的条件。于是PureRenderMixin作为一种优化技巧被使用。它仅仅是浅比较对象,深层次的数据结构根本不管用
js中的Immutable Data
在
javascript中我们可以通过deep clone来模拟Immutable Data,就是每次对数据进行操作,新对数据进行deep clone出一个新数据
- deep clone
- 当然你或许意识到了,这样非常的慢
'use strict';
var cloneDeep = require('lodash.clonedeep');
var data = {
id: 'data',
author: {
name: 'mdemo',
github: 'https://github.com/demohi'
}
};
var data1 = cloneDeep(data);
console.log('equal:', data1===data); //false
data1.id = 'data1';
data1.author.name = 'demohi';
console.log(data.id);// data
console.log(data1.id);// data1
console.log(data.author.name);//mdemo
console.log(data1.author.name);//demohi
这时候 immutableJS 就派得上用场了
var map1 = Immutable.fromJS({a:1, b:1, c:{b:{c:{d:{e:7}}}}});
var map2 = Immutable.fromJS({a:1, b:1, c:{b:{c:{d:{e:7}}}}});
Immutable.is(map1, map2); // true
- 遍历对象不再用
for-in,可以这样:
Immutable.fromJS({a:1, b:2, c:3}).map(function(value, key) { /* do some thing */});
# 二、什么是 Immutable Data
Immutable Data就是一旦创建,就不能再被更改的数据。对Immutable对象的任何修改或添加删除操作都会返回一个新的Immutable对象Immutable实现的原理是Persistent Data Structure(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变- 同时为了避免
deepCopy把所有节点都复制一遍带来的性能损耗,Immutable使用了Structural Sharing····(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。
打印
immutableJS看看有什么东西

一个说明不可变的例子
// 原生对象
let a1 = {
b: 1,
c: {
c1: 123
}
};
let b1 = a1;
b1.b = 2;
console.log(a1.b, b1.b); // 2, 2
console.log(a1 === b1); // true
console.log(a1.c === b1.c); // true
// immutable.js 的Map
let a2 = Immutable.fromJS({
b: 1,
c: {
c1: 123
}
});
let b2 = a2.set('b', 2);
// 对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象
console.log(a2.get('b'), b2.get('b')); // 1, 2 对象 a2 的 b 值并没有变成2。
console.log(a2 === b2); // false
//如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。
console.log(a2.get('c') === b2.get('c')); //true
# 三、为什么要在React.js中使用Immutable
- 它是一个完全独立的库,无论基于什么框架都可以用它。意义在于它弥补了
Javascript没有不可变数据结构的问题- 由于是不可变的,可以放心的对对象进行任意操作。在
React开发中,频繁操作state对象或是store,配合immutableJS快、安全、方便
- 熟悉
React.js的都应该知道,React.js是一个UI = f(states)的框架,为了解决更新的问题,React.js使用了virtual dom,virtual dom通过diff修改dom,来实现高效的dom更新。 - 但是有一个问题。当
state更新时,如果数据没变,你也会去做virtual dom的diff,这就产生了浪费。这种情况其实很常见
- 当然你可能会说,你可以使用
PureRenderMixin来解决呀,PureRenderMixin是个好东西,我们可以用它来解决一部分的上述问题- 但
PureRenderMixin只是简单的浅比较,不使用于多层比较。那怎么办?自己去做复杂比较的话,性能又会非常差
- 方案就是使用
immutable.js可以解决这个问题。因为每一次state更新只要有数据改变,那么PureRenderMixin可以立刻判断出数据改变,可以大大提升性能
Immutable 优点
- Immutable 降低了 Mutable 带来的复杂度
可变(
Mutable)数据耦合了Time和Value的概念,造成了数据很难被回溯
- 节省内存
Immutable.js使用了Structure Sharing会尽量复用内存,甚至以前使用的对象也可以再次被复用。没有被引用的对象会被垃圾回收
import { Map} from 'immutable';
let a = Map({
select: 'users',
filter: Map({ name: 'Cam' })
})
let b = a.set('select', 'people');
a === b; // false
a.get('filter') === b.get('filter'); // true
- Undo/Redo,Copy/Paste,甚至时间旅行这些功能做起来小菜一碟
因为每次数据都是不一样的,只要把这些数据放到一个数组里储存起来,想回退到哪里就拿出对应数据即可,很容易开发出撤销重做这种功能。
- 并发安全
传统的并发非常难做,因为要处理各种数据不一致问题,因此『聪明人』发明了各种锁来解决。但使用了
Immutable之后,数据天生是不可变的,并发锁就不需要了。
- 拥抱函数式编程
Immutable本身就是函数式编程中的概念,纯函数式编程比面向对象更适用于前端开发。因为只要输入一致,输出必然一致,这样开发的组件更易于调试和组装。
Immutable 缺点
- 需要学习新的 API
- 增加了资源文件大小
- 容易与原生对象混淆
# 四、Immutable 的几种数据类型
List: 有序索引集,类似JavaScript中的Array。Map: 无序索引集,类似JavaScript中的Object。OrderedMap: 有序的Map,根据数据的set()进行排序。Set: 没有重复值的集合。OrderedSet: 有序的Set,根据数据的add进行排序。Stack: 有序集合,支持使用unshift()和shift()添加和删除。Range(): 返回一个Seq.Indexed类型的集合,这个方法有三个参数,start表示开始值,默认值为0,end表示结束值,默认为无穷大,step代表每次增大的数值,默认为1.如果start = end,则返回空集合。Repeat(): 返回一个vSeq.Indexe类型的集合,这个方法有两个参数,value代表需要重复的值,times代表要重复的次数,默认为无穷大。Record: 一个用于生成Record实例的类。类似于JavaScript的Object,但是只接收特定字符串为key,具有默认值。Seq: 序列,但是可能不能由具体的数据结构支持。Collection: 是构建所有数据结构的基类,不可以直接构建
上面那么多常用的也就是
List和Map
# 五、几个重要的API
1、fromJS()
fromJS()是最最最常用的将原生JS数据转换为ImmutableJS数据的转换方法。使用方式类似于JSON.parse(),接收两个参数:json数据和reviver函数- 在不传递
reviver函数的情况下,默认将原生JS的Array转为List,Object转为Map
