我报名参加金石计划一期应战——瓜分10万奖池,这是我的第1篇文章,点击查看活动概况
要讲清楚功能优化的原理,就需求知道它的宿世此生,需求答复如下的问题:
- React 是怎么进行页面烘托的?
- 形成页面的卡顿的罪魁祸首是什么呢?
- 咱们为什么需求功能优化?
- React 有哪些场景会需求功能优化?
- React 自身的功能优化手段?
- 还有哪些东西能够提高功能呢?
为什么页面会呈现卡顿的现象?
为什么浏览器会呈现页面卡顿的问题?是不是浏览器不够先进?这都 2202 年了,怎么还会有这种问题呢?
实际上问题的本源来源于浏览器的改写机制。
咱们人类眼睛的改写率是 60Hz,浏览器根据人眼的改写率 计算出了
1000 Ms / 60 = 16.6ms
也便是说,浏览器要在16.6Ms 进行一次改写,人眼就不会感觉到卡顿,而假设超越这个时刻进行改写,就会感觉到卡顿。
而浏览器的主进程在仅仅需求页面的烘托,还需求做解析履行Js,他们运行在一个进程中。
假设js的在履行的长时刻占用主进程的资源,就会导致没有资源进行页面的烘托改写,从而导致页面的卡顿。
那么这个又和 React 的功能优化又有什么联系呢?
React 到底是在哪里呈现了卡顿?
根据咱们上面的知识,js 长时刻强占浏览器主线程形成无法改写而形成卡顿。
那么 React 的卡顿也是根据这个原因。
React 在render的时分,会根据现有render发生的新的jsx的数据和现有fiberRoot 进行比对,找到不同的当地,然后生成新的workInProgress,从而在挂载阶段把新的workInProgress交给服务器烘托。
在这个过程中,React 为了让底层机制更高效快速,进行了许多的优化处理,如建立使命优先级、异步调度、diff算法、时刻分片等。
整个链路便是了高效快速的完结从数据更新到页面烘托的全体流程。
为了不让递归遍历寻找一切更新节点太大而占用浏览器资源,React 晋级了fiber架构,时刻分片,让其能够增量更新。
为了找出一切的更新节点,建立了diff算法,高效的查找一切的节点。
为了更高效的更新,及时响应用户的操作,设计使命调度优先级。
而咱们的功能优化便是为了不给 React 拖后腿,让其更快,更高效的遍历。
那么功能优化的奥义是什么呢??
便是操控改写烘托的涉及规模,咱们只让该更新的更新,不该更新的不要更新,让咱们的更新链路尽可能的短的走完,那么页面当然就会及时改写不会卡顿了。
React 有哪些场景会需求功能优化?
- 父组件改写,而不涉及子组件
- 组件自己操控自己是否改写
- 削减涉及规模,无关改写数据不存入state中
- 兼并 state,削减重复 setState 的操作
- 怎么更快的完结diff的比较,加速进程
咱们别离从这些场景说一下:
一:父组件改写,而不涉及子组件。
咱们知道 React 在组件改写判定的时分,假设触发改写,那么它会深度遍历一切子组件,查找一切更新的节点,根据新的jsx数据和旧的 fiber ,生成新的workInProgress,从而进行页面烘托。
所以父组件改写的话,子组件必然会跟着改写,但是假设这次的改写,和咱们子组件没有联系呢?怎么削减这种涉及呢?
如下面这样:
export default function Father1 (){
let [name,setName] = React.useState('');
return (
<div>
<button onClick={()=>setName("获取到的数据")}>点击获取数据</button>
{name}
<Children/>
</div>
)
}
function Children(){
return (
<div>
这儿是子组件
</div>
)
}
运行成果:
能够看到咱们的子组件被涉及了,解决办法有许多,总体来说分为两种。
- 子组件自己判别是否需求更新 ,典型的便是 PureComponent,shouldComponentUpdate,memo
- 父组件对子组件做个缓冲判别
第一种:运用 PureComponent
运用 PureComponent 的原理便是它会对state 和props进行浅比较,假设发现并不相同就会更新。
export default function Father1 (){
let [name,setName] = React.useState('');
return (
<div>
<button onClick={()=>setName("父组件的数据")}>点击改写父组件</button>
{name}
<Children1/>
</div>
)
}
class Children extends React.PureComponent{
render() {
return (
<div>这儿是子组件</div>
)
}
}
履行成果:
实际上PureComponent
便是在内部更新的时分调用了会调用如下方法来判别 新旧state和props
function shallowEqual(objA: mixed, objB: mixed): boolean {
if (is(objA, objB)) {
return true;
}
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
// Test for A's keys different from B.
for (let i = 0; i < keysA.length; i++) {
const currentKey = keysA[i];
if (
!hasOwnProperty.call(objB, currentKey) ||
!is(objA[currentKey], objB[currentKey])
) {
return false;
}
}
return true;
}
它的判别步骤如下:
- 第一步,首先会直接比较新老
props
或许新老state
是否持平。假设持平那么不更新组件。 - 第二步,判别新老
state
或许props
,有不是目标或许为null
的,那么直接回来 false ,更新组件。 - 第三步,经过
Object.keys
将新老props
或许新老state
的特点名key
变成数组,判别数组的长度是否持平,假设不持平,证明有特点添加或许削减,那么更新组件。 - 第四步,遍历老
props
或许老state
,判别对应的新props
或新state
,有没有与之对应而且持平的(这个持平是浅比较),假设有一个不对应或许不持平,那么直接回来false
,更新组件。 到此为止,浅比较流程完毕,PureComponent
便是这么做烘托节省优化的。
在运用PureComponent时需求留意的细节:
因为PureComponent
运用的是浅比较判别state
和props
,所以假设咱们在父子组件中,子组件运用PureComponent
,在父组件改写的过程中不小心把传给子组件的回调函数变了,就会形成子组件的误触发,这个时分PureComponent
就失效了。
细节一:函数组件中,匿名函数,箭头函数和一般函数都会从头声明
下面这些情况都会形成函数的从头声明:
箭头函数
<Children1 callback={(value)=>setValue(value)}/>
匿名函数
<Children1 callback={function (value){setValue(value)}}/>
一般函数
export default function Father1 (){
let [name,setName] = React.useState('');
let [value,setValue] = React.useState('')
const setData=(value)=>{
setValue(value)
}
return (
<div>
<button onClick={()=>setName("父组件的数据"+Math.random())}>点击改写父组件</button>
{name}
<Children1 callback={setData}/>
</div>
)
}
class Children1 extends React.PureComponent{
render() {
return (
<div>这儿是子组件</div>
)
}
}
履行成果:
能够看到子组件的 PureComponent 完全失效了。这个时分就能够运用useMemo或许 useCallback 出马了,利用他们缓冲一份函数,保证不会呈现重复声明就能够了。
export default function Father1 (){
let [name,setName] = React.useState('');
let [value,setValue] = React.useState('')
const setData= React.useCallback((value)=>{
setValue(value)
},[])
return (
<div>
<button onClick={()=>setName("父组件的数据"+Math.random())}>点击改写父组件</button>
{name}
<Children1 callback={setData}/>
</div>
)
}
看成果:
能够看到咱们的子组件这次并没有参加父组件的改写,在React Profiler
中也提示,Children1
并没有烘托。
细节二:class组件中不运用箭头函数,匿名函数
原理和函数组件中的一样,class 组件中每一次改写都会重复调用render
函数,那么render
函数中运用的匿名函数,箭头函数就会形成重复改写的问题。
export default class Father extends React.PureComponent{
constructor(props) {
super(props);
this.state = {
name:"",
count:"",
}
}
render() {
return (
<div>
<button onClick={()=>this.setState({name:"父组件的数据"+Math.random()})}>点击获取数据</button>
{this.state.name}
<Children1 callback={()=>this.setState({count:11})}/>
</div>
)
}
}
履行成果:
而优化这个十分简略,只需求把函数换成一般函数就能够。
export default class Father extends React.PureComponent{
constructor(props) {
super(props);
this.state = {
name:"",
count:"",
}
}
setCount=(count)=>{
this.setState({count})
}
render() {
return (
<div>
<button onClick={()=>this.setState({name:"父组件的数据"+Math.random()})}>点击获取数据</button>
{this.state.name}
<Children1 callback={this.setCount(111)}/>
</div>
)
}
}
履行成果:
细节三:在 class 组件的render函数中调用bind 函数
这个细节是咱们在class组件中,没有在constructor
中进行bind
的操作,而是在render
函数中,那么因为bind
函数的特性,它的每一次调用都会回来一个新的函数,所以相同会形成PureComponent
的失效
export default class Father extends React.PureComponent{
//...
setCount(count){
this.setCount({count})
}
render() {
return (
<div>
<button onClick={()=>this.setState({name:"父组件的数据"+Math.random()})}>点击获取数据</button>
{this.state.name}
<Children1 callback={this.setCount.bind(this,"11111")}/>
</div>
)
}
}
看履行成果:
优化的方法也很简略,把bind
操作放在constructor
中就能够了。
constructor(props) {
super(props);
this.state = {
name:"",
count:"",
}
this.setCount= this.setCount.bind(this);
}
履行成果就不在此展现了。
而实际上上诉所说的三个细节相同对React.memo
有用,它相同也会浅比较传入的props
.
第二种:shouldComponentUpdate
class 组件中 运用 shouldComponentUpdate 是首要的优化方法,它不仅仅能够判别来自父组件的nextprops
,还能够根据nextState
和最新的nextContext
来决议是否更新。
class Children2 extends React. PureComponent{
shouldComponentUpdate(nextProps, nextState, nextContext) {
//判别只要偶数的时分,子组件才会更新
if(nextProps !== this.props && nextProps.count % 2 === 0){
return true;
}else{
return false;
}
}
render() {
return (
<div>
只要父组件传入的值等于 2的时分才会更新
{this.props.count}
</div>
)
}
}
它的用法也是十分简略,便是假设需求更新就回来true,不需求更新就回来false.
第三种:函数组件怎么判别props的改变的更新呢? 运用 React.memo函数
React.memo
的规则是假设想要复用最终一次烘托成果,就回来true
,不想复用就回来false
。
所以它和shouldComponentUpdate
的正好相反,false
才会更新,true
就回来缓冲。
const Children3 = React.memo(function ({count}){
return (
<div>
只要父组件传入的值是偶数的时分才会更新
{count}
</div>
)
},(prevProps, nextProps)=>{
if(nextProps.count % 2 === 0){
return false;
}else{
return true;
}
})
假设咱们不传入第二个函数,而是默许让 React.memo
包裹一下,那么它只会对props
浅比较一下,并不会有比较state
之类的逻辑。
以上三种都是咱们为了应对父组件更新触发子组件,子组件决议是否更新的实现。 下面咱们讲一下父组件对子组件缓冲实现的情况:
运用 React.useMemo来实现对子组件的缓冲
看下面这段逻辑,咱们的子组件只关心count
数据,当咱们改写name
数据的时分,并不会触发改写 Children1
子组件,实现了咱们对组件的缓冲操控。
export default function Father1 (){
let [count,setCount] = React.useState(0);
let [name,setName] = React.useState(0);
const render = React.useMemo(()=><Children1 count = {count}/>,[count])
return (
<div>
<button onClick={()=>setCount(++count)}>点击改写count</button>
<br/>
<button onClick={()=>setName(++name)}>点击改写name</button>
<br/>
{"count"+count}
<br/>
{"name"+name}
<br/>
{render}
</div>
)
}
class Children1 extends React.PureComponent{
render() {
return (
<div>
子组件只联系count 数据
{this.props.count}
</div>
)
}
}
履行成果: 当咱们点击改写name数据时,能够看到没有子组件参加改写
当咱们点击改写count 数据时,子组件参加了改写
二:组件自己操控自己是否改写
这儿就需求用到上面说到的shouldComponentUpdate
以及PureComponent
,这儿不再赘述。
三:削减涉及规模,无关改写数据不存入state中
这种场景便是咱们有意识的操控,假设有一个数据咱们在页面上并没有用到它,但是它又和咱们的其他的逻辑有联系,那么咱们就能够把它存储在其他的当地,而不是state中。
场景一:无意义重复调用setState,兼并相关的state
export default class Father extends React.Component{
state = {
count:0,
name:"",
}
getData=(count)=>{
this.setState({count});
//根据异步获取数据
setTimeout(()=>{
this.setState({
name:"异步获取回来的数据"+count
})
},200)
}
componentDidUpdate(prevProps, prevState, snapshot) {
console.log("烘托次数,",++count,"次")
}
render() {
return (
<div>
<button onClick={()=>this.getData(++this.state.count)}>点击获取数据</button>
{this.state.name}
</div>
)
}
}
React Profiler
的履行成果:
能够看到咱们的父组件履行了两次。
其间的一次是无意义的先setState
保存一次数据,然后又根据这个数据异步获取了数据今后又调用了一次setState
,形成了第二次的数据改写.
而解决办法便是把这个数据兼并到异步数据获取完结今后,一起更新到state中。
getData=(count)=>{
//根据异步获取数据
setTimeout(()=>{
this.setState({
name:"异步获取回来的数据"+count,
count
})
},200)
}
看履行成果:只烘托了一次。
场景二:和页面改写没有相关的数据,不存入state中
实际上咱们发现这个数据在页面上并没有展现,咱们并不需求把他们都存放在state 中,所以咱们能够把这个数据存储在state之外的当地。
export default class Father extends React.Component{
constructor(props) {
super(props);
this.state = {
name:"",
}
this.count = 0;
}
getData=(count)=>{
this.count = count;
//根据异步获取数据
setTimeout(()=>{
this.setState({
name:"异步获取回来的数据"+count,
})
},200)
}
componentDidUpdate(prevProps, prevState, snapshot) {
console.log("烘托次数,",++count,"次")
}
render() {
return (
<div>
<button onClick={()=>this.getData(++this.count)}>点击获取数据</button>
{this.state.name}
</div>
)
}
}
这样的操作并不会影响咱们对它的运用。
在class
组件中咱们能够把数据存储在this
上面,而在Function
中,则咱们能够经过利用 useRef
这个 Hooks
来实现相同的效果。
export default function Father1 (){
let [name,setName] = React.useState('');
const countContainer = React.useRef(0);
const getData=(count)=>{
//根据异步获取数据
setTimeout(()=>{
setName("异步获取回来的数据"+count)
countContainer.current = count++;
},200)
}
return (
<div>
<button onClick={()=>getData(++countContainer.current)}>点击获取数据</button>
{name}
</div>
)
}
场景三:经过存入useRef的数据中,防止父子组件的重复改写
假设父组件中有需求用到子组件的数据,子组件需求把数据回到回来给父组件,而假设父组件把这份数据存入到了 stat
e 中,那么父组件改写,子组件也会跟着改写。
这种的情况咱们就能够把数据存入到 useRef
中,以防止无意义的改写呈现。或许把数据存入到class的 this
下。
四:兼并 state,削减重复 setState 的操作
兼并 state
,削减重复 setState
的操作,实际上 React
现已帮咱们做了,那便是批量更新,在React18
之前的版本中,批量更新只要在 React自己的生命周期或许点击事情中有供给,而异步更新则没有,例如setTimeout
,setInternal
等。
所以假设咱们想在React18
之前的版本中也想在异步代码添加对批量更新的支持,就能够运用React
给咱们供给的api
。
import ReactDOM from 'react-dom';
const { unstable_batchedUpdates } = ReactDOM;
运用方法如下:
componentDidMount() {
setTimeout(()=>{
unstable_batchedUpdates(()=>{
this.setState({ number:this.state.number + 1 })
console.log(this.state.number)
this.setState({ number:this.state.number + 1})
console.log(this.state.number)
this.setState({ number:this.state.number + 1 })
console.log(this.state.number)
})
})
}
而在 React 18中的话,就不需求咱们这样做了,它 对settimeout、promise、原生事情、react事情、外部事情处理程序进行自动批量处理。
五:怎么更快的完结diff的比较,加速进程
diff
算法便是为了协助咱们找到需求更新的异同点,那么有什么办法能够让咱们的diff
算法更快呢?
那便是合理的运用key
diff
的调用是在reconcileChildren
中的reconcileChildFibers
,当没有能够复用current
fiber
节点时,就会走mountChildFibers
,当有的时分就走reconcileChildFibers
。
而reconcilerChildFibers
的函数中则会针render
函数回来的新的jsx
数据进行判别,它是否是目标,就会判别它的newChild.$$typeof
是否是REACT_ELEMENT_TYPE
,假设是就按单节点处理。 假设不是持续判别是否是REACT_PORTAL_TYPE
或许REACT_LAZY_TYPE
。
持续判别它是否为数组,或许可迭代目标。
而在单节点处理函数reconcileSingleElement
中,会履行如下逻辑:
- 经过
key
,判别前次更新的时分的Fiber
节点是否存在对应的DOM
节点。 假设没有 则直接走创立流程,新生成一个 Fiber 节点,并回来 - 假设有,那么就会持续判别,
DOM
节点是否能够复用?
- 假设有,就将前次更新的
Fiber
节点的副本作为本次新生的Fiber
节点并回来
- 假设没有,那么就标记
DOM
需求被删去,新生成一个Fiber
节点并回来。
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement
): Fiber {
const key = element.key; //jsx 虚拟 DOM 回来的数据
let child = currentFirstChild;//当时的fiber
// 首先判别是否存在对应DOM节点
while (child !== null) {
// 上一次更新存在DOM节点,接下来判别是否可复用
// 首先比较key是否相同
if (child.key === key) {
// key相同,接下来比较type是否相同
switch (child.tag) {
// ...省掉case
default: {
if (child.elementType === element.type) {
// type相同则表示能够复用
// 回来复用的fiber
return existing;
}
// type不同则跳出switch
break;
}
}
// 代码履行到这儿代表:key相同但是type不同
// 将该fiber及其兄弟fiber标记为删去
deleteRemainingChildren(returnFiber, child);
break;
} else {
// key不同,将该fiber标记为删去
deleteChild(returnFiber, child);
}
child = child.sibling;
}
// 创立新Fiber,并回来 ...省掉
}
从上面的代码就能够看出,React
是怎么判别一个 Fiber
节点是否能够被复用的。
- 第一步:判别
element
的key
和fiber
的key
是否相同
- 假设不相同,就会创立新的
Fiber
,并回来
- 第二步:假设相同,就判别
element.type
和fiber
的type
是否相同,type
便是他们的类型,比方p
标签便是p,div
标签便是div
.假设type
不相同,那么就会标识删去。
- 假设相同,那就能够能够判别能够复用了,回来
existing
。
而在多节点更新的时分,key
的效果则更加重要,React
会经过遍历新旧数据,数组和链表来经过按个判别它们的key
和 type
来决议是否复用。
所以咱们需求合理的运用key
来加速diff
算法的比对和fiber
的复用。
那么怎么合理运用key
呢。
其实很简略,只需求每一次设置的值和咱们的数据一直就能够了。不要运用数组
的下标,这种key
和数据没有关联,咱们的数据发生了更新,成果 React
还指望着复用。
还有哪些东西能够提高功能呢?
实际的开发中还有其他的许多场景需求进行优化:
- 频频输入或许滑动滚动的防抖节省
- 针对大数据展现的虚拟列表,虚拟表格
- 针对大数据展现的时刻分片 等等等等 后边再补充吧!
感谢大佬的文章:
React进阶实践攻略-烘托操控篇
over…