高质量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