React前端渲染优化--父组件导致子组件重复渲染
说明
目前我们所使用 react 版本一般会有以下四种方式触发渲染 render,而其中通过父组件 render 会直接通知子组件也进行 render。
一般的优化方式
鉴于此种情况,如果完全不做控制下,父组件 render, 那么子组件一定会 render。真实 dom 的渲染 react 会在 diff 算法之后合计出最小改动,进行操作。但对于结构复杂页面,自顶向下,只是单纯 diff 也要花费很长的时间来处理 js 任务。再加上我们每个组件的 render 中也会写很多业务、数据处理。
js 为单线程执行,显然,不必要的子组件的 render 会浪费 js 线程资源,复杂任务还会长时间占用线程导致假死状态,也就是页面卡顿,react 底层有 Fiber 来优化任务队列,但无法优化业务代码上的问题。
一般子组件可以通过确认 props 是否发生变化来控制自身是否进行 render,比如 react-mobx 中的 observer 高阶方法或者 React.PureComponet 就是用来做浅层比较进行控制处理。
项目中常见会导致重复渲染的写法以及改进方法
函数导致的渲染重复
箭头函数 props.fn = () => {} 或者 绑定方法 props.fn = this.xxx.bind(this)
这样的写法每次父组件 render 都会新声明一个 function 传递给子组件,会导致 observer 失去比对作用,父组件每次 render 都会使这个组件 render,严重影响性能!
import React from 'react'; import { observer } from 'mobx-react'; // 我们开发中常见的一个被观测组件,例如 ObserverComponent @observer class ObserverComponent extends React.Component { render() { return (ObserverComponent) } } // 例如在父组件 Parent 使用被观测的子组件 ObserverComponent // 请不要给子组件 ObserverComponent 的 props 设置 箭头函数 () => {} 或者 fn.bind(this) 方法 @observer class Parent extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); // 【正确】 } handleChange() {} doSomething = () => {} render() { return ({}} // 【错误】 onChange={this.handleChange.bind(this)} // 【错误】 onChange={this.handleChange} // 【正确】 todo={this.doSomething} // 【正确】 /> ) } }
字面量写法导致的渲染重复
由于字面量的写法{} 和 { pageSizeOptions: ['10'] },每次都会字面量声明一个新的对象传递给列表组件,导致页面重新 render。
toJS() 方法每次也会返回新对象,会导致页面重新渲染
组件重复渲染问题(pureComponent, React.memo, useMemo, useCallback)
在一个组件中, 其state变化会引起render的重新执行, 函数式组件中, 使用setHook更新state也会引起render的重新执行
render执行会带来两个方面的影响
- 1.当前组件需要重新渲染, 除了那些状态和生命周期初始化被保留的,其余正常的都会重新执行。
- 2.子组件会重新渲染, 即使其是一个无状态组件
针对上述问题, react给出来解决方案:
pureComponent
React.memo
useMemo
useCallback
下面将具体说明这几个都使用场景和解决的问题
- useMemo设计的初衷就是避免重复进行大规模的计算, 它的理想作用对象是当前组件
具体是将当前组件中一个经过很复杂的计算得到的值缓存起来, 当其依赖项不变的时候, 即使组件重新渲染, 也不会重新计算。
通过上述描述也能理解出其缓存的是一个具体的数据(可以和接下来的useCallback区分开)
/* 缓存了一个对象, 只有当count变化时才会重新返回该对象 */ const useInfo = useMemo( () => ({ count: count, name: "name" }), [count] )
- 针对第二点, 分别有三个解决方案
首先是useCallback, 其语法和useMemo基本一致, 但是其使用场景是父组件定义了一个函数并且将这个函数传递给了子组件, 那么当父组件重新渲染时,生成的会是一个新的函数, 这个时候就可以使用useCallback了,如下:
const Page = (props) => { const [count, setCount] = useState(0); const [name, setName] = useState('Child组件'); return ( <>setName(newName), []) }/> {/* useCallback((newName: string) => setName(newName),[]) */} {/* 这里使用了useCallback优化了传递给子组件的函数,只初始化一次这个函数,下次不产生新的函数 > ) }
上述是一个简写的形式,意思就是将传递给子组件的这个函数缓存了,其第二个参数就是依赖,当该依赖变化时,将会重新缓存该函数
其余useMemo的区别就在于,其缓存的是函数本身,而useMemo缓存的是函数计算后的值,都会在依赖项变化时重新缓存。
注:虽然其可能对于父组件传递给子组件函数时可能很理想,但实际上其带来的性能损耗也是显而易见的,其使用场景不应该是担心本组件的函数因为本组件重新渲染而重新生成,这样反而起到了反效果,当前组件更新,其重新渲染,内部的函数也重新生成,其性能损耗可以忽略不计,如下图。
使用场景应该是父组件更新导致重新生成的函数又传递给了子组件,导致子组件重新渲染。
- 接着是pureComponent
它是一个类, 组件继承自它后, 其作为子组件时, 每次父组件更新后, 会浅对比传来的props是否变化, 若没变化, 则子组件不更新。
- React.memo
同上条功能类似, 当其作用于函数式组件并且作为子组件时, 每次父组件更新后, 会浅对比传来的props是否变化, 若没变化, 则子组件不更新。
// 子组件暴露时暴露为处理后的组件 import {memo} from 'react' const TeacherModal = (props: any) => { return } export default memo(TeacherModal)
上面两个都区别在于, 一个是类, 一个是高阶组件, 前者作用于类后者作用于函数
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。