react--learning--渲染优化

83 阅读9分钟

一 React 几种控制 render 方法

说到对render 的控制,究其本质,主要有以下两种方式:

  • 第一种就是从父组件直接隔断子组件的渲染,经典的就是 memo,缓存 element 对象。
  • 第二种就是组件从自身来控制是否 render ,比如:PureComponent ,shouldComponentUpdate

1 缓存React.element对象

useMemo 用法:

const cacheSomething = useMemo(create,deps)
  • create:第一个参数为一个函数,函数的返回值作为缓存值,如上 demo 中把 Children 对应的 element 对象,缓存起来。
  • deps: 第二个参数为一个数组,存放当前 useMemo 的依赖项,在函数组件下一次执行的时候,会对比 deps 依赖项里面的状态,是否有改变,如果有改变重新执行 create ,得到新的缓存值。
  • cacheSomething:返回值,执行 create 的返回值。如果 deps 中有依赖项改变,返回的重新执行 create 产生的值,否则取上一次缓存值。

useMemo原理: useMemo 会记录上一次执行 create 的返回值,并把它绑定在函数组件对应的 fiber 对象上,只要组件不销毁,缓存值就一直存在,但是 deps 中如果有一项改变,就会重新执行 create ,返回值作为新的值记录到 fiber 对象上。

useMemo应用场景:

  • 可以缓存 element 对象,从而达到按条件渲染组件,优化性能的作用。
  • 如果组件中不期望每次 render 都重新计算一些值,可以利用 useMemo 把它缓存起来。
  • 可以把函数和属性缓存起来,作为 PureComponent 的绑定方法,或者配合其他Hooks一起使用。

原理揭秘

如上讲了利用 element 的缓存,实现了控制子组件不必要的渲染,究其原理是什么呢?

原理其实很简单,上述每次执行 render 本质上 createElement 会产生一个新的 props,这个 props 将作为对应 fiber 的 pendingProps ,在此 fiber 更新调和阶段,React 会对比 fiber 上老 oldProps 和新的 newProp ( pendingProps )是否相等,如果相等函数组件就会放弃子组件的调和更新,从而子组件不会重新渲染;如果上述把 element 对象缓存起来,上面 props 也就和 fiber 上 oldProps 指向相同的内存空间,也就是相等,从而跳过了本次更新。

2 PureComponent

/* 纯组件本身 */
class Children extends React.PureComponent{
    state={
        name:'alien',
        age:18,
        obj:{
            number:1,
        }
    }
    changeObjNumber=()=>{
        const { obj } = this.state
        obj.number++
        this.setState({ obj })
    }
    render(){
        console.log('组件渲染')
        return <div  >
           <div> 组件本身改变state </div>
           <button onClick={() => this.setState({ name:'alien' }) } >state相同情况</button>
           <button onClick={() => this.setState({ age:this.state.age + 1  }) }>state不同情况</button>
           <button onClick={ this.changeObjNumber } >state为引用数据类型时候</button>
           <div>hello,my name is alien,let us learn React!</div>
        </div>
    }
}
/* 父组件 */
export default function Home (){
    const [ numberA , setNumberA ] = React.useState(0)
    const [ numberB , setNumberB ] = React.useState(0)
    return <div>
        <div> 父组件改变props </div>
        <button onClick={ ()=> setNumberA(numberA + 1) } >改变numberA</button>
        <button onClick={ ()=> setNumberB(numberB + 1) } >改变numberB</button>
        <Children number={numberA}  /> 
    </div>
}
  • 对于 props ,PureComponent 会浅比较 props 是否发生改变,再决定是否渲染组件,所以只有点击 numberA 才会促使组件重新渲染。
  • 对于 state ,如上也会浅比较处理,当上述触发 ‘ state 相同情况’ 按钮时,组件没有渲染。
  • 浅比较只会比较基础数据类型,对于引用类型,比如 demo 中 state 的 obj ,单纯的改变 obj 下属性是不会促使组件更新的,因为浅比较两次 obj 还是指向同一个内存空间,想要解决这个问题也容易,浅拷贝就可以解决,将如上 changeObjNumber 这么修改。这样就是重新创建了一个 obj ,所以浅比较会不相等,组件就会更新了。

PureComponent 原理及其浅比较原则

PureComponent 内部是如何工作的呢,首先当选择基于 PureComponent 继承的组件。原型链上会有 isPureReactComponent 属性。一起看一下创建 PureComponent 时候:

/* pureComponentPrototype 纯组件构造函数的 prototype 对象,绑定isPureReactComponent 属性。 */ pureComponentPrototype.isPureReactComponent = true;

isPureReactComponent 这个属性在更新组件 updateClassInstance 方法中使用的,

在这个函数内部,有一个专门负责检查是否更新的函数 checkShouldComponentUpdate 。

function checkShouldComponentUpdate(){
     if (typeof instance.shouldComponentUpdate === 'function') {
         return instance.shouldComponentUpdate(newProps,newState,nextContext)  /* shouldComponentUpdate 逻辑 */
     } 
    if (ctor.prototype && ctor.prototype.isPureReactComponent) {
        return  !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
    }
}
  • isPureReactComponent 就是判断当前组件是不是纯组件的,如果是 PureComponent 会浅比较 props 和 state 是否相等。
  • 还有一点值得注意的就是 shouldComponentUpdate 的权重,会大于 PureComponent。
  • shallowEqual 是如何浅比较的呢,由于我不想在章节中写过多的源码,我在这里就直接描述过程了。

shallowEqual 浅比较流程:

  • 第一步,首先会直接比较新老 props 或者新老 state 是否相等。如果相等那么不更新组件。
  • 第二步,判断新老 state 或者 props ,有不是对象或者为 null 的,那么直接返回 false ,更新组件。
  • 第三步,通过 Object.keys 将新老 props 或者新老 state 的属性名 key 变成数组,判断数组的长度是否相等,如果不相等,证明有属性增加或者减少,那么更新组件。
  • 第四步,遍历老 props 或者老 state ,判断对应的新 props 或新 state ,有没有与之对应并且相等的(这个相等是浅比较),如果有一个不对应或者不相等,那么直接返回 false ,更新组件。

到此为止,浅比较流程结束, PureComponent 就是这么做渲染节流优化的。

PureComponent注意事项

PureComponent 可以让组件自发的做一层性能上的调优,但是,父组件给是 PureComponent 的子组件绑定事件要格外小心,避免两种情况发生:

1 避免使用箭头函数。不要给是 PureComponent 子组件绑定箭头函数,因为父组件每一次 render ,如果是箭头函数绑定的话,都会重新生成一个新的箭头函数, PureComponent 对比新老 props 时候,因为是新的函数,所以会判断不想等,而让组件直接渲染,PureComponent 作用终会失效。

2 PureComponent 的父组件是函数组件的情况,绑定函数要用 useCallback 或者 useMemo 处理。这种情况还是很容易发生的,就是在用 class + function 组件开发项目的时候,如果父组件是函数,子组件是 PureComponent ,那么绑定函数要小心,因为函数组件每一次执行,如果不处理,还会声明一个新的函数,所以 PureComponent 对比同样会失效,如下情况:

class Index extends React.PureComponent{}
export default function (){
    const callback = function handerCallback(){} /* 每一次函数组件执行重新声明一个新的callback,PureComponent浅比较会认为不想等,促使组件更新  */
    return <Index callback={callback}  />
}

综上可以用 useCallback 或者 useMemo 解决这个问题,useCallback 首选,这个 hooks 初衷就是为了解决这种情况的。

export default function (){
    const callback = React.useCallback(function handerCallback(){},[])
    return <Index callback={callback}  />
}

useCallback 接受二个参数,第一个参数就是需要缓存的函数,第二个参数为deps, deps 中依赖项改变返回新的函数。如上处理之后,就能从根本上解决 PureComponent 失效问题。

useCallback 和 useMemo 有什么区别?

答:useCallback 第一个参数就是缓存的内容,useMemo 需要执行第一个函数,返回值为缓存的内容,比起 useCallback , useMemo 更像是缓存了一段逻辑,或者说执行这段逻辑获取的结果。那么对于缓存 element 用 useCallback 可以吗,答案是当然可以了。

3 shouldComponentUpdate

有的时候,把控制渲染,性能调优交给 React 组件本身处理显然是靠不住的,React 需要提供给使用者一种更灵活配置的自定义渲染方案,使用者可以自己决定是否更新当前组件,shouldComponentUpdate 就能达到这种效果。在生命周期章节介绍了 shouldComponentUpdate 的用法,接下来试一下 shouldComponentUpdate 如何使用。

class Index extends React.Component{ //子组件
    state={
        stateNumA:0,
        stateNumB:0
    }
    shouldComponentUpdate(newProp,newState,newContext){
        if(newProp.propsNumA !== this.props.propsNumA || newState.stateNumA !== this.state.stateNumA ){
            return true /* 只有当 props 中 propsNumA 和 state 中 stateNumA 变化时,更新组件  */
        }
        return false 
    }
    render(){
        console.log('组件渲染')
        const { stateNumA ,stateNumB } = this.state
        return <div>
            <button onClick={ ()=> this.setState({ stateNumA: stateNumA + 1 }) } >改变state中numA</button>
            <button onClick={ ()=> this.setState({ stateNumB: stateNumB + 1 }) } >改变stata中numB</button>
            <div>hello,let us learn React!</div>
        </div>
    }
}
export default function Home(){ // 父组件
    const [ numberA , setNumberA ] = React.useState(0)
    const [ numberB , setNumberB ] = React.useState(0)
    return <div>
        <button onClick={ ()=> setNumberA(numberA + 1) } >改变props中numA</button>
        <button onClick={ ()=> setNumberB(numberB + 1) } >改变props中numB</button>
        <Index propsNumA={numberA}  propsNumB={numberB}   />
    </div>
}

shouldComponentUpdate 可以根据传入的新的 props 和 state ,或者 newContext 来确定是否更新组件,如上面例子🌰,只有当 props 中 propsNumA 属性和 state 中 stateNumA 改变的时候,组件才渲染。但是有一种情况就是如果子组件的 props 是引用数据类型,比如 object ,还是不能直观比较是否相等。那么如果想有对比新老属性相等,怎么对比呢,而且很多情况下,组件中数据可能来源于服务端交互,对于属性结构是未知的。

immutable.js 可以解决此问题,immutable.js 不可变的状态,对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。鉴于这个功能,所以可以把需要对比的 props 或者 state 数据变成 Immutable 对象,通过对比 Immutable 是否相等,来证明状态是否改变,从而确定是否更新组件。

4 React.memo

React.memo(Component,compare)

React.memo 可作为一种容器化的控制渲染方案,可以对比 props 变化,来决定是否渲染组件,首先先来看一下 memo 的基本用法。React.memo 接受两个参数,第一个参数 Component 原始组件本身,第二个参数 compare 是一个函数,可以根据一次更新中 props 是否相同决定原始组件是否重新渲染。

memo的几个特点是:

  • React.memo: 第二个参数 返回 true 组件不渲染 , 返回 false 组件重新渲染。和 shouldComponentUpdate 相反,shouldComponentUpdate : 返回 true 组件渲染 , 返回 false 组件不渲染。
  • memo 当二个参数 compare 不存在时,会用浅比较原则处理 props ,相当于仅比较 props 版本的 pureComponent 。
  • memo 同样适合类组件和函数组件。

被 memo 包裹的组件,element 会被打成 REACT_MEMO_TYPE 类型的 element 标签,在 element 变成 fiber 的时候, fiber 会被标记成 MemoComponent 的类型。

react/src/ReactMemo.js
function memo(type,compare){
  const elementType = {
    $$typeof: REACT_MEMO_TYPE, 
    type,  // 我们的组件
    compare: compare === undefined ? null : compare,  //第二个参数,一个函数用于判断prop,控制更新方向。
  };
  return elementType
}
react-reconciler/src/ReactFiber.js
case REACT_MEMO_TYPE: 
fiberTag = MemoComponent;

那么对于 MemoComponent React 内部又是如何处理的呢?首先 React 对 MemoComponent 类型的 fiber 有单独的更新处理逻辑 updateMemoComponent 。首先一起看一下主要逻辑:

react-reconciler/src/ReactFiberBeginWork.js
function updateMemoComponent(){
    if (updateExpirationTime < renderExpirationTime) {
         let compare = Component.compare;
         compare = compare !== null ? compare : shallowEqual //如果 memo 有第二个参数,则用二个参数判定,没有则浅比较props是否相等。
        if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
            return bailoutOnAlreadyFinishedWork(current,workInProgress,renderExpirationTime); //已经完成工作停止向下调和节点。
        }
    }
    // 返回将要更新组件,memo包装的组件对应的fiber,继续向下调和更新。
}

memo 主要逻辑是

  • 通过 memo 第二个参数,判断是否执行更新,如果没有那么第二个参数,那么以浅比较 props 为 diff 规则。如果相等,当前 fiber 完成工作,停止向下调和节点,所以被包裹的组件即将不更新。
  • memo 可以理解为包了一层的高阶组件,它的阻断更新机制,是通过控制下一级 children ,也就是 memo 包装的组件,是否继续调和渲染,来达到目的的。