React(四)setState原理-性能优化-ref
setState详解
实现原理
开发中我们并不能直接修改State来重新渲染界面:
因为修改State之后,希望React根据最新的State来重新渲染界面,但这种方式的修改React并不知道数据发生了变化;
React并没有类似于Vue2中的Object.defineProperty或Vue3中的Proxy的方式来监听数据的变化;
我们必须通过setState来告知React数据已经发生了变化;
该方法的源码实现如下:
三种使用方式
import React, { Component } from 'react'
export class App extends Component {
constructor () {
super ()
this.state={
message:'hello',
count:20
}
}
changeText() {
// 1.setState更多用法
// 1.1基本使用
// this.setState({
// message:'你好'
// })
/*
1.2setState可以传入一个回调函数
好处一:可在回调函数中编写新的state处理逻辑
好处二:当前回调函数会将之前的state和props传递进来
*/
// this.setState((state,props) => {
// console.log(state,props);
// return {
// message:'你好'
// }
// })
/*
1.3setState在React的事件处理中是一个异步调用
如果希望在数据更新之后(数据合并),获取到对应的结果执行一些逻辑代码
那么可以在setState中传入第二个参数:callback
*/
this.setState({
message:"你好呀,小橙子"
},() => {
console.log(this.state.message);//你好呀,小橙子
})
// 会先执行这行代码
console.log("-----------",this.state.message);// hello
}
addCount () {}
render() {
const { message,count } = this.state
return (
<div>
<h2>{message}</h2>
<button onClick={() => this.changeText()}>修改文本</button>
<h2>当前计数:{count}</h2>
<button onClick={() => this.addCount()}>count+1</button>
</div>
)
}
}
export default App
setState异步更新
为什么setState设计为异步呢?
React核心成员(Redux的作者)Dan Abramov也有对应的回复:RFClarification: why is `setState` asynchronous? · Issue #11527 · facebook/react · GitHub
- setState设计为异步,可以显著的提升性能;
- 如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;
- 最好的办法应该是获取到多个更新,之后进行批量更新;
- 如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步;
- state和props不能保持一致性,会在开发中产生很多的问题;
import React, { Component } from 'react'
function Hello (props) {
return <h2>{props.message}</h2>
}
export class App extends Component {
constructor () {
super ()
this.state={
message:'hello',
count:20
}
}
changeText() {
}
addCount () {
// 一直是20+1——也说明setState是异步的
/* this.setState({
count:this.state.count + 1
})
this.setState({
count:this.state.count + 1
})
this.setState({
count:this.state.count + 1
}) */
this.setState((state) => {
console.log(this.state.count);//20
return {
count:state.count + 1
}
})
this.setState((state) => {
console.log(this.state.count);//20
return {
count:state.count + 1
}
})
this.setState((state) => {
console.log(this.state.count);//20
return {
count:state.count + 1
}
})
}
/*
假设setState是同步的,那么点击按钮后,this.state.message已经改变——但是还未执行render函数——那么页面上的数据就还是之前的-即state和props不能保持同步
*/
render() {
// 调用addCount函数,里面有三个setState,但render只会重新渲染一次——批量更新
console.log("render函数被执行了几次");
const { message,count } = this.state
return (
<div>
<h2>{message}</h2>
<button onClick={() => this.changeText()}>修改文本</button>
<h2>当前计数:{count}</h2>
<button onClick={() => this.addCount()}>count+1</button>
<Hello message={message} />
</div>
)
}
}
export default App
如何获取异步修改完的结果?
方式一:setState的回调
setState接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行;
方式二:生命周期函数
注意:在React18之前,如果在setState外边包个setTimeout这种宏任务,它不由React回调,而是浏览器。故setState就变成了同步操作
React18之前:setTimeout中setState操作,是同步操作
React18之后:setTimeout中setState操作,是异步操作
setTimeout ( () => {
this.setState({
message:'你好'
})
console.log(this.state.message);//React18之前:你好;React18之后:hello
},0)
性能优化SCU
当我们修改根组件中的数据,所有组件都需要重新render,进行diff算法,性能极低!——只更新数据改变的即可——通过shouldComponentUpdate方法
//nextProps:修改之后最新的props; nextState:修改之后最新的state
shouldComponentUpdate(nextProps, nextState) {
// 性能优化:自己对前后state进行对比,如果前后state没有变化,就不更新组件
if(this.state.message !== nextState.message || this.state.count !== nextState.count){
return true
}
//根据返回值觉得是否调用render方法
return false
}
但如果所有的类都需要手动设置,那工作量也太大了!
React内部已帮我们实现PureComponent
底层原理实现:
注意:PureComponent只能检测第一层数据的变化,也就是复杂数据类型若地址未发生变化,是检测不到的
import React, { PureComponent } from 'react'
export default class App extends PureComponent {
constructor () {
super()
this.state = {
books:[
{name:"你不知道的JS",price:99,count:2},
{name:"JS高级程序设计",price:78,count:2},
{name:"React高级设计",price:94,count:2},
{name:"LeetCode",price:88,count:2},
]
}
}
/*
PureComponent底层实现原理:
shouldComponentUpdate(nextProps,nextState) {
浅层比较
return (!shallowEqual(nextProps,this.props) || shallowEqual(nextState,this.state))
}
*/
addBook() {
const newBook = {name:"算法",price:99,count:8}
/*
React 依赖于状态的不可变性来确定何时需要更新 UI
直接修改状态对象的属性不会让 React 知道状态已经发生了变化,因此不会重新渲染组件
该做法在PureComponent中是不能引起重新渲染的
this.state.books.push(newBook)
*/
this.setState({
books:[...this.state.books,newBook]
})
}
addCount (index) {
// this.state.books[index]++ //引用对象并未发生改变
const newBooks = [...this.state.books]//引用发生改变
newBooks[index].count++
this.setState({
books:newBooks
})
}
render() {
const {books} = this.state
return (
<div>
<h2>书籍列表</h2>
<ul>
{
books.map((item,index) => {
return (
<li key={item.name}>
<span>{item.name}-{item.price}-{item.count}</span>
<button onClick={() => this.addCount(index)}>+1</button>
</li>
)
})
}
</ul>
<button onClick={() => this.addBook()}>添加新书籍</button>
</div>
)
}
}
针对类组件可使用 PureComponent。那函数组件那???
我们可使用高阶函数memo将组件都包裹一层
import { memo } from "react";
const Profile = memo(function(props) {
console.log("Profile render函数");
return (
<div>
<h2>Profile-{props.message}</h2>
</div>
)
})
export default Profile
ref获取元素或组件实例
传入一个对象
1.类组件:通过 React.createRef() 创建 ref 对象,并绑定到 JSX 元素的 ref 属性
class ClassComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef(); // 创建 ref 对象
}
componentDidMount() {
console.log(this.myRef.current); // 获取 DOM 元素
}
render() {
return <div ref={this.myRef}>类组件中使用 ref 对象</div>;
}
}
2.函数组件:通过 React.useRef() Hook 创建 ref 对象,并绑定到 JSX 元素
function FunctionComponent() {
const myRef = React.useRef(null); // 创建 ref 对象
React.useEffect(() => {
console.log(myRef.current); // 获取 DOM 元素
}, []);
return <div ref={myRef}>函数组件中使用 useRef</div>;
}
使用回调 ref
原理:将回调函数传递给元素的 ref 属性,React 在挂载/卸载时调用该回调,参数为 DOM 元素
1.类组件
class ClassComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = null; // 直接保存 DOM 引用
}
componentDidMount() {
console.log(this.myRef); // 直接访问 DOM 元素
}
render() {
return (
<div ref={(el) => { this.myRef = el; }}> {/* 回调 ref */}
类组件中使用回调 ref
</div>
);
}
}
2.函数组件:
function FunctionComponent() {
const myRef = React.useRef(null); // 持久化保存 DOM 引用
const setRef = (el) => {
myRef.current = el; // 通过回调更新 ref
};
React.useEffect(() => {
console.log(myRef.current); // 获取 DOM 元素
}, []);
return <div ref={setRef}>函数组件中使用回调 ref</div>;
}
获取函数子组件的DOM
import React, { PureComponent, createRef,forwardRef } from 'react'
const HelloWorld = forwardRef(function(props,ref) {
return(
<h1 ref={ref}>Hello World</h1>
)
})
export class App extends PureComponent {
constructor(props) {
super(props);
this.whyRef = createRef();
}
getNativeDOM(){
console.log(this.whyRef.current);
}
render() {
return (
<div>
{/* 不能在函数组件上直接使用 ref属性,因为他们没有实例——通过forwardRef做一个转发*/}
<HelloWorld ref={this.whyRef}/>
<button onClick={() => this.getNativeDOM()}>获取组件实例</button>
</div>
)
}
}
export default App