第五节:setState详解、react更新机制、render函数优化、数据不可变、ref获取DOM和组件
一. setState详解
1. 使用背景
开发中我们并不能直接通过修改state的值来让界面发生更新:
(1).因为我们修改了state之后,希望React根据最新的State来重新渲染界面,但是这种方式的修改React并不知道数据发生了变化;
(2).React并没有实现类似于Vue2中的Object.defineProperty或者Vue3中的Proxy的方式来监听数据的变化;
(3).我们必须通过setState来告知React数据已经发生了变化;
补充:
在组件中并没有实现setState的方法,为什么可以调用呢? 原因:setState方法是从Component中继承过来的。 详见图1
2. 三种写法
(1). 基本写法
this.setState({ msg: "hello ypf1" }); 内部执行是一个合并操作, object.assign()
(2). 传入一个回调函数
this.setState((state, props) => { return {msg: "hello ypf2",}; });
用途:
A.在回调函数中对新的state逻辑进行编写
PS:这里的state拿到的是最新的state,即上面如果执行了setState一次,这里的state是更新后的state
B.回调函数中可以将之前的state和props传递进来,进行使用
(3). setState是一个异步调用
如果希望在数据更新之后(数据合并), 获取到对应的结果执行一些逻辑代码,那么可以在setState中传入第二个参数: callback
this.setState({ msg: "hello ypf3" }, () => {});
查看代码
export class APP extends Component {
constructor() {
super();
this.state = { msg: "hello ypf", counter: 100 };
}
//setState的三种写法
// 用法1--基本使用
changeText1() {
// 内部执行是一个合并操作, object.assign()
this.setState({ msg: "hello ypf1" });
}
//用法2--传入一个回调函数
/*
用途:
1.在回调函数中对新的state逻辑进行编写
PS:这里的state拿到的是最新的state,即上面如果执行了setState一次,这里的state是更细后的state
2.回调函数中可以将之前的state和props传递进来,进行使用
*/
changeText2() {
this.setState((state, props) => {
// 1. 获取之前的state和props
console.log(state, props);
// 2. 在return中设置state中的值
return {
msg: "hello ypf2",
};
});
}
//用法3--setState是一个异步调用
/*
如果希望在数据更新之后(数据合并), 获取到对应的结果执行一些逻辑代码
那么可以在setState中传入第二个参数: callback
*/
changeText3() {
this.setState({ msg: "hello ypf3" }, () => {
console.log("----回调中获取:", this.state.msg); //hello ypf3
});
console.log("----执行后获取:", this.state.msg); //hello ypf
}
render() {
const { msg, counter } = this.state;
return (
<div>
<h3>msg:{msg}</h3>
<h3>counter:{counter}</h3>
<p>
<button onClick={() => this.changeText1()}>修改文本1</button>
</p>
<p>
<button onClick={() => this.changeText2()}>修改文本2</button>
</p>
<p>
<button onClick={() => this.changeText3()}>修改文本3</button>
</p>
</div>
);
}
}
3. setState异步更新
(1). 证明setState是异步的?
详见案例1即可,默认值是0,连续执行三次setState,最终结果是1,间接的证明了setState是异步的,三个log中输出的都是0
constructor() {
super();
this.state = { counter: 0 };
}
/*
案例1
默认值是0,连续执行三次setState,最终结果是1,间接的证明了setState是异步的
三个log中输出的都是0
*/
Add1() {
this.setState({ counter: this.state.counter + 1 });
console.log("-----------", this.state.counter); //0
this.setState({ counter: this.state.counter + 1 });
console.log("-----------", this.state.counter); //0
this.setState({ counter: this.state.counter + 1 });
console.log("-----------", this.state.counter); //0
}
(2). 为什么setState设计为异步呢?
(React核心成员(Redux的作者)的回复:https://github.com/facebook/react/issues/11527#issuecomment-360199710)
A. setState设计为异步,可以显著的提升性能;
如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;最好的办法应该是获取到多个更新,之后进行批量更新
B.如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步(render中的显示和实际不一致)
state和props不能保持一致性,会在开发中产生很多的问题
4. 如何获取异步结果?
(1). setState的回调
setState接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行;(或者是直接传入一个回调函数)
格式如下:setState(partialState, callback) this.setState({ msg: "hello ypf3" }, () => {});
或: this.setState((state, props)=>{});
/*
案例2
默认值是0,连续执行三次setState,最终结果是3,证明了回调中拿到是更新后state
三个log中输出的一次是:0、1、2
*/
Add2() {
this.setState((state, props) => {
console.log("-----------", state.counter); //0
return { counter: state.counter + 1 };
});
this.setState((state, props) => {
console.log("-----------", state.counter); //1
return { counter: state.counter + 1 };
});
this.setState((state, props) => {
console.log("-----------", state.counter); //2
return { counter: state.counter + 1 };
});
}
(2).在生命周期函数componentDidUpdate
componentDidUpdate() {
console.log("------------componentDidUpdate:", this.state.counter);
}
5. setState不同版本的异步情况
(1). 在react18之前
A. 在组件生命周期或React合成事件中,setState是异步;
B. 在setTimeout或者原生dom事件(eg: addEventListener)中,setState是同步
(2). 在react18之后
默认所有的操作都被放到了批处理中(都是异步处理),如果希望代码可以同步会拿到,则需要执行特殊的flushSync操作。
二. react更新机制
1. react更新流程
(1). React在props或state发生改变时,会调用React的render方法,会创建一颗不同的树。
(2). React需要基于这两颗不同的树之间的差别来判断如何有效的更新UI:
如果一棵树参考另外一棵树进行完全比较更新,那么即使是最先进的算法,该算法的复杂程度为 O(n³),其中 n 是树中元素的数量;
https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf;
如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围;
这个开销太过昂贵了,React的更新性能会变得非常低效;
(3). 于是,React对这个算法进行了优化,将其优化成了O(n),如何优化的呢?
同层节点之间相互比较,不会垮节点比较;
不同类型的节点,产生不同的树结构;
开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定;
2. keys的优化
我们在前面遍历列表时,总是会提示一个警告,让我们加入一个key属性:
(1). key的几种情况
◼ 方式一:在最后位置插入数据
这种情况,有无key意义并不大
◼ 方式二:在前面插入数据
这种做法,在没有key的情况下,所有的li都需要进行修改;
◼ 方式三: 当子元素(这里的li)拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素:
在下面这种场景下,key为111和222的元素仅仅进行位移,不需要进行任何的修改; 将key为333的元素插入到最前面的位置即可;
(2). key的注意事项
key应该是唯一的;
key不要使用随机数(随机数在下一次render时,会重新生成一个数字);
使用index作为key,对性能是没有优化的;
三. render函数优化
1. 背景
App组件中引用Son1 和 Son2 组件,App中修改一下counter,发现Son1和Son2也重新渲染了,即所有的组件都需要重新render,进行diff算法,性能必然是很低的,也是没必要的,很多的组件没有必须要重新render。
如:V1版本的案例,在App中修改counter, 发现 App、Son1、Son2 都重新渲染了。
2. 解决方案1--shouldComponentUpdate
(1). 说明
A. React给我们提供了一个生命周期方法 shouldComponentUpdate(很多时候,我们简称为SCU),这个方法接受参数,并且需要有返回值,该方法有两个参数:
参数一:nextProps 修改之后,最新的props属性
参数二:nextState 修改之后,最新的state属性
B. 该方法返回值是一个boolean类型:
返回值为true,那么就需要调用render方法;
返回值为false,那么久不需要调用render方法;
默认返回的是true,也就是只要state发生改变,就会调用render方法;
(2). 实操
如:V2版本的案例
App组件中引用Son1 和 Son2 组件, 这两个Son组件中都在shouldComponentUpdate中判断msg是否修改,修改了则返回true,需要渲染,反之返回false,不渲染。 App中Son1绑定msg1,Son2绑定msg2
APP中修改counter,App渲染,Son1和Son2都不渲染
APP中修改msg1,App和Son1渲染,Son2不渲染
核心代码分享:
export class Son1 extends Component {
constructor() {
super();
this.state = { msg: "hello ypf" };
}
/**
* 组件是否更新的生命周期
* @param {*} nextProps 修改之后,最新的props属性
* @param {*} nextState 修改之后,最新的state属性
*/
shouldComponentUpdate(nextProps, nextState) {
// 自己编写业务决定是否更新
if (this.state.msg !== nextProps.msg) {
return true;
}
return false;
}
render() {
console.log("Son1 Render");
return <div>Son1</div>;
}
}
3. 解决方案3--PureComponent
(1). 背景
如果所有的类,我们都需要手动来实现 shouldComponentUpdate,那么会给我们开发者增加非常多的工作量,React已经考虑到了这一点,使用 PureComponent 解决了这个问题
(2). 实操
如:V3版本的案例
App组件中引用Son1 和 Son2 组件,且该三个组件都继承PureComponent,App中Son1绑定msg1,Son2绑定msg2
APP中修改counter,App渲染,Son1和Son2都不渲染
APP中修改msg1,App和Son1渲染,Son2不渲染
核心代码分享:
export class Son1 extends PureComponent {
constructor() {
super();
this.state = { msg: "hello ypf" };
}
render() {
console.log("Son1 Render");
return <div>Son1</div>;
}
}
4. 高阶组件memo
(1). 说明
函数式组件没有PureComponent,则需要使用memo高阶组件,函数式组件我们在props没有改变时,也是不希望其重新渲染其DOM树结构的。
(2). 实操
如:V4版本的案例, Son2组件中使用memo
核心代码分享:
import { memo } from "react";
const Son2 = memo(function (props) {
console.log("Son2 Render");
return <div>Son2</div>;
});
export default Son2;
5. 原理剖析--shallowEqual方法
PureComponent内部使用shallowEqual方法进行浅层比较,详见图1
四. 数据不可变
1. 背景
修改一个引用类型的数据(eg:this.state.books 这个数据),我们能想到两种修改方式
方式1: 【不要用这种!!】
直接修改这个books, 然后调用setState进行赋值
方式2:【推荐】
先把这个books进行浅拷贝一下,然后修改浅拷贝后的数据,最后调用setState进行赋值
上述两种方式,如果类组件继承的是 Component,两种方式都能生效。如果继承的是PureComponent,那么只有方式2生效。
import React, { PureComponent, Component } from "react";
export class App extends PureComponent {
constructor() {
super();
this.state = {
books: [
{ name: "你不知道JS", price: 99, count: 1 },
{ name: "JS高级程序设计", price: 88, count: 1 },
{ name: "React高级设计", price: 78, count: 2 },
{ name: "Vue高级设计", price: 95, count: 3 },
],
friend: { name: "ypf", age: 18 },
msg: "Hello World",
};
}
addNewBook() {
const newBook = { name: "Angular高级设计", price: 88, count: 1 };
//写法1(不推荐!)
//在PureComponent是不能引入重新渲染(re-render),因为 pureComponent认为this.state 和 修改后后nextState是一样的,所以不渲染
// this.state.books.push(newBook);
// this.setState({ books: this.state.books });
//写法2
let tempBooks = [...this.state.books];
tempBooks.push(newBook);
this.setState({ books: tempBooks });
}
addBookCount(index) {
// 写法1(不推荐!):
// this.state.books[index].count++; //不能直接修改
// this.setState({ books: this.state.books });
// 写法2:
let tempBooks = [...this.state.books];
tempBooks[index].count++;
this.setState({ books: tempBooks });
}
render() {
const { books } = this.state;
return (
<div>
<h3>数据列表</h3>
<ul>
{books.map((item, index) => (
<li key={index}>
<span>--name--:{item.name}--</span>
<span>--price--:{item.price}--</span>
<span>--count--:{item.count}--</span>
<button onClick={() => this.addBookCount(index)}>+1</button>
</li>
))}
</ul>
<p>
<button onClick={() => this.addNewBook()}>AddNewBook</button>
</p>
</div>
);
}
}
export default App;
2. 剖析
方式1 在PureComponent中为什么不生效呢?
在PureComponent是不能引入重新渲染(re-render),因为 pureComponent认为this.state 和 修改后nextState是一样的,所以不渲染。
浅拷贝一下,虽然堆的数据还是一致的, 但是栈中却是不能的两个对象了,所以PureComponent认为是不一样的,就能生效,重新渲染。
五. ref获取Dom和组件
1. 获取DOM
(1) 方式一:传入字符串
使用时通过 this.refs.传入的字符串格式获取对应的元素;
(2) 方式二:传入一个对象 【推荐使用】
对象是通过 React.createRef() 方式创建出来的;使用时获取到创建的对象其中有一个current属性就是对应的元素;
(3) 方式三:传入一个函数
该函数会在DOM被挂载时进行回调,这个函数会传入一个 元素对象,我们可以自己保存;
使用时,直接拿到之前保存的元素对象即可
export class App extends PureComponent {
constructor() {
super();
this.state = {};
// 方式2
this.myRef2 = createRef();
// 方式3
this.myRef3 = null;
}
getNativeDOM1() {
// 方式1:在react上绑定一个ref字符串
console.log(this.refs.myRef1);
}
getNativeDOM2() {
// 方式2:createRef()提前创建好ref对象, 将创建出来的对象绑定到元素 【推荐!】
// 需要使用 current 属性获取DOM对象
console.log(this.myRef2.current);
}
getNativeDOM3() {
// 方式3: 传入一个回调函数, 在对应的元素被渲染之后, 回调函数被执行, 并且将元素传入
console.log(this.myRef3);
}
render() {
return (
<div>
<h3 ref="myRef1">hello ypf1</h3>
<h3 ref={this.myRef2}>hello ypf2</h3>
<h3 ref={el => (this.myRef3 = el)}>hello ypf3</h3>
<button onClick={() => this.getNativeDOM1()}>获取DOM-方式1</button>
<button onClick={() => this.getNativeDOM2()}>获取DOM-方式2</button>
<button onClick={() => this.getNativeDOM3()}>获取DOM-方式3</button>
</div>
);
}
}
2. 获取类组件
采用上述方式二的模式,在在类组件上通过ref绑定一个 createRef() 创建的对象,然后通过.current属性就可以获取对应的组件
export class App extends PureComponent {
constructor() {
super();
// 采用方式2的模式获取
this.myHelloRef = createRef();
}
getChildCpn() {
let helloCpn = this.myHelloRef.current;
console.log(helloCpn);
// 调用子组件的方法
helloCpn.myTest();
}
render() {
return (
<div>
App
<HelloWorld ref={this.myHelloRef}></HelloWorld>
<button onClick={() => this.getChildCpn()}>获取类组件</button>
</div>
);
}
}
export class HelloWorld extends PureComponent {
myTest() {
console.log("HelloWorld中的myTest方法");
}
render() {
return <div>HelloWorld</div>;
}
}
3. 获取函数式组件的某个标签
(1). 函数式组件是没有实例的,所以无法通过ref获取他们的实例, 但是可以获取函数组件中的某个标签。
(2). 如何获取?
A. 通过上述方式二的模式,给函数组件绑定要给ref对象
B. 函数式组件要通过forwardRef包裹,接收两个参数:props 和 ref, 然后把这个ref绑定到对应的DOM上即可
export class App extends PureComponent {
constructor() {
super();
// 采用方式2的模式获取
this.myHelloRef = createRef();
}
getChildCpn() {
// 获取HelloWorldFun组件中的某个标签
let helloCpn = this.myHelloRef.current;
console.log(helloCpn);
}
render() {
return (
<div>
App
<HelloWorldFun ref={this.myHelloRef}></HelloWorldFun>
<button onClick={() => this.getChildCpn()}>获取类组件</button>
</div>
);
}
}
const HelloWorldFun = forwardRef(function (props, ref) {
return (
<div>
<h1 ref={ref}>HelloWorldFun中的h1标签</h1>
<p>哈哈哈</p>
</div>
);
});
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。