我报名参加金石计划一期应战——瓜分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>
    )
}

运行成果:

React 功能优化的那些事儿

能够看到咱们的子组件被涉及了,解决办法有许多,总体来说分为两种。

  • 子组件自己判别是否需求更新 ,典型的便是 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>
        )
    }
}

履行成果:

React 功能优化的那些事儿

实际上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 运用的是浅比较判别stateprops,所以假设咱们在父子组件中,子组件运用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>
        )
    }
}

履行成果:

React 功能优化的那些事儿

能够看到子组件的 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 功能优化的那些事儿
能够看到咱们的子组件这次并没有参加父组件的改写,在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>
        )
    }
}

履行成果:

React 功能优化的那些事儿

而优化这个十分简略,只需求把函数换成一般函数就能够。

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>
        )
    }
}

履行成果:

React 功能优化的那些事儿

细节三:在 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>
        )
    }
}

看履行成果:

React 功能优化的那些事儿

优化的方法也很简略,把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数据时,能够看到没有子组件参加改写

React 功能优化的那些事儿
当咱们点击改写count 数据时,子组件参加了改写

React 功能优化的那些事儿

二:组件自己操控自己是否改写

这儿就需求用到上面说到的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的履行成果:

React 功能优化的那些事儿

能够看到咱们的父组件履行了两次。 其间的一次是无意义的先setState保存一次数据,然后又根据这个数据异步获取了数据今后又调用了一次setState,形成了第二次的数据改写.

而解决办法便是把这个数据兼并到异步数据获取完结今后,一起更新到state中。

getData=(count)=>{
        //根据异步获取数据
        setTimeout(()=>{
            this.setState({
                name:"异步获取回来的数据"+count,
                count
            })
        },200)
}

看履行成果:只烘托了一次。

React 功能优化的那些事儿

场景二:和页面改写没有相关的数据,不存入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的数据中,防止父子组件的重复改写

假设父组件中有需求用到子组件的数据,子组件需求把数据回到回来给父组件,而假设父组件把这份数据存入到了 state 中,那么父组件改写,子组件也会跟着改写。 这种的情况咱们就能够把数据存入到 useRef 中,以防止无意义的改写呈现。或许把数据存入到class的 this 下。

四:兼并 state,削减重复 setState 的操作

兼并 state ,削减重复 setState 的操作,实际上 React现已帮咱们做了,那便是批量更新,在React18 之前的版本中,批量更新只要在 React自己的生命周期或许点击事情中有供给,而异步更新则没有,例如setTimeoutsetInternal等。

所以假设咱们想在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 节点是否能够被复用的。

  • 第一步:判别elementkey fiberkey 是否相同
  • 假设不相同,就会创立新的 Fiber,并回来
  • 第二步:假设相同,就判别element.typefibertype 是否相同,type 便是他们的类型,比方p标签便是p,div 标签便是div.假设 type 不相同,那么就会标识删去。
  • 假设相同,那就能够能够判别能够复用了,回来existing

而在多节点更新的时分,key的效果则更加重要,React 会经过遍历新旧数据,数组和链表来经过按个判别它们的keytype 来决议是否复用。

所以咱们需求合理的运用key来加速diff算法的比对和fiber的复用。

那么怎么合理运用key呢。

其实很简略,只需求每一次设置的值和咱们的数据一直就能够了。不要运用数组的下标,这种key和数据没有关联,咱们的数据发生了更新,成果 React 还指望着复用。

还有哪些东西能够提高功能呢?

实际的开发中还有其他的许多场景需求进行优化:

  • 频频输入或许滑动滚动的防抖节省
  • 针对大数据展现的虚拟列表,虚拟表格
  • 针对大数据展现的时刻分片 等等等等 后边再补充吧!

感谢大佬的文章:

React进阶实践攻略-烘托操控篇

over…