高质量React指北之「重新渲染」
起因
最近接触的框架是React,作为一个新手,如果觉得没有遇到问题往往就是最大的问题(哈哈)。使用中常常会有一些让我疑惑的现象产生,其中困扰我最多的当之无愧的就是React的渲染问题,在研究了一段时间之后就有了我现在的这篇文章。姑且是一篇学习笔记吧。
内容
本文会围绕三个最主要的问题展开。
- 组件何时会触发渲染?
- 渲染中哪些是不必要的?
- 我们又该如何去阻止不必要的重新渲染?
组件何时会触发渲染?
- 组件第一次显示到浏览器时
- 组件的
state、props有变化等 - 父组件的重新渲染将导致其所有的子组件重新渲染
渲染中哪些是不必要的?
父组件中与子组件无关的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)

我们又该如何去阻止不必要的重新渲染?
在上一点中,我们知道了重新渲染的组件将会得到一个新的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>
}

注意
如果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>
}

通过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.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
useCallback和useMemo同样都可以缓存值,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>
}
使用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>
}

注意
当组件并没有使用memo进行过包裹时,对其props使用useMemo与useCallback是毫无意义的,在重新渲染时并不会走对比逻辑,而只会直接进行重新渲染的操作。
结语
总结的来说,哪怕使用了memo对组件进行了包裹,但如果props中,只要有一个引用类型的值没有进行记忆,对其他的值进行记忆都是没有意义的emmm
转载自:https://juejin.cn/post/7130643978936352775