一、什么是 re-render
在 React 中 re-render(重新渲染)
是经常发生的行为,主要确保视图要时刻保持最新的数据来呈现。
但每次发生 re-render
也是有代价的,比如数据状态、focus 焦点、表单数据、都得重置,
遇到代码复杂一点,还给性能带来不必要的消耗。
React 的 re-render
是一个很核心的概念,我们必须得掌握它,否则出现问题将无从下手。
下面我将会介绍触发 re-render
的场景,以及如何避免 re-render
。
二、什么场景会触发 re-render
前言:
re-render
只会影响自身和 children,如果 children 还有 children 则继续影响,以此类推。
理解这点很重要,它将会作为后续如何避免re-render
的解题思路。
提示:此 children 非 props.children
2.1 parent
当 parent
组件发生 re-render
时,自身与 children(子组件) 都会受影响(上面已提到过)。
代码如下:
import ReactDOM from 'react-dom/client';
import { useState, } from 'react';
const root = ReactDOM.createRoot(document.getElementById("root"));
const Parent = () => {
const [parentName, setParentName] = useState('Jack')
const ChildComponent = () => {
console.log('Init child Component.')
return <>
<div>I am child.</div>
</>
}
return <>
<div>
<span>Parent name: {parentName}</span>
<button onClick={() => setParentName('Tony')}>Change Parent'name.</button>
</div>
<ChildComponent/>
</>
}
root.render(
<Parent/>
);
关系如图:
效果:
2.2 state
当 state
数据发生变化时,那些引用过 state 的组件都会发生 re-render
。
提示: 其实和 parent 没啥两样,上面的 parent 演示只是为了加深理解 re-render 的渲染机制。
下面我将以子组件自身更改 state 来完成自身 re-render
的演示。
代码如下:
import ReactDOM from 'react-dom/client';
import { useState, } from 'react';
const root = ReactDOM.createRoot(document.getElementById("root"));
const Parent = () => {
const [parentName, setParentName] = useState('Jack')
const ChildComponent = () => {
console.log('Init child Component.')
return <>
<div>I am child.</div>
<button onClick={() => setParentName('Tony')}>A child can change his parent'name.</button>
</>
}
return <>
<div>
<span>Parent name: {parentName}</span>
</div>
<ChildComponent/>
</>
}
root.render(
<Parent/>
);
关系如图:
效果:
2.3 props
当组件的 props
数据发生变化时触发 re-render
。
代码如下:
import ReactDOM from 'react-dom/client';
import { useState, } from 'react';
const root = ReactDOM.createRoot(document.getElementById("root"));
const Parent = () => {
const [parentName, setParentName] = useState('Jack')
const ChildComponent = (props) => {
console.log('Init child Component.')
return <>
<div>I am child. My parent'name is {props.parentName}</div>
</>
}
return <>
<div>
<span>Parent name: {parentName}</span>
<button onClick={() => setParentName('Tony')}>Change Parent'name.</button>
</div>
<ChildComponent parentName={parentName}/>
</>
}
root.render(
<Parent />
);
关系如图:
你可能注意到了,这触发
re-render
还不是因为更改了 state ? 好像跟上面的例子没啥两样啊?
因为 props 本身就支持传递 state 呀,这块先别着急,目前先把它当成 props 发生变化时,组件会执行re-render
即可 ,等到后面的避免re-render
章节自然就明白了。
效果:
2.4 context
当 context 数据发生变化时,那些引用 context 的组件都会触发 re-render
。
代码如下:
import ReactDOM from 'react-dom/client';
import { useState, createContext, useContext, } from 'react';
const root = ReactDOM.createRoot(document.getElementById("root"));
const ThemeContext = createContext(null)
const Parent = () => {
const [theme, setTheme] = useState({
mode: 'light'
})
const ChildComponent = () => {
const theme = useContext(ThemeContext)
console.log('Init child component.')
return <>
<div>
The current mode is: {theme.mode}
</div>
</>
}
const onChangeTheme = () => {
setTheme(() => ({
...theme,
mode: theme.mode === 'light' ? 'dark' : 'light'
}))
}
return <>
<ThemeContext.Provider value={theme}>
<button onClick={onChangeTheme}>Change mode</button>
<ChildComponent/>
</ThemeContext.Provider>
</>
}
root.render(
<Parent />
);
关系如图:
你可能注意到了,这触发
re-render
还不是因为更改了 state ? 好像跟上面的例子没啥两样啊?
因为 context 本身就支持传递 state 呀,这块先别着急,目前先把它当成 context 发生变化时,组件会执行re-render
即可 ,等到后面的避免re-render
章节自然就明白了。
效果:
提示:还没用过 context 的可以先阅读React & 解释常见的 hooks: useState / useRef / useContext / useReducer 再回头看本文。
三、如何优雅地避免 re-render
前言:在理解
re-render
的触发场景后,对如何避免re-render
将会是一件既有趣又有挑战性的事情。
“挑战性”皆指容易犯“过度/错误
地避免re-render
,这是每个新手基本都会犯的错甚至老手,对于刚学 React 的同学也不必担心,因为它并不是一俩天就能搞定的,得不断的实践和积累才能慢慢减少。
废话不多说,咋们开始进入正文。
3.1 提取 state
既然 state 发生变化时,组件自身和 children 都会 re-render
,我们可以把变更 state 代码单独抽取一个组件来维护。
代码如下:
import ReactDOM from 'react-dom/client';
import { useState, } from 'react';
const root = ReactDOM.createRoot(document.getElementById("root"));
const Parent = () => {
const BaseComponent = () => {
const [parentName, setParentName] = useState('Jack')
return <>
<div>
<span>Parent name: {parentName}</span>
<button onClick={() => setParentName('Tony')}>Change Parent'name.</button>
</div>
</>
}
const ChildComponent = () => {
console.log('Init child Component.')
return <>
<div>I am child.</div>
</>
}
return <>
<BaseComponent />
<ChildComponent/>
</>
}
root.render(
<Parent />
);
关系如图:
效果:
3.2 组件以 props / children 的方式传递
children
其实是props
的一部分,而props
的其它字段也可作为组件,因此我将它俩单独放一起做演示。
回到正题,在不想分离 state 的情况下,我们可以将组件以 props 的形式传递,一旦 state 发生变化时,若组件自身的 props 未发生变化就不会触发 re-render
,本质上也是一种抽离的思想。
代码如下:
import ReactDOM from 'react-dom/client';
import { useState, } from 'react';
const root = ReactDOM.createRoot(document.getElementById("root"));
// 方式1:采用 children 作为组件。
import ReactDOM from 'react-dom/client';
import { useState, } from 'react';
const root = ReactDOM.createRoot(document.getElementById("root"));
const Parent = () => {
const BaseComponent = ({children}) => {
const [parentName, setParentName] = useState('Jack')
return <>
<div>
<span>Parent name: {parentName}</span>
<button onClick={() => setParentName('Tony')}>Change Parent'name.</button>
</div>
{children}
</>
}
const ChildComponent = () => {
console.log('Init child Component.')
return <>
<div>I am child.</div>
</>
}
return <>
<BaseComponent>
<ChildComponent/>
</BaseComponent>
</>
}
root.render(
<Parent />
);
// 方式2:采用 props 其它字段作为组件传递。
const Parent = () => {
const BaseComponent = ({ bottom }) => {
const [parentName, setParentName] = useState('Jack')
return <>
<div>
<span>Parent name: {parentName}</span>
<button onClick={() => setParentName('Tony')}>Change Parent'name.</button>
</div>
{bottom}
</>
}
const ChildComponent = () => {
console.log('Init child Component.')
return <>
<div>I am child.</div>
</>
}
return <>
<BaseComponent bottom={ChildComponent()}>
</BaseComponent>
</>
}
root.render(
<Parent />
);
关系如图:
效果:
3.3 使用 memo + useMemo/useCallback
memo
用于缓存组件,useMemo
用于缓存数据,useCallback
用于缓存函数。
提示1:
useMemo
也能模拟useCallback
,但俩者的用法有着细微差别,后面会提到。
提示2:memo
的底层实现逻辑也需要维护成本,建议在不能使用 3.1、3.2 的方式情况下再来考虑memo
。
3.3.1 memo
当我们无法让组件分离 state 或以 props/children 的方式时,memo
可以派上用场,当组件内引用 props/state/context 发生变化时才会发生re-render
。
代码如下:
import ReactDOM from 'react-dom/client';
import { useState, memo } from 'react';
const root = ReactDOM.createRoot(document.getElementById("root"));
const ChildMemo = memo(function ChildMemo({ name }) {
console.log('Init component.')
return <>
<h3>Child's name is: {name}</h3>
</>
})
const Parent = () => {
const [childName, setChildName] = useState('Lucy')
const [parentName, setParentName] = useState('Jack')
return <>
<div>
<div>
<input type="text" value={parentName} placeholder='Please enter your name' onChange={(e) => setParentName(e.target.value)} />
</div>
<div>
<button onClick={() => setChildName('Tony')}>Set child name</button>
</div>
</div>
<ChildMemo name={childName} />
</>
}
root.render(
<Parent />
);
关系如图:
效果:
3.3.2 useMemo
上面讲的是缓存组件,那在什么情况下需要缓存数据呢?先思考下这段代码:
知道引用类型的同学应该都知道,当声明两个对象(数据相同)进行比较时,
obj1 === obj2 总是等于 false,于是每次 re-render
的时候,这个函数实际上就会被重新创建一次,因此这里的 memo
将会一直无效。
解决方法就是使用 useMemo 来缓存这份对象:
其中 useCallback 的第二个参数是可选项,可以指定某个依赖项发生变化时再触发 re-render
。
具体代码如下:
import ReactDOM from 'react-dom/client';
import { useState, memo, useMemo } from 'react';
const root = ReactDOM.createRoot(document.getElementById("root"));
const ProfileMemo = memo(function Profile({info}) {
console.log('Init component')
return <>
<div>
Address: {info.address}
</div>
</>
})
const Parent = () => {
const [parentName, setParentName] = useState('Jack')
const info = useMemo(() => ({
address: 'London'
}), [])
return <>
<div>Parent name: {parentName}</div>
<button onClick={() => setParentName('John')}>Change parent's name</button>
<ProfileMemo info={info} />
</>
}
root.render(
<Parent />
);
效果:
3.3.3 useCallback
在什么情况下需要缓存函数呢?首先得明确一点,函数也是对象的一部分,因此两个同样的函数比较时永远为 false ,思考下这段代码:
这里的 ProfileMemo
组件将因为函数的不等价导致再次被 re-render
。
此时缓存函数 useCallback
就可以解决啦,改造后关系图如下:
代码如下:
import ReactDOM from 'react-dom/client';
import { useState, memo, useMemo, useCallback } from 'react';
const root = ReactDOM.createRoot(document.getElementById("root"));
const ProfileMemo = memo(function Profile({info}) {
console.log('Init component')
return <>
<div>
Address: {info.address}
</div>
</>
})
const Parent = () => {
const [parentName, setParentName] = useState('Jack')
const info = useMemo(() => ({
address: 'London'
}), [])
const handSubmit = useCallback(() => {
console.log('Save profile.')
}, [])
return <>
<div>Parent name: {parentName}</div>
<button onClick={() => setParentName('John')}>Change parent's name</button>
<ProfileMemo handSubmit={handSubmit} info={info} />
</>
}
root.render(
<Parent />
);
useCallback
的第二个参数和 useMemo
是一样的,可以指定某个依赖项发生变化时再触发 re-render
。
效果:
3.3.4 useMmeo 模拟 useCallback
useMemo
除了缓存数据也支持缓存函数,以下两种写法的作用是一样的:
const handleSumit = useMemo(() => (
return () => {
console.log('Save profile')
}
), [])
const handSubmit = useCallback(() => {
console.log('Save profile.')
}, [])
唯一区别是, useMemo
共包裹两层函数,官方也建议 useCallback
作为缓存函数首选。
3.4 提取 context
经过前几章的学习,对如何避免 re-render
我们其实已经有了一个框架和概念。
比如 3.1 章节,我们也可以将变化 context
的部分提取出来,与其它组件保持隔离。
具体效果不演示了,大家可自行练习。
四、避免 re-render 的几个注意事项
4.1 memo 不要与变更父级 state 数据的子组件放一起
思考下这段代码:当子组件更改外部的 state 时,ChildMemo
组件是否会触发 re-render
?
答案是会的!而且 input 的每一次更改外部 state 时,ChildMemo 都将被重新 re-render
。
为什么?
这里会涉及到作用域
的概念,当前 BaseCmp
与 state
并不在同个作用域,它改变了外部 state 作用域的值,
根据 2.2 章节讲到,当 state
数据发生变化时,那些引用过 state 的组件都将被 re-render
,而 ChildMemo
刚好与 BaseCmp 处于同个作用域,因此也就一并被 re-render
。
解决的方式有几种:
-
将 ChildMemo 提取出来,与 BaseCmp 隔离。
-
将外部的 state 转成内部 state ,保持同级作用域。
-
自行发挥~
结论:同一作用域的 state 发生变化时,memo 在没有引入该 state 的情况下不会受到影响,
若 memo 放在引入外部 state 的子组件时,当 state 发生变化时则受到影响,它并不能阻止 re-render
。
以上便是有关 re-render
的所有内容,如有问题,欢迎补充。
完!