react中直接修改state解构出来的值合理吗?

第一个栗子🌰

state = {
  name: 'Sam'
}

fn() {
  const { data } = this.state;
  data.name = 'xiaobe';
  this.setState({
    data: { ...data }
  }) 
}

没有出现警告,且能实现效果,但这样的做法合理吗?

第二个栗子🌰:
使用了lodash/find,重新赋值了一个新的数组,但是原数组依旧被更改了,为什么?

import find from 'lodash/find';

state = {
  data: [{
    id: 1,
    name: 'xiaobe'
  }, {
    id: 2,
    name: 'Sam'
  }]
}

fn() {
   const { data } = this.state;
   const newData = [...data];
   console.log('修改前 data', data);
   const newItem = find(newData, { id: 1 }) || {};
   console.log('修改前 newItem', newItem);
   if (Object.keys(newItem).length) {
      newItem.name = 'new-xiaobe';
    }
   console.log('修改后 newItem', newItem);
   this.setState(
     {
       data: [...newData],
     },
     () => {
       console.log('修改后 data', data);
     },
    );
}

发现,修改前 data里第一条name已经从xiaobe变成了new-xiaobe,请问为什么?

阅读 6k
3 个回答

在回答问题之前,先补充以下几个知识点:

  • 栈内存和堆内存。基本数据类型的数据是存在栈内存的,而引用类型的数据则是存在堆内存的,在栈内存里面存放的实际是数据在堆内存中的内存地址。
  • 浅拷贝和深拷贝。浅拷贝仅仅是复制了数据在栈内存中的内存地址,而深拷贝指在计算机中开辟了一块新的内存地址用于存放复制的对象。

第一个栗子:
首先这样子的写法是没有问题,react调度的时候会判断oldData和newData是否相等,不相等时则更新视图,引用类型之间的比较实际就是内存地址的比较,如果内存地址是一样则判断结果为true,不一样则判断结果为false。这里的{...data},相当于重新开辟了内存地址,newData相比较oldData,内存地址发生了改变,那么react便会知道其发生了改变,然后去更新视图。但这种写法并不是最好的,因为毕竟是对原数据做了修改,而this.setState又是异步的,如果在this.setState之后紧跟着依据data又做了其他相应的操作,那么就会有问题。我个人推荐的写法应该是这样子的:

state = {
  name: 'Sam'
}

fn() {
  const { data } = this.state;
  this.setState({
    data: { ...data , name:'xiaobe' }
  }) 
}

第二个栗子:
理解这个问题的核心就是得了解[...data]到底干了啥?下面以你提供的数据来解释以下:

为了方便理解,我改造一下data。

const a = {
    id: 1,
    name: 'xiaobe'
}
const b = {
    id: 2,
    name: 'Sam'
}
const data = [a,b]

[...data]的流程如下:

const newData = [] //new Array()

newData.push(a)
newData.push(b)

通过上面的代码,我们发现如果直接判断newData===data,结果一定为false,因为newData的内存地址和data的不一致。但是如果你判断newData[0]===data[0]以及newData[1]===data[1],那结果变为true,子项的内存地址并没有发生变化。当你修改第一项的name值之后,便会影响到data和newData,因为这个子项被data和newData都引用了。

这样解构就没有意义了。首先你要理解对象实际上是一个引用来的:

var a = {name:'jack'}

// 将 a 的引用赋值给 b
var b = a

// 由于 a,b 指向同一个引用,因此修改 b 对象相当于修改 a 对象
b.name = 'chen'

console.log(a.name, b.name) // chen chen

这时候你会想到copy对象,解构赋值的确是复制对象的方法之一, 但你要现在不影响原本对象的情况下复制:

var a = {name:'jack'}
var b = {...a}

b.name = 'chen'
console.log(a.name, b.name) // jack chen

这个同理能用于数组上,所以现在的问题是你对引用类型不够熟悉。这种也属于一种基础知识,需要自己再额外去看资料补充把。

newItem 是对象,是个复杂类型,是引用的。在上面修改,原数组这个 newItem 的引用又不会变,自然会受影响。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题