文章目录
为什么要是用 setState
当我们希望通过点击一个按钮从而改变一个文本时,就需要使用到 setState方法,比如:
import React, { Component } from 'react'
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
message: "Hello World"
}
}
render() {
return (
<div>
<h2>{this.state.message}</h2>
<button onClick={e => this.changeText()}>改变文本</button>
</div>
)
}
changeText() {
this.setState({
message: "文本已经被改变"
})
}
}
当我们调用setState时,会重新执行render函数,根据最新的State来创建ReactElement对象;然后再根据最新的ReactElement对象,对DOM进行修改
另外,setState方法是从Component中继承过来的
setState 异步更新
changeText() {
this.setState({
message: "文本已经被改变"
})
console.log(this.state.message); // Hello World
}
上面代码中再使用 setState 方法后,再打印 message,发现 message 没有被改变,由此可以看到 setState 是异步操作,在执行完setState之后不能够立刻拿到最新的state的结果
为什么要异步更新
- 设计成异步,可以显著提升性能,如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的,所以最好的办法是获取到多个更新,然后进行批量的更新
- 如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步,这样做会引起很多问题
如何获取更新后的值
1、setState接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行
changeText() {
this.setState({
message: "文本已经被改变"
}, () => {
console.log(this.state.message); // 文本已经被改变
});
}
2、通过生命周期函数 componentDidUpdate
componentDidUpdate(prevProps, provState, snapshot) {
console.log(this.state.message);
}
同步 or 异步
setState 方法到底是同步还是异步分两种情况
-
在组件生命周期或React合成事件中,setState是异步
-
在setTimeout或者原生dom事件中,setState是同步
// setTimeout
changeText() {
setTimeout(() => {
this.setState({
message: "文本已经被改变"
});
console.log(this.state.message); // 文本已经被改变
}, 0);
}
// 原生DOM事件
componentDidMount() {
const btnEl = document.getElementById("btn");
btnEl.addEventListener('click', () => {
this.setState({
message: "文本已经被改变"
});
console.log(this.state.message); // 文本已经被改变
})
}
setState的合并
数据的合并
this.state = {
name: '小冯',
age: 19
}
// 通过setState去修改age,是不会对name产生影响的
this.setState({
age: 18
})
多个setState合并
this.state = {
counter: 0
}
increment() {
this.setState({
counter: this.state.counter + 1
});
this.setState({
counter: this.state.counter + 1
});
this.setState({
counter: this.state.counter + 1
});
}
setState 方法执行了3次,但是 counter 的值还是为1,这就是多个state进行合并
如果想要变为3,如何去做:传入一个函数
increment() {
this.setState((state, props) => {
return {
counter: state.counter + 1
}
})
this.setState((state, props) => {
return {
counter: state.counter + 1
}
})
this.setState((state, props) => {
return {
counter: state.counter + 1
}
})
}
setState性能优化
react 更新机制
react 渲染流程
react 更新流程
React在props或state发生改变时,会调用React的render方法,会创建一颗不同的树。
React需要基于这两颗不同的树之间的差别来判断如何有效的更新UI:
- 如果一棵树参考另外一棵树进行完全比较更新,那么即使是最先进的算法,该算法的复杂程度为 O(n 3 ),其中 n 是树中元素的数量;
- https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf;
- 如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围;
- 这个开销太过昂贵了,React的更新性能会变得非常低效;
于是,React对这个算法进行了优化,将其优化成了O(n),如何优化的呢?
- 同层节点之间相互比较,不会跨层级进行节点的比较。
- 不同类型的节点,产生不同的树结构;
- 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定;
diff算法的作用
计算出虚拟 dom中真正变化的部分,并只针对该部分进行原生DOM操作,而非重新渲染整个页面
传统 diff算法
通过循环递归节点进行依次比较,复杂度为O(n^3) ,n为是树中节点(元素)的数量
如果在 React 中使用了该算法,那么展示 1000 个元素就需要进行上亿次比较,太浪费性能
react diff算法
- 同层节点之间相互比较,不会跨节点比较;
- 不同类型的节点,产生不同的树结构;
- 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定;
对比不同类型的元素
当节点为不同的元素,React会拆卸原有的树,并且建立起新的树:
- 当一个元素从
<a>
变成<img>
,从<Article>
变成<Comment>
,或从<Button>
变成<div>
都会触发一个完整的重建流程; - 当卸载一棵树时,对应的DOM节点也会被销毁,组件实例将执行
componentWillUnmount()
方法; - 当建立一棵新的树时,对应的 DOM 节点会被创建以及插入到 DOM 中,组件实例将执行
componentWillMount()
方法,紧接着componentDidMount()
方法;
比如下面的代码更改:
- React 会销毁
Counter
组件并且重新装载一个新的组件,而不会对Counter进行复用;
<div>
<Counter />
</div>
<span>
<Counter />
</span>
对比同一类型的元素
当比对两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性。
比如下面的代码更改:
- 通过比对这两个元素,React 知道只需要修改 DOM 元素上的
className
属性;
<div className="before" title="stuff" />
<div className="after" title="stuff" />
比如下面的代码更改:
- 当更新
style
属性时,React 仅更新有所更变的属性。 - 通过比对这两个元素,React 知道只需要修改 DOM 元素上的
color
样式,无需修改fontWeight
。
<div style={{color: 'red', fontWeight: 'bold'}} />
<div style={{color: 'green', fontWeight: 'bold'}} />
如果是同类型的组件元素:
- 组件会保持不变,React会更新该组件的props,并且调用
componentWillReceiveProps()
和componentWillUpdate()
方法; - 下一步,调用
render()
方法,diff 算法将在之前的结果以及新的结果中进行递归;
对子节点进行递归
在默认条件下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个 mutation。
我们来看一下在最后插入一条数据的情况:
- 前面两个比较是完全相同的,所以不会产生mutation;
- 最后一个比较,产生一个mutation,将其插入到新的DOM树中即可;
<ul>
<li>first</li>
<li>second</li>
</ul>
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
但是如果我们是在中间插入一条数据:
- React会对每一个子元素产生一个mutation,而不是保持
<li>星际穿越</li>
和<li>盗梦空间</li>
的不变; - 这种低效的比较方式会带来一定的性能问题;
<ul>
<li>星际穿越</li>
<li>盗梦空间</li>
</ul>
<ul>
<li>大话西游</li>
<li>星际穿越</li>
<li>盗梦空间</li>
</ul>
keys的优化
在遍历数据进行渲染时,需要加 key 值,没有 key 值得话会出现下面的错误
分析一下下面这个例子
import React, { Component } from 'react'
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
movies: ["星际穿越", "盗梦空间"]
}
}
render() {
return (
<div>
<h2>电影列表</h2>
<ul>
{
this.state.movies.map((item, index) => {
return <li>{item}</li>
})
}
</ul>
<button onClick={e => this.insertMovie()}>插入数据</button>
</div>
)
}
insertMovie() {
}
}
在最后位置插入数据
这种情况,有无key意义并不大
insertMovie() {
const newMovies = [...this.state.movies, "大话西游"];
this.setState({
movies: newMovies
})
}
在前面插入数据
- 这种做法,在没有key的情况下,所有的li都需要进行修改;
insertMovie() {
const newMovies = ["大话西游", ...this.state.movies];
this.setState({
movies: newMovies
})
}
当子元素(这里的li)拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素:
- 在下面这种场景下,key为111和222的元素仅仅进行位移,不需要进行任何的修改;
- 将key为333的元素插入到最前面的位置即可;
<ul>
<li key="111">星际穿越</li>
<li key="222">盗梦空间</li>
</ul>
<ul>
<li key="333">Connecticut</li>
<li key="111">星际穿越</li>
<li key="222">盗梦空间</li>
</ul>
key的注意事项:
- key应该是唯一的;
- key不要使用随机数(随机数在下一次render时,会重新生成一个数字);
- 使用index作为key,对性能是没有优化的;