咱们在学习 React 的过程当中常常会碰到一个概念,那就是数据的不可变性(immutable),不可变数据是函数式编程里的重要概念,由于可变数据在提供方便的时候会带了不少棘手的反作用,那么咱们应该如何处理这些棘手的问题,如何实现不可变数据呢?javascript
咱们应该都知道的基本知识,在JavaScript中分为原始类型和引用类型. 前端
JavaScript原始类型:Undefined、Null、Boolean、Number、String、Symboljava
JavaScript引用类型:Objectnode
同时引用类型在使用过程当中常常会产生反作用.react
const person = {player: {name: 'Messi'}};
const person1 = person;
console.log(person, person1);
//[ { name: 'Messi' } ] [ { name: 'Messi' } ]
person.player.name = 'Kane';
console.log(person, person1);
//[ { name: 'Kane' } ] [ { name: 'Kane' } ]
复制代码
咱们看到,当修改了person
中属性后,person1
的属性值也随之改变,由于这两个变量的指针指向了同一块内存,当一个变量被修改后,内存随之变更,而另外一个变量因为指向同一块内存,天然也随之变化了,这就是引用类型的反作用.git
但是绝大多数状况下咱们并不但愿person1
的属性值也发生改变,咱们应该如何解决这个问题?es6
在ES6中咱们能够用Object.assign
或者 ...
对引用类型进行浅复制.github
const person = [{name: 'Messi'}];
const person1 = person.map(item =>
({...item, name: 'Kane'})
)
console.log(person, person1);
// [{name: 'Messi'}] [{name: 'Kane'}]
复制代码
person
的确被成功复制了,可是之因此咱们称它为浅复制,是由于这种复制只能复制一层,在多层嵌套的状况下依然会出现反作用.面试
const person = [{name: 'Messi', info: {age: 30}}];
const person1 = person.map(item =>
({...item, name: 'Kane'})
)
console.log(person[0].info === person1[0].info); // true
复制代码
上述代码代表当利用浅复制产生新的person1
后其中嵌套的info
属性依然与原始的person
的info
属性指向同一个堆内存对象,这种状况依然会产生反作用.typescript
咱们能够发现浅复制虽然能够解决浅层嵌套的问题,可是依然对多层嵌套的引用类型无能为力.
既然浅复制(克隆)没法解决这个问题,咱们天然会想到利用深克隆的方法来实现多层嵌套复制的问题.
咱们以前已经讨论过如何实现一个深克隆,在此咱们不作深究,深克隆毫无疑问能够解决引用类型产生的反作用.
实现一个在生产环境中能够用的深克隆是很是繁琐的事情,咱们不只要考虑到正则、Symbol、Date等特殊类型,还要考虑到原型链和循环引用的处理,固然咱们能够选择使用成熟的开源库进行深克隆处理.
但是问题就在于咱们实现一次深克隆的开销太昂贵了,如何实现深克隆中咱们展现了一个勉强可使用的深克隆函数已经处理了至关多的逻辑,若是咱们每使用一次深克隆就须要一次如此昂贵的开销,程序的性能是会大打折扣.
const person = [{name: 'Messi', info: {age: 30}}];
for (let i=0; i< 100000;i++) {
person.push({name: 'Messi', info: {age: 30}});
}
console.time('clone');
const person1 = person.map(item =>
({...item, name: 'Kane'})
)
console.timeEnd('clone');
console.time('cloneDeep');
const person2 = lodash.cloneDeep(person)
console.timeEnd('cloneDeep');
// clone : 105.520ms
// cloneDeep : 372.839ms
复制代码
咱们能够看到深克隆的的性能相比于浅克隆大打折扣,可是浅克隆又不能从根本上杜绝引用类型的反作用,咱们须要找到一个兼具性能和效果的方案.
immutable.js是正是兼顾了使用效果和性能的解决方案
原理以下: Immutable实现的原理是Persistent Data Structur(持久化数据结构),对Immutable对象的任何修改或添加删除操做都会返回一个新的Immutable对象, 同时使用旧数据建立新数据时,要保证旧数据同时可用且不变。
为了不像 deepCopy
同样 把全部节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享),即若是对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。请看下面动画
咱们看到动画中右侧的子节点因为发生变化,相关父节点进行了重建,可是左侧树没有发生变化,最后造成的新的树依然复用了左侧树的节点,看起来真的是无懈可击.
immutable.js 的实现方法确实很高明,毕竟是花了 Facebook 工程师三年打造的全新数据结构,相比于深克隆,带来的 cpu 消耗很低,同时内存占用也很小.
可是 immutable.js 就没有弊端吗?
在使用过程当中,immutable.js也存在不少问题.
我目前碰到的坑有:
toJS()
转化为原生数据结构再进行调试,这让人很崩溃.toJS
转化为正常的 js 数据结构,这个时候新旧 props 就永远不会相等了,就致使了大量重复渲染,严重下降性能.immutable.js在某种程度上来讲,更适合于对数据可靠度要求颇高的大型前端应用(须要引入庞大的包、额外的学习成本甚至类型检测工具对付immutable.js与原生js相似的api),中小型的项目引入immutable.js的代价有点高昂了,但是咱们有时候不得不利用immutable的特性,那么如何保证性能和效果的状况下减小immutable相关库的体积和提升api友好度呢?
咱们的原则已经提到了,要尽量得减少体积,这就注定了咱们不能像immutable.js那样本身定义各类数据结构,并且要减少使用成本,因此要用原生js的方式,而不是自定义数据结构中的api.
这个时候须要咱们思考如何实现上述要求呢?
咱们要经过原生js的api来实现immutable,很显然咱们须要对引用对象的set、get、delete等一系列操做的特性进行修改,这就须要defineProperty
或者Proxy
进行元编程.
咱们就以Proxy
为例来进行编码,固然,咱们须要事先了解一下Proxy
的使用方法.
咱们先定义一个目标对象
const target = {name: 'Messi', age: 29};
复制代码
咱们若是想每访问一次这个对象的age
属性,age
属性的值就增长1
.
const target = {name: 'Messi', age: 29};
const handler = {
get: function(target, key, receiver) {
console.log(`getting ${key}!`);
if (key === 'age') {
const age = Reflect.get(target, key, receiver)
Reflect.set(target, key, age+1, receiver);
return age+1
}
return Reflect.get(target, key, receiver);
}
};
const a = new Proxy(target, handler);
console.log(a.age, a.age);
//getting age!
//getting age!
//30 31
复制代码
是的Proxy
就像一个代理器,当有人对目标对象进行处理(set、has、get等等操做)的时候它会拦截操做,并用咱们提供的代码进行处理,此时Proxy
至关于一个中介或者叫代理人,固然Proxy
的名字也说明了这一点,它常常被用于代理模式中,例如字段验证、缓存代理、访问控制等等。
咱们的目的很简单,就是利用Proxy
的特性,在外部对目标对象进行修改的时候来进行额外操做保证数据的不可变。
在外部对目标对象进行修改的时候,咱们能够将被修改的引用的那部分进行拷贝,这样既能保证效率又能保证可靠性.
function createState(target) {
this.modified = false; // 是否被修改
this.target = target; // 目标对象
this.copy = undefined; // 拷贝的对象
}
复制代码
createState.prototype = {
// 对于get操做,若是目标对象没有被修改直接返回原对象,不然返回拷贝对象
get: function(key) {
if (!this.modified) return this.target[key];
return this.copy[key];
},
// 对于set操做,若是目标对象没被修改那么进行修改操做,不然修改拷贝对象
set: function(key, value) {
if (!this.modified) this.markChanged();
return (this.copy[key] = value);
},
// 标记状态为已修改,并拷贝
markChanged: function() {
if (!this.modified) {
this.modified = true;
this.copy = shallowCopy(this.target);
}
},
};
// 拷贝函数
function shallowCopy(value) {
if (Array.isArray(value)) return value.slice();
if (value.__proto__ === undefined)
return Object.assign(Object.create(null), value);
return Object.assign({}, value);
}
复制代码
createState
接受目标对象state
生成对象store
,而后咱们就能够用Proxy
代理store
,producer
是外部传进来的操做函数,当producer
对代理对象进行操做的时候咱们就能够经过事先设定好的handler
进行代理操做了.const PROXY_STATE = Symbol('proxy-state');
const handler = {
get(target, key) {
if (key === PROXY_STATE) return target;
return target.get(key);
},
set(target, key, value) {
return target.set(key, value);
},
};
// 接受一个目标对象和一个操做目标对象的函数
function produce(state, producer) {
const store = new createState(state);
const proxy = new Proxy(store, handler);
producer(proxy);
const newState = proxy[PROXY_STATE];
if (newState.modified) return newState.copy;
return newState.target;
}
复制代码
producer
并无干扰到以前的目标函数.const baseState = [
{
todo: 'Learn typescript',
done: true,
},
{
todo: 'Try immer',
done: false,
},
];
const nextState = produce(baseState, draftState => {
draftState.push({todo: 'Tweet about it', done: false});
draftState[1].done = true;
});
console.log(baseState, nextState);
/* [ { todo: 'Learn typescript', done: true }, { todo: 'Try immer', done: true } ] [ { todo: 'Learn typescript', done: true , { todo: 'Try immer', done: true }, { todo: 'Tweet about it', done: false } ] */
复制代码
没问题,咱们成功实现了轻量级的 immutable.js,在保证 api友好的同时,作到了比 immutable.js 更小的体积和不错的性能.
实际上这个实现就是不可变数据库immer 的迷你版,咱们阉割了大量的代码才缩小到了60行左右来实现这个基本功能,实际上除了get/set
操做,这个库自己有has/getOwnPropertyDescriptor/deleteProperty
等一系列的实现,咱们因为篇幅的缘由不少代码也十分粗糙,深刻了解能够移步完整源码.
在不可变数据的技术选型上,我查阅了不少资料,也进行过实践,immutable.js 的确十分难用,尽管我用他开发过一个完整的项目,由于任何来源的数据都须要经过 fromJS()将他转化为 Immutable 自己的结构,而咱们在组件内用数据驱动视图的时候,组件又不能直接用 Immutable 的数据结构,这个时候又须要进行数据转换,只要你的项目沾染上了 Immutable.js 就不得不将整个项目所有的数据结构用Immutable.js 重构(不然就是处处可见的 fromjs 和 tojs 转换,一方面影响性能一方面影响代码可读性),这个解决方案的侵入性极强,不建议你们轻易尝试.
本文主要参考: