[TOC]
什么是不可变性?##
不可变性(Immutability)是函数式编程的核心原则,在面向对象编程里也有大量应用。
可变性(Mutability)是指它在创建之后状态依旧可被改变,不可变性意思是,创建之后,就再也不能被修改了。
关于不可变性,我们从js的数据存储开始说起
1.先来说一下概念 按值传递:函数的形参是被调用时所传实参的副本,修改形参的值并不会影响实参。 按引用传递:形参的值如果被修改,实参也会被修改,两者指向相同的值。
2.上结论 主要分三类来叙述:基本类型、对象、String类
JS中的基本类型按值传递,对象类型按引用传递 (基本类型是不可变的(immutable),只有对象是可变的(mutable).)
- 对象是可变(mutable)的,修改对象本身会影响到共享这个对象的引用。
- 基本类型,是不可变的(immutable),按共享传递与按值传递(call by value)没有任何区别,所以说JS基本类型既符合按值传递,也符合按共享传递。
- string是不可变的(Immutable) - 它们不能被修改,我们能做的就是基于原string操作后得到一个新string。
var obj = {x : 1};
obj.x = 100;
var o = obj;
o.x = 1;
obj.x; // 1, 被修改
o = true;
obj.x; // 1, o与obj的引用已经断了,而是新建了一个值,所以obj不会因o = true改变
在JS中,任何看似对string值的”修改”操作,实际都是创建新的string值。
var str = "abc";
str[0]; // "a"
str[0] = "d";
str; // 仍然是"abc";赋值是无效的。没有任何办法修改字符串的内容
number也是不可变的。否则的话,你试想下这个表达式2 + 3,如果2的含义能被修改,那代码该怎么写啊
3.例子
var a = 1;
function foo(x) {
x = 2;
}
foo(a);
console.log(a); // 仍为1, 未受x = 2赋值所影响
外部传值不改变,内部定义(无 var)覆全局
var obj = {x : 1};
function foo(o) {
o.x = 3;
}
foo(obj);
console.log(obj.x); // 3, 被修改了!--------按引用传递
其他案例:
(1)
var b="外";
function fa(b){
b="内";//此处从外部传值进来
c=3;//定义了一个全局变量
console.log("局部变量:"+b);
};
fa(b);
console.log("全局变量:"+b+";内部(不写var)定义全局变量:"+c);
//结果为:"全局变量:"+外+";内部(不写var)定义全局变量:"+3
(2)
var time="外部";
function changetime(){
time="内部";//由于没有传参进来,所以此处的time的自己创建的全局变量time,覆盖了外部的变量;
}
changetime();
document.write(time);//内部
var a,b;
(function(){
alert(a);
alert(b);
var a=3;
b=3; //此句没有var,所以定义的是全局变量覆盖了前面的b;前两句相当于var a=b=3;
alert(a);
alert(b);
})();
alert(a);
alert(b);
结果:
undefined,undefined,3,3,undefined,3
如何使用可变与不可变的性质
JSON.parse(JSON.stringfy())
我们在开发中,使用了let b = JOSN.parse(JSON.stringify(a))的方法实现了其深复制,保持了a的不可变性,在修改b的时候不影响a,非常的简单。
特点是:用法简单,然而使用这种方法会有一些隐藏的坑:因为在序列化JavaScript对象时,所有函数和原型成员会被有意忽略,
通俗点说,JSON.parse(JSON.stringfy(X)),其中X只能是Number, String, Boolean, Array, 扁平对象,即那些能够被 JSON 直接表示的数据结构。
let dif = {name:'zzf',fun:function(){console.log(this.name)}}
let dif2 = JSON.parse(JSON.stringify(dif))
结果得到:dif2 = {name: "zzf"}
JavaScript里目前还没有不可变的list和map,所以暂时我们还是需要三方库的帮助。
immutability-helper-x
从掘金看到阿里团队的分享——《如何管理好10万行代码的前端单页面应用》,地址如下:
juejin.cn/post/684490…
阿里团队使用轻量级的immutable方案immutability-helper,相比完全拷贝一份(deep clone)性能更优、存储空间利用率更高。
此插件是一个被遗留的插件,如今好像更新为这个地址:github.com/kolodny/imm…, 阿里数据团队将其优化后发布为**immutability-helper-x:github.com/ProtoTeam/i… (facebook也有一个类似的插件immutable-js**,可以使用与nodejs中,地址为:github.com/facebook/im…)
immutability-helper-x在前端项目中的使用
install
npm install -S immutability-helper-x
usage
import update, { updateChain } from 'immutability-helper-x';
const data = {
a: {
b: {
c: 1,
d: 2,
e: [3, 2, 1],
}
},
f: 4,
};
// 1. if you want to update the value of data.a.b, you can
update.$set(data, 'a.b', newValue);
// or
update.$set(data, ['a', 'b'], newValue);
// 2. other methods like push and apply in original update lib
update.$push(data, 'arr', ['car', 'bus']);
update.$apply(data, 'a.b', value => ++value);
// 3. extend method
// step 1
update.extend('$addtax', function(tax, original) {
return original + (tax * original);
});
// step 2
update.$addtax(data, 'price', 0.8);
// or
update(data, {
price: { $addtax: 0.8 },
});
// 4. update multiple values at the same time in a chain
const d = updateChain(data)
.$set('a.b.c', 444)
.$merge('a.b', {
d: 555,
})
.$push('a.e', [4])
.$unshift('a.e', [0])
.$splice('a.e', [[1, 0, 8]])
.$apply('a.b.d', val => val + 100)
.value();
** 在vue中使用案例: **
<script>
import update, { updateChain } from 'immutability-helper-x';
... ...
mounted () {
const data = {
a: {
b: {
c: 1,
d: 2,
e: [3, 2, 1],
}
},
f: 4,
};
// dataCopy为data的深拷贝,并update了dataCopy.a.b的值;原数据不变
let dataCopy = update.$set(data, 'a.b', 'newValue');
console.log(data)
console.log(dataCopy)
}
打印结果显示data未发生变化
dataCopy值为:
dataCopy = {
a: {
b: 'newValue'
},
f: 4,
};
【推荐】# immer.js
mmer 是 mobx 的作者写的一个 immutable 库,核心实现是利用 ES6 的 proxy,几乎以最小的成本实现了 js 的不可变数据结构,简单易用、体量小巧、设计巧妙,满足了我们对JS不可变数据结构的需求。
数据处理存在的问题
先定义一个初始对象,供后面例子使用:
首先定义一个currentState对象,后面的例子使用到变量currentState时,如无特殊声明,都是指这个currentState对象
let currentState = {
p: {
x: [2],
},
}
安装
npm i --save immer
下面将对 Immer 的常用 api 分别进行介绍。
概念说明
Immer 涉及概念不多,在此将涉及到的概念先行罗列出来
-
currentState 被操作对象的最初状态
-
draftState 根据 currentState 生成的草稿状态,它是 currentState 的代理,对 draftState 所做的任何修改都将被记录并用于生成 nextState 。在此过程中,currentState 将不受影响
-
nextState 根据 draftState 生成的最终状态
-
produce 生产 用来生成 nextState 或 producer 的函数
-
producer 生产者 通过 produce 生成,用来生产 nextState ,每次执行相同的操作
-
recipe 生产机器 用来操作 draftState 的函数
常用api介绍
使用 Immer 前,请确认将immer包引入到模块中
import produce from 'immer'
or
import { produce } from 'immer'
这两种引用方式,produce 是完全相同的
produce
备注:出现PatchListener先行跳过,后面章节会做介绍
第1种使用方式:
语法:
produce(currentState, recipe: (draftState) => void | draftState, ?PatchListener): nextState
例子1:
let nextState = produce(currentState, (draft) => {
})
currentState === nextState; // true
例子2:
let currentState = {
a: [],
p: {
x: 1
}
}
let nextState = produce(currentState, (draft) => {
draft.a.push(2);
})
currentState.a === nextState.a; // false
currentState.p === nextState.p; // true
由此可见,对 draftState 的修改都会反应到 nextState 上,而 Immer 使用的结构是共享的,nextState 在结构上又与 currentState 共享未修改的部分。
自动冻结功能
Immer 还在内部做了一件很巧妙的事情,那就是通过 produce 生成的 nextState 是被冻结(freeze)的,(Immer 内部使用Object.freeze方法,只冻结 nextState 跟 currentState 相比修改的部分),这样,当直接修改 nextState 时,将会报错。
这使得 nextState 成为了真正的不可变数据。
let nextState = produce(currentState, (draft) => {
draft.p.x.push(2);
})
currentState === nextState; // true
第2种使用方式
利用高阶函数的特点,提前生成一个生产者 producer
语法:
produce(recipe: (draftState) => void | draftState, ?PatchListener)(currentState): nextState
例子:
let producer = produce((draft) => {
draft.x = 2
});
let nextState = producer(currentState);
** recipe的返回值**
recipe 是否有返回值,nextState 的生成过程是不同的: recipe 没有返回值时:nextState 是根据 recipe 函数内的 draftState 生成的; recipe 有返回值时:nextState 是根据 recipe 函数的返回值生成的;
let nextState = produce(
currentState,
(draftState) => {
return {
x: 2
}
}
)
此时,nextState 不再是通过 draftState 生成的了,而是通过 recipe 的返回值生成的。
** recipe中的this**
recipe 函数内部的this指向 draftState ,也就是修改this与修改 recipe 的参数 draftState ,效果是一样的。
注意:此处的 recipe 函数不能是箭头函数,如果是箭头函数,this就无法指向 draftState 了
produce(currentState, function(draft){
// 此处,this 指向 draftState
draft === this; // true
})
react项目中的使用
首先定义一个state对象,后面的例子使用到变量state或访问this.state时,如无特殊声明,都是指这个state对象
state = {
members: [
{
name: 'ronffy',
age: 30
}
]
}
** 抛出需求 **
就上面定义的state,我们先抛一个需求出来,好让后面的讲解有的放矢:members 成员中的第1个成员,年龄增加1岁
优化setState方法
错误示例
this.state.members[0].age++;
只所以有的新手同学会犯这样的错误,很大原因是这样操作实在是太方便了,以至于忘记了操作 State 的规则。
下面看下正确的实现方法
setState的第1种实现方法
const { members } = this.state;
this.setState({
members: [
{
...members[0],
age: members[0].age + 1,
},
...members.slice(1),
]
})
setState的第2种实现方法
this.setState(state => {
const { members } = state;
return {
members: [
{
...members[0],
age: members[0].age + 1,
},
...members.slice(1)
]
}
})
以上2种实现方式,就是setState的两种使用方法,相比大家都不陌生了,所以就不过多说明了,接下来看下,如果用 Immer 解决,会有怎样的烟火?
用immer更新state
this.setState(produce(draft => {
draft.members[0].age++;
}))
是不是瞬间代码量就少了很多,阅读起来舒服了很多,而且更易于阅读了。
优化reducer
immer的produce的拓展用法
在开始正式探索之前,我们先来看下 produce 第2种使用方式的拓展用法:
例子:
let obj = {};
let producer = produce((draft, arg) => {
obj === arg; // true
});
let nextState = producer(currentState, obj);
相比 produce 第2种使用方式的例子,多定义了一个obj对象,并将其作为 producer 方法的第2个参数传了进去;可以看到, produce 内的 recipe 回调函数的第2个参数与obj对象是指向同一块内存。
ok,我们在知道了 produce 的这种拓展用法后,看看能够在 Redux 中发挥什么功效?
普通reducer怎样解决上面抛出的需求
const reducer = (state, action) => {
switch (action.type) {
case 'ADD_AGE':
const { members } = state;
return {
...state,
members: [
{
...members[0],
age: members[0].age + 1,
},
...members.slice(1),
]
}
default:
return state
}
}
集合immer,reducer可以怎样写
const reducer = (state, action) => produce(state, draft => {
switch (action.type) {
case 'ADD_AGE':
draft.members[0].age++;
}
})
可以看到,通过 produce ,我们的代码量已经精简了很多; 不过仔细观察不难发现,利用 produce 能够先制造出 producer 的特点,代码还能更优雅:
const reducer = produce((draft, action) => {
switch (action.type) {
case 'ADD_AGE':
draft.members[0].age++;
}
})
总结
-
** 单纯用在vue前端:** ** immer.js: www.cnblogs.com/ronffy/p/im…** immutability-helper-x:github.com/ProtoTeam/i…
-
用于前端和node immutable-js:github.com/facebook/im…
-
JSON.parse(JSON.stringfy()) 可以实现json信息的深拷贝,但是所有函数和原型成员会被忽略丢失**。**
-
数组也可以采用let b = a.slice();实现对a的深拷贝
参考文章,在此感谢原作者: Immer 实战讲解:www.cnblogs.com/ronffy/p/im…