likes
comments
collection
share

高质量React指北之「重新渲染」

作者站长头像
站长
· 阅读数 33

起因

最近接触的框架是React,作为一个新手,如果觉得没有遇到问题往往就是最大的问题(哈哈)。使用中常常会有一些让我疑惑的现象产生,其中困扰我最多的当之无愧的就是React的渲染问题,在研究了一段时间之后就有了我现在的这篇文章。姑且是一篇学习笔记吧。

内容

本文会围绕三个最主要的问题展开。

  • 组件何时会触发渲染?
  • 渲染中哪些是不必要的?
  • 我们又该如何去阻止不必要的重新渲染?

组件何时会触发渲染?

  • 组件第一次显示到浏览器时
  • 组件的stateprops有变化等
  • 父组件的重新渲染将导致其所有的子组件重新渲染

渲染中哪些是不必要的?

父组件中与子组件无关的state更改造成的子组件重新渲染

例如下面这样的代码:

const Child = ()=>{
    console.log('Child-render')
    return <div>Child</div>
}
const  Parent=()=> {
    const [pos,setPos]=useState([0,0])
    return <div onMouseMove={(e=>setPos([e.pageX,e.pageY]))}>
        Parent:{pos.toString()}
        <Child/>
    </div>
}

组件自身组件props中的引用类型参数,内容相同,却传入了新的引用地址,这也会重新渲染。

重新渲染的本质是什么?

ReactElemnt的本质是又调用了一次渲染函数从而得到了一个新的用于描述Node节点的js对象。像下面的代码中:

const child = <Child/>
console.log(child)

高质量React指北之「重新渲染」

我们又该如何去阻止不必要的重新渲染?

在上一点中,我们知道了重新渲染的组件将会得到一个新的ReactNode对象地址,所以,想要规避掉那些不必要的重新渲染,我们最要做的,其实就是保存下原本用于描述ReactNode的js对象,让我们的React组件延用原本的地址引用。很容易想到的是,我们的下文将会在“记忆”中展开。

通过props渲染children

如果一个父组件会高频的更改状态导致重新渲染,我们可以将其这个组件抽象出来,并将原本需要渲染的子组件通过props.children拿到,并得到一样的页面效果,但你会神奇的发现,<Child/>将只会渲染一次,导致这样的原因很简单,<Parent/>state更改导致其重新渲染,而props并没有改变,因此。props.children指向的引用也就没有改变,这正做到了我上面所说的“记忆”。具体代码如下:

const Child = ()=>{
    console.log('Child-render')
    return <div>Child</div>
}
const  Parent=({children})=> {
    const [pos,setPos]=useState([0,0])
    return <div onMouseMove={(e=>setPos([e.pageX,e.pageY]))}>
        Parent:{pos.toString()}
        {children}
    </div>
}
const  App=()=> {
    return <Parent >
        <Child/>
    </Parent>
}

高质量React指北之「重新渲染」

注意

如果children为自定义渲染函数(常用于组件传值)时,本方法将会失效。通过我上面的观点可以很容易想到,props.children函数虽然有着一样的地址引用,但我们最终得到的是调用children自定义函数后的结果,而每一次调用都会返回一个新的ReactNode对象:

const Child = ()=>{
    console.log('Child-render')
    return <div>Child</div>
}
const  Parent=({children})=> {
    const [pos,setPos]=useState([0,0])
    return <div onMouseMove={(e=>setPos([e.pageX,e.pageY]))}>
        Parent:{pos.toString()}
        {children()}
    </div>
}
const  App=()=> {
    return <Parent >
        {()=><Child/>}
    </Parent>
}

高质量React指北之「重新渲染」

通过memo来阻止重新渲染

有的人也许会觉得上面的方式会有点绕,又是封装又是children,那么使用下面memo函数方法将会更直观且能够实现同样的效果。

const Child = ()=>{
    console.log('Child-render')
    return <div>Child</div>
}
const ChildMemo = memo(Child)
const  Parent=()=> {
    const [pos,setPos]=useState([0,0])
    return <div onMouseMove={(e=>setPos([e.pageX,e.pageY]))}>
        Parent:{pos.toString()}
        <ChildMemo/>
    </div>
}

高质量React指北之「重新渲染」

React.memo 为高阶组件。它与 React.PureComponent 非常相似,但只适用于函数组件,而不适用 class 组件。

如果你的函数组件在给定相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。

React.memo 仅检查 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useState 或 useContext 的 Hook,当 context >发生变化时,它仍会重新渲染。

默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。

通过useMemo与useCallback进一步加强memo

memo只会对组件的props做浅层对比,来一个小栗子:

'lizi'==='lizi' //true
{name:'lizi'}==={name:'lizi'} //false

useMemo

返回一个 memoized 值。

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo

如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

你可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证。 将来,React 可能会选择“遗忘”以前的一些 memoized 值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。先编写在没有 useMemo 的情况下也可以执行的代码 —— 之后再在你的代码中添加 useMemo,以达到优化性能的目的。

当子组件的props的有引用类型的值时(对象、数组、函数),每一次父组件的重新渲染都没再创建一次该值,为其分配新的内存地址,memo判断不通过而导致重新渲染,示例代码如下:

const Child = ()=>{
    console.log('Child-render')
    return <div>Child</div>
}
const ChildMemo = memo(Child)
const  Parent=()=> {
    const [pos,setPos]=useState([0,0])
    const value = {
        name:"lizi"
    }
    return <div onMouseMove={(e=>setPos([e.pageX,e.pageY]))}>
        Parent:{pos.toString()}
        <ChildMemo value={value}/>
    </div>
}

使用useMemo进行优化

const Child = ()=>{
    console.log('Child-render')
    return <div>Child</div>
}
const ChildMemo = memo(Child)
const  Parent=()=> {
    const [pos,setPos]=useState([0,0])
    const value = useMemo(()=>({
        name:"lizi"
    }),[])
    return <div onMouseMove={(e=>setPos([e.pageX,e.pageY]))}>
        Parent:{pos.toString()}
        <ChildMemo value={value}/>
    </div>
}

useCallback

useCallbackuseMemo同样都可以缓存值,useCallback缓存的是传入的第一个函数,useMemo缓存的是第一个参数函数执行后的返回值。因此,更常用于缓存回调函数。

何时需要使用useCallback

当父组件传入给子组件的回调函数会更改父组件的状态(会导致父组件重新渲染)时,此时使用useCallback缓存该回调函数。 如果不使用useCallback,调用回调函数更改了父组件state,导致父组件重新渲染,此时就会触发子组件的渲染,在父组件中重新创建回调函数而使子组件memo判断不通过,重新渲染了子组件。示例代码:

const Child = (props)=>{
    console.log('Child-render')
    return <div>Child<button onClick={props.callback}>Parent+1</button></div>
}
const ChildMemo = memo(Child)
const  Parent=()=> {
    const [count,setCount]=useState(0)
    const onClick = ()=>{
        setCount(count+1)
    } //每一次重新渲染父组件都会重新分配内存地址
    return <div>
        Parent{count}
        <ChildMemo callback={onClick}/>
    </div>
}

高质量React指北之「重新渲染」 使用useCallback优化

const Child = (props)=>{
    console.log('Child-render')
    return <div>Child<button onClick={props.callback}>Parent+1</button></div>
}
const ChildMemo = memo(Child)
const  Parent=()=> {
    const [count,setCount]=useState(0)
    const onClick = useCallback(()=>{
        setCount(count=>count+1)
    },[])
    return <div>
        Parent{count}
        <ChildMemo callback={onClick}/>
    </div>
}

高质量React指北之「重新渲染」

注意

当组件并没有使用memo进行过包裹时,对其props使用useMemouseCallback是毫无意义的,在重新渲染时并不会走对比逻辑,而只会直接进行重新渲染的操作。

结语

总结的来说,哪怕使用了memo对组件进行了包裹,但如果props中,只要有一个引用类型的值没有进行记忆,对其他的值进行记忆都是没有意义的emmm

转载自:https://juejin.cn/post/7130643978936352775
评论
请登录