ReactP8_setState详解_React更新机制_render调用优化
这边是react学习记录,期间加入了大量自己的理解,用于加强印象,若有错误之处还请多多指出
本节原理性的东西是真的多,感觉自己可以梳理出一条主线却发现真的整理起来到处都会发散,然后说一堆废话,这里尽量使用精简的语言和排版文字对应流程化叙述来讲清楚内容。
setState( )
这是一个非常关键的函数,此处主要通过源码来介绍关于setState的执行流程和使用注意点
使用背景
根据之前的经验来看,直接对state进行数值上的操作,只能导致数值的改变而不会引起页面的更新
而开发中希望修改了state之后,React能够根据最新的State来重新渲染界面
只有通过setState来告知React数据已经发生了变化,才会让React主动更新页面
setState方法调用自Component类
setState异/同步更新问题
异步更新
setState异步更新依据
import React,{ Component } from 'react';
export default class App extends Component{
constructor(props){
super(props);
this.state = {
counter:0
}
}
render(){
return (
<div>
<div>counter:{this.state.counter}</div>
<button onClick={ e=>{this.increment()}}>+1</button>
</div>
)
}
increment(){
this.setState({
counter:this.state.counter+1
})
console.log(this.state.counter+"increment");
}
}
根据以上代码执行一次点击操作得到如下结果(左边的边框效果是我方便截图使用图钉效果截下来的,不是页面本身效果)
在页面中显示的counter值为1,但是在执行setState之后立刻使用log打印出来的值却是0,可见setState是异步的操作,并不能在执行完setState之后立马拿到最新的state的结果
setState异步设计原因
原文出处(由Redux开发核心成员回答)
总结:
-
设计为异步,可以显著的提升性能
如果每次调用 setState进行一次更新,那么render函数会被频繁调用,界面重新渲染导致效率降低,对性能损耗较大。异步更新数据可以收集数据状态和操作并进行处理,利于对数据批量更新,降低函数调用频率,减少性能损耗 -
state更新而不执行render函数会导致state和props无法同步
而state和props不能保持一致性,会在开发中产生很多的问题
获取异步结果
方法一:setState使用回调函数:
对increment函数进行修改,在第二个参数地方补充一个箭头函数,在函数内部进行一次log
increment(){
this.setState({
counter:this.state.counter+1
},()=>{console.log("getCounter " + this.state.counter)})
console.log(this.state.counter+" increment");
}
效果如下:
方法二:调用声明周期函数componentDidUpdate
componentDidUpdate(prevProps, provState, snapshot){
console.log("componentDidUpdate " + this.state.counter)
}
结果如下:
setState合并操作
比如state中有一个name:A和age:10,如果对age进行setState操作改变成其他值比如age:8,为什么name依旧存在而不会被覆盖,这里关系到源代码执行使用的是Object.assign( )。此处会创建一个新的对象,把原来的属性值和改变了的属性值全都赋予创建的新对象。最后得到的就是原有的值和被操作覆盖的新值。
setState同步更新
setState在以下两种情况下是同步更新数据的:
- setTimeout函数中使用setState可以对数据进行同步更新
increment(){
setTimeout(() => {
this.setState({
counter:this.state.counter+1
})
console.log(this.state.counter+" setTimeout");
}, 0);
console.log(this.state.counter+" increment");
}
效果如下:
- 在原生DOM事件中setState也是同步更新数据的
componentDidMount(){
var btn = document.querySelector('button');
btn.addEventListener('click',()=>{
this.setState({
counter:this.state.counter + 1
})
console.log(this.state.counter);
})
}
效果如下:
setState执行原理
先上部分源码,这里是四个部分的源码,按照田字格布局排布
大致归纳流程:(没那个能力通读源码,只能大致总结)
- 调用setState
- 检查上下文环境生成更新时间相关参数并判定事件优先级(fiber,currenttime,expirationtime等…)
- 根据优先级相关参数判断更新模式是sync(同步更新)或是batched(批量处理)
- 加入执行更新事件的队列,生成事件队列的链表结构
- 根据链表顺序执行更新
总结:setState既是同步的,也是异步的。同步异步取决于setState运行时的上下文。
且setState 只在合成事件和钩子函数中是“异步”的,在原生DOM事件和 setTimeout 中都是同步的。
React更新
更新流程
React渲染流程
- JSX
- 虚拟DOM
- 真实DOM
React更新流程
- props/state的改变
- render函数执行
- 产生新DOM树
- 新旧DOM树进行diff算法寻差
- 根据差异进行更新
- 真实DOM被更新
更新机制
React在props或state发生改变时,会调用React的render方法,创建一颗不同的树
React需要基于这两颗不同的树之间的差别来判断如何有效的更新UI
如果一棵树参考另外一棵树进行完全比较更新,即使是最先进的算法,算法的复杂程度为 O(n3)。(其中 n 是树中元素的数量。复杂度是衡量算法性能的两个指标如空间复杂度,时间复杂度。大O符号里面主要关注的是算法花费和参数之间影响最大的渐进函数式关系)
该算法在React中开销过大,React的更新会变得非常低效,而React对这个算法进行了优化,将其优化成了O(n)
优化如下:
- 同层节点之间相互比较,不会跨节点比较
- 不同类型的节点,产生不同的树结构
- 设置key来指定节点在不同的渲染下保持稳定
列举三种情况:
-
当节点为不同的元素,React会拆卸原有的树,并且建立起新的树:
当元素标签不一致,比如div变成span,都会触发一个完整的重建流程
当卸载一棵树时,对应的DOM节点也会被销毁,组件实例将执行 componentWillUnmount() 方法;
当建立一棵新的树时,对应的 DOM 节点会被创建以及插入到 DOM 中,组件实例将执行 componentWillMount() 方法,随后执行 componentDidMount() 方法
- 当比对两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性。
- 当更新 style 属性时,React 仅更新有所更变的属性
-
同类型的组件元素:
组件会保持不变,React会更新该组件的props,并且调用componentWillReceiveProps( ) 和 componentWillUpdate( ) 方法,随后调用 render( ) 方法,diff 算法将在之前的结果以及新的结果中进行递归
-
在默认条件下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表。当产生差异时,生成一个mutation。
如果是最后插入一条数据:前面两个比较是完全相同的,所以不会产生mutation。最后一个比较,产生一个mutation,将其插入到新的DOM树中,完成更新。 -
但如果是在开头插入一条数据:React会对每一个子元素产生一个mutation,而不是保持 B 和 C 的不变,这种低效的比较方式会带来一定的性能问题。
keys优化
我们在前面遍历列表时,总是会提示一个警告,让我们加入一个key属性:
-
方式一:在最后位置插入数据,这种情况,有无key意义并不大。因为前面的子元素并没有发生变化,react遍历也不会产生mutation操作。
-
方式二:在前面插入数据。在没有key的情况下,所有li都需要进行修改。当子元素拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素。在这种场景下,key为2和3的元素仅仅进行位移,不需要进行任何的修改。将key为1的元素进行一个mutaion操作,插入到最前面的位置即可。
key的注意事项:
-
key应该是唯一的
-
key不要使用随机数(随机数在下一次render时,会重新生成一个数字);
-
使用index作为key,对性能是没有优化的
render调用优化
结合之前的用例示范,发现只要是修改了App中的数据,所有的组件都需要重新render,进行diff算法,这样机制的性能必然是很低的。事实上,很多的组件没有必须要重新render。它们调用render应该有一个前提,就是依赖的数据(state、props)发生改变时,再调用自己的render方法
控制render方法是否被调用可以通过shouldComponentUpdate方法
shouldComponentUpdate
React提供了一个生命周期方法 shouldComponentUpdate(简称为SCU),该方法接受参数,并且需要有返回值:
该方法有两个参数:
- 参数一:nextProps 修改之后,最新的props属性
- 参数二:nextState 修改之后,最新的state属性
该方法返回值
boolean类型值
- 返回值为true,那么就需要调用render方法
- 返回值为false,那么久不需要调用render方法
默认返回的是true,也就是只要state发生改变,就会调用render方法。比如我们在App中增加一个message属性,jsx中并没有依赖这个message,那么它的改变不应该引起重新渲染,但是因为render监听到state的改变,就会重新render,所以最后render方法还是被重新调用了
验证代码:
import React,{ Component } from 'react';
export default class App extends Component{
constructor(props){
//...省略代码
}
render(){
console.log("rendering");
return (
<div>
<div>counter:{this.state.counter}</div>
<button onClick={ e=>{this.increment()}}>+1</button>
</div>
)
}
shouldComponentUpdate(nextProps, nextState){
if(this.state.counter !== nextState.counter){
return true;
}
return false;
}
increment(){
this.setState({
counter:0
})
console.log(this.state.counter+" increment");
}
}
验证效果(区别是有没有把shouldComponentUpdate的相关代码注释掉):
PureComponent
如果在每一个控件中都手动来实现 shouldComponentUpdate的相关逻辑,会极大增加开发的工作量,而开发的过程通常只需要props和state进行状态比较来决定是否进行render,这是一个一般化的操作。可以使用React中的PureComponent来解决。使用方法,直接继承PureComponent即可。
//...省略import代码
export default class App extends React.PureComponent{
//...省略App代码
}
increment(){
this.setState({
// counter: 0
// counter:this.state.counter + 1
})
console.log(this.state.counter+" increment");
}
验证结果同上…
purecomponent的工作机制主要依赖于shallowEqual方法,随后将进行介绍
shallowEqual方法
shallowequal源代码执行逻辑:(调用该函数的时候输出结果被取反,所以此处返回true则是不更新,返回false则更新)
- 判断是不是同一个对象,是的话返回true
- 判断对象是否相同,如果不同返回false
- 获取对象中的key值,判断长度是否相同,长度不同返回false
- keys进行一一比对,如果不同返回false,相同返回true
高阶组件memo
memo一个高阶组件,概念上和PureComponent组件相似,用于判断props和state来决定更新策略,主要用于函数式组件
例如:
const MemoChild = memo(
function Child(){
return(
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
)
}
);
以上是课程全部内容
感谢coderwhy(王红元老师)的课程内容
爱生活,爱猪猪