React组件化开发(6)- setState&性能优化(4)
只要调用了this.setState(),就会调用render()更新DOM。所以如果要修改数据必须通过setState,否则可能数据发生了改变,页面内容并没有改变。
9.1setState是异步的
setState设计为异步,可以显著的提升性能;
- 如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;
- 最好的办法应该是获取到多个更新,之后进行批量更新
如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步;
- state和props不能保持一致性,会在开发中产生很多的问题;
如何获取异步后的结果
方式一:setState的回调
方式二:在componentDidUpdate函数里
因为执行componentDidUpdate后值必然是更新后的值
9.2setState不一定是异步的
什么样的情况下SetState是同步的?
1.在setTimeout里
Click() {
setTimeout(() => {
this.setState({
message: "你好!"
})
console.log(this.state.message);
}, 0)
}
2.在原生DOM事件里
componentDidMount() {
const btnClick = document.getElementById("btn");
btnClick.addEventListener("click", () => {
this.setState({
message: "哈哈哈"
})
console.log(this.state.message);
})
}
其实分两种情况:
- 在组件生命周期函数和合成事件(比如onClick就是合成事件)里,setState是异步的
- 在setTimeout和原生DOM事件中,setState是同步的
9.3setState数据合并
如果this.state里有多个数据,此时setState修改了其中一个数据,是不会影响其他数据的。
源码中其实是有对 原对象 和 新对象进行合并的:使用Object.assign()
9.4setState本身的合并
上面的代码按道理来说应该是+1+1+1 最后结果是3,但是实际上却只加了1。
如果要知道为什么只有看源码了,但其实,setState第一个参数可以是对象或者函数,如果是函数就会是这种累加的效果
第一个参数是函数(里面传三个参数):
perState是上一次的值。
9.5state的不可变性
import React, { Component, PureComponent } from 'react'
export default class App extends PureComponent {
constructor(props) {
super(props);
this.state = {
friendList: [
{ name: "curry", age: 33 },
{ name: "kebi", age: 43 },
{ name: "James", age: 37 }
]
}
}
render() {
return (
<div>
<h2>好友列表</h2>
<ul>
{
this.state.friendList.map((item, index) => {
return (
<li key={item.name}>
姓名:{item.name}
年龄:{item.age}
<button onClick={e => this.addAge(index)}>age+1</button>
</li>
)
})
}
</ul>
<button onClick={e => this.AddFriend()}>添加数据</button>
</div>
)
}
AddFriend() {
// 不推荐
// this.state.friendList.push({ name: "雷霆嘎巴", age: 20 })
// this.setState({
// friendList: this.state.friendList
// })
// 推荐
const newFriend = [...this.state.friendList, { name: "雷霆嘎巴", age: 20 }];
this.setState({
friendList: newFriend
})
// 为什么说state的不可变性,因为state里面的数据,没一个数据需要开辟一块空间,如果是引用数据类型,你在原来的数据里面添加东西
// 只是添加了数据,原来的空间并没有改变,这时候进行浅比较时是相同的,
}
addAge(index) {
const newAge = [...this.state.friendList];
newAge[index].age += 1;
this.setState({
friendList: newAge
})
}
}
10.性能优化
React更新机制
jsx -> 虚拟DOM -> 真实DOM
10.1React的更新流程
props和state在发生改变时,会调用render方法,创建一棵新的虚拟DOM树
React需要基于这两棵树之间的差异来判断如何有效的进行更新UI
- 如果一棵树参考另外一棵树进行完全比较更新,那么即使是最先进的算法,该算法的复杂程度为 O(n^3 ),其中 n 是树中元素的数量;
- 如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围;
- 这个开销太过昂贵了,React的更新性能会变得非常低效
于是,React对这个算法进行了优化,将其优化成了O(n),如何优化的呢?
- 同层节点之间相互比较,不会垮节点比较;
- 不同类型的节点,产生不同的树结构;
- 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定;
情况一:对比不同类型的元素
当节点为不同的元素,React会拆卸原有的树,并且建立起新的树:
- 当一个元素从 变成
- 当更新style属性时,react仅更新有所改变的属性,比如:通过比对这两个元素,React 知道只需要修改 DOM 元素上的 color 样式,无需修改 fontWeight。
如果时同类型的组件元素
- 组件会保持不变,React会更新该组件的props,并且调用componentWillReceiveProps() 和 componentWillUpdate() 方法。
- 下一步,调用 render() 方法,diff 算法将在之前的结果以及新的结果中进行递归
情况三:对子节点进行递归
- 在默认条件下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个
mutation。
- 如果在最后一条插入数据的情况:前面两条是相同的所以不会产生mutation,最后一个比较产生mutation,将其插入到新的DOM树中即可;
- 如果前面插入一条数据的情况:React会对每一个子元素产生一个mutation,而不是保持 <li>星际穿越</li>和<li>盗梦空间</li>的不变;
- 这种低效的比较方式会带来一定的性能问题(使用keys来优化这个问题);
10.2keys的优化
我们在前面遍历列表时,总是会提示一个警告,让我们加入一个key属性
当子元素(这里的li)拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素:
key的注意事项:
- key应该是唯一的;
- key不要使用随机数(随机数在下一次render时,会重新生成一个数字);
- 使用index作为key,对性能是没有优化的;
10.3render函数被调用
之前有个嵌套案例:
- 在App中,增加了一个计数器的代码
- 当点击+1的时候,就会调用App的render函数;
- 而当调用render函数后,所有子组件的render函数都会调用
那么,我们可以思考一下,在以后的开发中,我们只要是修改了App中的数据,所有的组件都需要重新render,进行diff算法,性能必然是很低的:
- 事实上,很多的组件没有必须要重新render;
- 它们调用render应该有一个前提,就是依赖的数据(state、props)发生改变时,再调用自己的render方法;
如何控制render方法是否被调用呢?
- 通过shouldComponentUpdate方法即可
10.4shouldComponentUpdate
React给我们提供了一个生命周期方法 shouldComponentUpdate(很多时候,我们简称为SCU),这个方法接受参数,并且需要有返回值:
该方法有两个参数:
- 参数一:nextProps 修改之后,最新的props属性
- 参数二:nextState修改之后,最新的state属性
该方法的返回值时一个boolean类型
- 返回值为true,那么就需要调用render方法
- 返回值为false,那么就不需要调用render方法
- 默认值时true,也就是state发生改变就会调用render方法
例如下面我们修改了currentText值并不会重新调用render方法
import React, { Component } from 'react'
export default class App extends Component {
constructor() {
super();
this.state = {
counter: 0,
currentText: "Hello !"
}
}
render() {
console.log("App调用了render");
return (
<div>
<h2>当前计数:{this.state.counter}</h2>
<button onClick={e => this.increnment()}>+1</button>
<button onClick={e => this.btnClick()}>点击更改文件</button>
</div>
)
}
shouldComponentUpdate(nextProps, nextState) {
// 这里的this.state.counter是之前的值
if (this.state.counter != nextState.counter) {
return true;
}
return false;
}
increnment() {
this.setState({
counter: this.state.counter + 1
})
}
btnClick() {
this.setState({
currentText: "你好锕"
})
}
}
10.5 PureComponent
上面我们只是控制了一个组件的render方法,如果每个组件都要去手动控制shouldComponentUpdate,那么会给我们开发者增加非常多的工作量。
我们来设想一下shouldComponentUpdate中的各种判断的目的是什么?
- props或者state中的数据是否发生了改变,来决定shouldComponentUpdate返回true或者false
事实上React已经考虑到了这一点,所以React已经默认帮我们实现好了,如何实现呢?
- 将class继承自PureComponent。
- 函数不能用PureComponent,使用memo()
import React, { Component, PureComponent } from 'react'
class Header extends PureComponent {
render() {
console.log("Header调用了render方法");
return (
<div>
Header组件
</div>
)
}
}
function ProductList() {
return (
<ul>
<li>小程序</li>
<li>H5</li>
<li>企业官网</li>
</ul>
)
}
class Main extends PureComponent {
render() {
console.log("Main调用了render方法");
return (
<div>
<h2></h2>
<ProductList />
</div>
)
}
}
class Footer extends PureComponent {
render() {
console.log("Footer调用了render方法");
return (
<div>我是底部</div>
)
}
}
export default class App extends Component {
constructor() {
super();
this.state = {
counter: 0
}
}
render() {
return (
<div>
<h2>当前计数:{this.state.counter}</h2>
<button onClick={e => this.btnClick()}>+1</button>
<Header />
<Main />
<Footer />
</div>
)
}
btnClick() {
this.setState({
counter: this.state.counter + 1
})
}
}
10.6 memo()
需要使用一个高阶函数组件memo:
Footer组件并没有被memo包裹,所以发生点击后,Footer会重新调用render方法
import React, { Component, memo, PureComponent } from 'react'
const MemoHeader = memo(
function Header() {
console.log("Header调用了render方法");
return (
<div>
Header组件
</div>
)
}
)
function ProductList() {
return (
<ul>
<li>小程序</li>
<li>H5</li>
<li>企业官网</li>
</ul>
)
}
const MemoMain = memo(
function Main() {
console.log("Main调用了render方法");
return (
<div>
<h2></h2>
<ProductList />
</div>
)
}
)
function Footer() {
console.log("Footer调用了render方法");
return (
<div>我是底部</div>
)
}
export default class App extends Component {
constructor() {
super();
this.state = {
counter: 0
}
}
render() {
return (
<div>
<h2>当前计数:{this.state.counter}</h2>
<button onClick={e => this.btnClick()}>+1</button>
<MemoHeader />
<MemoMain />
<Footer />
</div>
)
}
btnClick() {
this.setState({
counter: this.state.counter + 1
})
}
}
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。
评论已关闭