学习前端不到一年,在编写react框架下的代码时,经常会遇到明明已经改变了props页面却没有重新渲染的问题。于是为自己整理了一下关于react中的浅复制与深复制的区别以及实现方式,希望对刚刚入门前端的童鞋们也有所帮助。
首先我们通过下面一个例子来理解一下什么是浅复制。
/*代码一*/
let obj = {a: 1};
let newObj = obj;
newObj.a = 2;
console.log(obj.a);
//log输出为 2
我们都知道 javascript 存储对象是只存地址的,在上面的代码中
newObj = obj
其实是将 obj 对象所指向的地址赋给 newObj ,也就是说这步操作后 obj 和 newObj 指向了同一块内存地址,所以我们对 newObj.a 赋值其实等同于对 obj.a 进行赋值。所以浅复制可以理解为只复制了一个内存地址。在我们写代码的时候最常用的就是浅复制,比如我们经常会写下面的代码。
/*代码二*/
/*......*/
this.state = {
id : 1,
value : "test"
}
/*......*/
render(
const { id, value } = this.state;
return(
<div> id: { id }, value: { value } div> ) )
这里的
const { id, value } = this.state
其实就是一次浅复制, 我们通常用于在渲染的时候提供一些变化的值,而不需要我们在render中对某个变量进行其他操作。所以在明确只是浅复制的情况下,尽量使用const定义变量,因为const定义的变量不能赋值或更改,这样就避免了后面不小心改变了该变量而引起的问题了。浅复制的好处就是可以有效的节约内存地址,避免不必要的内存浪费。回到代码一,如果不希望只对 obj 复制一个内存地址,那么就要给 newObj 分配一个新的内存地址,然后进行赋值。
/*代码三*/
let obj = {a: 1};
//分配一个新地址给newObj
let newObj = {};
//赋值
newObj = obj;
newObj.a = 2;
console.log(obj.a);
//log输出为 1,obj的值没有改变
这里可以看作一次深复制,我们获得了一个新的对象以及新的内存地址,之后对newObj的操作不会影响到obj,可以大胆放心的操作。
代码三的例子是一次显而易见的深复制,先分配一个内存地址然后再赋值。除了这种方法,我们还可以使用es6中的Object.assign()方法实现深复制。
/*代码四*/
let obj = {a: 1, b:{ c: 2}};
let newObj = Object.assign({},obj);
newObj.a = 2;
newObj.b.c = 3;
console.log(obj.a);
//log输出为 1,obj的值没有改变
console.log(obj.b.c)
//log输出为 3,obj的值改变
可以看到对 newObj.a 赋值后obj并没有改变,然而对 newObj.b.c 赋值之后, obj的值却改变了。这是因为 Object.assign() 方法只能深复制第一层的变量,所以第二层其实是一次浅复制。我们需要对obj.b也用一次Object.assign()才能完成一次完整的深复制。如果obj里面有很多层,就要循环嵌套使用Object.assign()方法很多次。代码三中的方法也只能深复制一层。
在ES7中可以利用 … 对对象解构并且对对象中需要深复制的变量进行拷贝。
/*代码五*/
let obj = {a:1,b:2};
let newObj = {...obj,a:2};//对a深复制
newObj.b = 3;//对b浅复制
console.log(obj);
//{a:1,b:3}
console.log(newObj);
//{a:2,b:3}
在reducer中经常会用到这种方式来更新我们的state,从而达到重新渲染的目的。
/*代码六*/
const initialState = {
count: 1,
value: { name : "Michael" }
}
function reducer(state = initialState, action){
switch(action.type){
case ADD_COUNT:
return { ...state, count: state.count + 1 };
case CHANGE_NAME:
return { ...state, value : { name : "Lyle" } }
default:
return state;
}
}
改变state需要用到 setState() 方法,setState() 方法属于深复制,最常用的写法就是
/*代码七*/
this.state = {
value: { a: 1 }
}
const { value } = this.state;//浅复制
value.a = 2;
console.log(this.state.value.a);//输出2,但dom不更新
this.setState({ value });//dom更新
这里 value.a = 2 虽然已经改变了state中value的值,但由于是浅复制,新旧value指向的是同一块内存地址,组件更新时(state,props改变)默认只比较新旧对象的内存地址是否一致,如果一致则不更新。同理,如果在reducer中,直接对当前的state进行修改并返回props,相应的调用该props的组件不会更新渲染。
/*代码八*/
const initialState = {
count: 1
}
function reducer(state = initialState, action){
switch(action.type){
case ADD_COUNT:
state.count += 1;
return { ...state }; //state改变,用到该state的组件不更新渲染
default:
return state;
}
}
基于组件更新的原理,即比较新旧state或props是否指向同一块内存地址,如果是则不更新,如果不是则更新。也就是说就算是两个对象的值相等但不指向同一地址,dom也会重新渲染。这并不是我们想要看到的,我们需要的是当props的值改变的时候dom重新渲染。我们可以在shouldComponentUpdate()方法里面定义dom是否更新的条件,如 if ( props === nextProps ) return true。
之前谈到的深复制基本是只能复制一层变量,或者必须嵌套着复制多层变量。如果想要更方便的完全复制一个对象,我们可以使用以下方法。
1.使用JSON.stringify()和parse()方法,如:
const newValue = JSON.parse(JSON.stringify(value));
2.lodash —— .clone(obj, true) / .cloneDeep(obj)
1.在reducer中更新state的时候尽量确保每次都返回一个新(深复制)的对象;
2.在常用的组件中使用shouldComponentUpdate()方法提高渲染效率;