关于setState性能优化
js
当你调用setState时,即使更改的数据与原来的数据是一样的,它仍然会去调用render函数!
// 这种优化我们通常称之为SCU
所以有个shouldComponentUpdate()函数给我们做优化,若数据相同,return false,即可不触发render()生命周期
在类组件中,React也想到了这一层,所以如果我们不想繁琐的写SCU去优化的话,那么我们可以将类不是继承React的Component,而是React的PureComponent
import { PureComponent } from 'react'
export class demo extends PureComponent {}
export default demo
函数组件中也有类似PureComponent的,
import { memo } from "react"
const Test = memo(function(props) {
console.log('Test render')
return <div>Test</div>
})
export default Profile
所以这里也就牵扯到为什么修改state数据时,要保持它的不可变性,更改数据,保持replace的原则,而不是modify;
例子:
// 这样写,如果继承的是React的Component,你也能成功更新渲染,但是如果继承的是React的PureComponent,那么就不会更新成功,因为新旧state是一模一样的,那就不会触发render函数
this.state = { arr:[1,2,3] }
this.state.arr.push(4)
this.setState({ arr: this.state.arr })
js
// pureComponent底层是类似使用这样的方法进行优化的:
shouldComponentUpdate(nextProps, nextState) {
return (
!shallowEqual(nextProps, this.props) || !shallowEqual(nextState, this.state)
)
}
其进行的是浅层比较,如果props或state有变化的话,才return true
setState更新数据的原理
js
this.state = {msg: 'msg', num: 0}
this.setState({msg: 'newMsg'})
但是num数据还会在,其原理层面上,
react在更新数据时使用的是Object.assign(this.state, newState), // 新数据会覆盖旧数据中相同属性的值
并非是this.state = newState 这种直接覆盖值的操作。
在Object.assign()处理完数据后,再调用render()函数更新渲染
为什么setSate是异步的?(但是需要分情况,不一定是异步,详见下面主题:setState一定是异步吗?)
在GitHub上,react的核心成员(redux的作者gaearon)有作过这样的一个回答(issue)
js
简单的总结就是:
setState设计为异步,可以`显著的提升性能`;
1.如果每次调用setState都进行一次更新,那么就意味着render函数会被频繁调用,界面重新渲染,这样的的效率非常低;
2.最好的办法应该是获取到多个更新,之后进行批量更新;
`如果同步更新了state,但是还没有执行render函数,那么state和props就不能保持同步了`
1.state和props不能保持一致性,就会在开发中产生很多的问题;
2.所以最好是setState后,尽快执行render函数;(虽然有一种setState后,立马手动this.render()去执行,
但是这样就会造成很多性能上的问题,且setState后,可能还有一些逻辑代码需要写)
例子:在App组件中setState了,如果是同步更新的话,那么setState后,
this.state就立马更新了,但如果在后面的代码中出错了,没有执行render函数,
那么App的子组件Son,Son组件中props仍然是之前的数据(因为没有调用render函数),
导致数据不同步,后续就会发生很多的问题。
setState一定是异步吗?
这就需要分两种情况了
React18之前
js
// 在组件生命周期或React合成事件中,setState是异步;
// 在setTimeout或原生dom事件或promise.then中,setState是同步;
changeName() {
setTimeout(() => {
this.setState({name: 'newName'})
console.log(this.state.name); // newName
}, 0);
}
componentDidMount() {
const btn = document.getElementById('btn');
btn.addEventListener('click', () => {
this.setState({
name: 'newName'
})
console.log(this.state.name); // newName
})
}
React18之后
setState默认都是异步(批处理)
的了,即使在setTimeout或原生dom事件或promise.then中。
查看更多关于React18中,更少使用render函数的自动批处理
js
// 但是使其同步仍然是有办法的,引入react-dom中{ flushSync }
import { flushSync } from 'react-dom'
changeName() {
flushSync(() => {
// 这里的3个setState仍然会是批处理
this.setState({name: 'newName'})
this.setState({name: 'newName'})
this.setState({name: 'newName'})
console.log(this.state.name) //oldName
})
console.log(this.state.name) // newName
}
diff算法的优化
首先如果一棵树参考另外一棵树进行完全比较更新,第一个虚拟 DOM 树有 m 个节点,第二个虚拟 DOM 树有 n 个节点,那么该算法的复杂程度为O(m * n),如果使用该算法,那么展示的元素过多时,需要执行的计算量将非常大,该更新性能会非常低效;
于是React对算法进行了优化,将其优化成O(n)
js
在一般情况下,React 的 diff 算法的时间复杂度可以近似地看作是 O(n),其中 n 是虚拟 DOM 树中的节点数量。
但是在非常深的树结构或存在大量兄弟节点的情况下,算法的复杂度可能会增加到 O(n^3)。为了应对这种情况,
React 引入了最大深度限制和时间片(Time Slicing)等机制,以确保算法的执行时间不会超过某个阈值,
从而保持用户界面的响应性。
具体的复杂度可能受到树结构和优化策略的影响,在一般情况下能够提供高效的更新操作。
那是如何优化的呢
- 同层节点之间相互比较,不会跨节点比较;
- 不同类型的节点,产生不同的树结构;(比如:一个div节点,变成了p节点,那么其所有子节点这个树,都会重新生成)
- 还有开发中,我们也会用key来指定哪些节点在不同的渲染下保持稳定;(比如:同层级中(a、b、c),在中间多了一个新节点(a、x、b、c),它并不会把a后面的所有节点都替换掉,根据key,如果能复用,就继续复用(仅进行位移就好),只会在索引为1中插入新节点)
useSelector的性能优化(shallowEqual)
useSelector是根据整个state去判断当前组件是否需要重新渲染,比如:修改父组件中redux的某个数据,(子组件没有用到,但仍触发子组件的重新渲染)
为了避免没必要的重新render,我们只需要在每次用到useSelector,就给该hook中的第二个参数写上 shallowEqual 即可
js
import { shallowEqual, useDispatch, useSelector } from "react-redux";
// 子组件
const Home = memo((props) => {
// 给useSelector加上浅层比较(shallowEqual),就不会频繁触发重新渲染
const { msg } = useSelector(
(state) => ({
msg: state.counter.msg,
}),
shallowEqual
);
const dispatch = useDispatch();
function handleChangeMsg() {
dispatch(changeMsgAction(Math.random().toString()));
}
console.log("Home render");
return (
<div>
<h2>Home</h2>
<div>msg:{msg}</div>
<button onClick={(e) => handleChangeMsg()}>改变msg</button>
</div>
);
});
// 父组件
const App = memo((props) => {
const { counter } = useSelector(
(state) => ({
counter: state.counter.counter,
}),
shallowEqual
);
const dispatch = useDispatch();
function handleSetCounter(num, isAdd = true) {
if (isAdd) {
dispatch(addCounterAction(num));
} else {
dispatch(subCounterAction(num));
}
}
console.log("App render");
return (
<div>
App
<div>counter: {counter}</div>
<div>
<button onClick={(e) => handleSetCounter(2)}>+2</button>
<button onClick={(e) => handleSetCounter(3, false)}>-3</button>
</div>
<Home />
</div>
);
});