React类组件和函数组件修改自身状态的设计机制
class组件
export default class App extends Component {
state = {
list: {
name:'zhangsan'
}
};
render() {
return (
<div>
....
</div>
);
}
}
如上代码,如果想修改class组件的list状态值:
this.setState({list:{name:'lisi})
// 修改之后的list状态值为
{
name:'lisi
}
如果,你不想修改name字段的值,只是想新加一个属性值,则直接:
this.setState({ list:{age:18}})
// 修改之后的list状态值为
{
name:'lisi,
age:18
}
很多人这时候就纳闷了,为何修改了list为age,可name属性还存在。
结论:因为React底层设计setState用于修改类组件的自身状态时,规定新数据会与原来的数据进行合并操作,而非替换
函数组件
- 函数组件通过useState Hook来声明自身状态及,修改状态的方法函数,如下:
import React, { useState, useEffect, useCallback, useMemo } from "react";
import Header from "./components/Header";
import List from "./components/List";
import Footer from "./components/Footer";
import "./App.css";
import { myContext } from "./context";
export default function App() {
let arr = [
{
id: 1,
checked: true,
title: "打球",
},
{
id: 2,
checked: false,
title: "看美女",
},
{
id: 3,
checked: true,
title: "唱歌",
},
]
const [data, setData] = useState(arr);
// 将data作为value值传入context.Provider
const contextValue = { data, setData };
return (
<myContext.Provider value={ contextValue }>
<div className="todo-container">
<div className="todo-wrap">
<Header ></Header>
<List></List>
<Footer></Footer>
</div>
</div>
</myContext.Provider>
);
}
可能有小伙伴纳闷,这里myContext.Provider
是什么,这里解释一下,myContext
是通过React.createContext()
创建的一个Context上下文,在这个组件中通过myContext.Provider
标签包裹,value属性传递值,下面被包裹的所有子组件都能获取到value传递下去的值,并且下面的子组件也都会随着value内的值的改变而触发重新渲染。
myContext.Provider
内有个输入框添加任务的组件:
import React, {
useState,
useEffect,
useCallback,
useMemo,
useContext,
} from "react";
import { nanoid } from "nanoid";
import { myContext } from "../../context";
import "./index.css";
export default function Header(props) {
const { } = props
const [inputV,setInputV] = useState('')
//获取祖先组件的Context
const { data,setData } = useContext(myContext);
console.log("Header data :>> ", data);
//输入框触发修改事件
let changeMethod = useCallback((evt) => {
console.log("Header changeMethod :>> ", evt.target.value);
setInputV(evt.target.value)
});
//输入框触发键盘按键抬起事件 =》新增数据
let keyUpMethod = useCallback((evt) => {
console.log("Header keyUpMethod :>> ", evt.target.value);
if (evt.keyCode === 13) {
let tempData = data; // 注意这里...................
tempData.unshift({
id: nanoid(),
checked: false,
title: evt.target.value,
});
console.log(`tempData`, tempData);
setData(tempData);
setInputV('')
console.log(data);
}
});
return (
<div className="todo-header">
<input
type="text"
placeholder="请输入你的任务名称,按回车键确认"
value={ inputV }
onChange={changeMethod}
onKeyUp={keyUpMethod}
/>
</div>
);
}
以上代码块内有个keyUpMethod事件
,当输入框键盘抬起的时候会触发,并且当抬起的按键是13=>Enter键位的时候,会先获取到通过useContext Hook获取到的祖先组件传递的data值和修改值的方法setData。接下来就是先声明一个临时变量获取之前的值,然后往数组前面追加一条新加入的数据,然后调用setData修改数据。从而使下面的后代组件重新渲染。
但是,事与愿违,注意上面打标记的一行let tempData = data;
。效果并不是我们预期的那样,数据添加,并且触发组件重新渲染。而是下面这样:
我们通过react调试工具就可以直观的看到,data数组已经被我添加到了6条数据,而列表就是不触发重新渲染。注意,这里就是本文的重点,为什么会这样呢???不科学…
不卖关子,结论:React官方在设计Hook时候,规定使用useState创建的数据,修改时,不像React类组件中那天,去合并原来的数据,而是直接完全替换原数据。
既然知道这样原理了。那我们离真相已经不远了。考虑一下let tempData = data;
是什么?如果data是引用类型数据,那么我们这么写其实只是做了一个浅拷贝的操作。
这里再废话一下***浅拷贝的原理:***
也就是说,我们这句代码的意思就是tempData获取到了data数据的引用地址而已。并没有完全生成一块新的堆内存去存放之前的数据。所以就算你是修改了数据,也同样只是修改了原来的堆内存中存放的数据。React不认为这需要触发页面重新渲染。
改成深拷贝就能解决这个坑了!!!
let tempData = [...data]; // 利用...和数组的解构赋值可以深拷贝数组,对应的对象深拷贝是 {...data}
// 这里经过和小伙伴的一翻讨论,发现...扩展符仅可以深拷贝一维数组或者是一层的对象解构,
// 所以遇到多层结构时,大家可以使用 JSON.parse(JSON.stringify('引用类型变量' )) 进行数据的深拷贝
兄弟姐妹们,点波关注吧,一起分享有趣的技术!
掘金: https://juejin.cn/user/3034307824456296/posts 全部原创好文
CSDN: https://blog.csdn.net/qq_42753705?type=lately 全部原创好文
segmentfault 思否: https://segmentfault.com/u/jasonma1995/articles 全部原创好文