跳到主要内容

精准渲染

2024年03月11日
柏拉文
越努力,越幸运

一、缓存变量


1.1 useMemo

未使用 useMemo 缓存之前: Appnum 的变化导致 App 组件重新渲染, App 函数重新执行, uniqueId 重新生成新的值。造成的现象就是, Component1 组件与 App 中的 num 状态毫无关系, 但是却因为 num 的变化而重新渲染了, 造成渲染浪费。

function getUniqueIdByNanoID(size = 10) {
let id = '';
let i = size;
const urlAlphabet =
'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict';
while (i--) {
id += urlAlphabet[(Math.random() * 64) | 0];
}
return id;
}

function getUniqueId(size) {
return getUniqueIdByNanoID(size);
}

function Component1(props){
const { uniqueId } = props;
console.log("Component1 Render", uniqueId);

return <div> Component 1 组件</div>
}

function App(){
const [num,setNum] = useState(0);
const uniqueId = getUniqueId();

console.log("App Render");

const handClick = ()=>{
setNum(num + 1);
}

return <div>
<h3>App 组件</h3>
<Component1 { uniqueId } />
</div>
}

使用 useMemo 缓存之后: Appnum 的变化导致 App 组件重新渲染, App 函数重新执行。 而 uniqueId 通过 useMemo 包裹进行缓存, 不会生成新的值, 因此, App 中的 num 状态的变化不会导致 Component1 组件重新渲染。

function getUniqueIdByNanoID(size = 10) {
let id = '';
let i = size;
const urlAlphabet =
'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict';
while (i--) {
id += urlAlphabet[(Math.random() * 64) | 0];
}
return id;
}

function getUniqueId(size) {
return getUniqueIdByNanoID(size);
}

function Component1(props){
const { uniqueId } = props;
console.log("Component1 Render", uniqueId);

return <div> Component 1 组件</div>
}

function App(){
const [num,setNum] = useState(0);
const uniqueId = useMemo(()=> getUniqueId(), []);

console.log("App Render");

const handClick = ()=>{
setNum(num + 1);
}

return <div>
<h3>App 组件</h3>
<Component1 { uniqueId } />
</div>
}

二、缓存函数


2.1 useCallback

未使用 useCallback 缓存之前: Appnum 的变化导致 App 组件重新渲染, App 函数重新执行, handleClick 重新生成新的函数。造成的现象就是, Component1 组件与 App 中的 num 状态毫无关系, 但是却因为 num 的变化而重新渲染了, 造成渲染浪费。

function Component1(props){
const { handleClick } = props;
console.log("Component1 Render");

return <div onClick={ handleClick }> Component 1 组件</div>
}

function App(){
const [num,setNum] = useState(0);
console.log("App Render");

const handleClick = ()=>{
setNum(num + 1);
}

return <div>
<h3>App 组件</h3>
<Component1 { handleClick } />
</div>
}

使用 useMemo 缓存之后: Appnum 的变化导致 App 组件重新渲染, App 函数重新执行。 而 handleClick 通过 useCallback 包裹进行缓存, 不会生成新的函数, 因此, App 中的 num 状态的变化不会导致 Component1 组件重新渲染。

function Component1(props){
const { handleClick } = props;
console.log("Component1 Render");

return <div onClick={ handleClick }> Component 1 组件</div>
}

function App(){
const [num,setNum] = useState(0);
console.log("App Render");

const handleClick = useCallback(()=>{
setNum(num + 1);
}, [])

return <div>
<h3>App 组件</h3>
<Component1 { handleClick } />
</div>
}

三、缓存组件


3.1 memo

未使用 memo 缓存之前: Appnum 的变化导致 App 组件重新渲染。造成的现象就是, Component1 组件与 App 中的 num 状态毫无关系, 但是却因为 num 的变化而重新渲染了, 造成渲染浪费。

function Component1(props){
const { num } = props;
console.log("Component1 Render", num);

return <div> Component 1 组件</div>
}

function App(){
const [num,setNum] = useState(0);
console.log("App Render");

const handleClick = ()=>{
setNum(num + 1);
}

return <div>
<h3 onClick={ handleClick }>App 组件</h3>
<Component1 { num: 0 } />
</div>
}

使用 memo 缓存之后: Appnum 的变化导致 App 组件重新渲染, Component1 组件使用 memo 包裹, 内部通过 Object.is 对比前后 num 的值有无变化, 由于 num 始终为 0, 所以 Component1 组件不会重新渲染。

function Component1(props){
const { num } = props;
console.log("Component1 Render", num);

return <div> Component 1 组件</div>
}

const MemoComponent1 = memo(Component1);

function App(){
const [num,setNum] = useState(0);
console.log("App Render");

const handleClick = ()=>{
setNum(num + 1);
}

return <div>
<h3 onClick={ handleClick }>App 组件</h3>
<MemoComponent1 { num: 0 } />
</div>
}

3.2 useMemo

未使用 useMemo 缓存之前: Appnum 的变化导致 App 组件重新渲染。造成的现象就是, Component1 组件与 App 中的 num 状态毫无关系, 但是却因为 num 的变化而重新渲染了, 造成渲染浪费。

function Component1(props){
const { num } = props;
console.log("Component1 Render", num);

return <div> Component 1 组件</div>
}

function App(){
const [num,setNum] = useState(0);
console.log("App Render");

const handleClick = ()=>{
setNum(num + 1);
}

return <div>
<h3 onClick={ handleClick }>App 组件</h3>
<Component1 { num: 0 } />
</div>
}

使用 useMemo 缓存之后: Appnum 的变化导致 App 组件重新渲染, Component1 组件使用 useMemo 缓存, 所以 Component1 组件不会重新渲染。

function Component1(props){
const { num } = props;
console.log("Component1 Render", num);

return <div> Component 1 组件</div>
}

function App(){
const [num,setNum] = useState(0);
console.log("App Render");

const handleClick = ()=>{
setNum(num + 1);
}

const MemoComponent1 = useMemo(()=> <Component1 { num: 0} />, []);

return <div>
<h3 onClick={ handleClick }>App 组件</h3>
{ MemoComponent1 }
</div>
}

四、分离组件


4.1 将变化部分与不变部分分离

未分离之前: View 组件未订阅父组件中的任何状态, 理论上不应该随着 App 状态的改变而渲染。但是现在跟随者 num 状态的改变也发生了不必要的渲染。

function View(){
return <div> 嘻嘻哈哈 </div>
}

function App(){
const [num,setNum] = useState(0);

return <div>
<div>
<button onClick={ ()=> setNum(num+1) }> + 1</button>
<p> num is: { num }</p>
</div>

<View />
</div>
}

分离之后: Num 组件中 num 状态的改变只影响 Num 组件自身, 对 App 组件和 View 组件毫无影响。

function View(){
return <div> 嘻嘻哈哈 </div>
}

function Num(){
const [num,setNum] = useState(0);

return <div>
<button onClick={ ()=> setNum(num+1) }> + 1</button>
<p> num is: { num }</p>
</div>
}

function App(){
return <div>
<Num />
<View />
</div>
}

五、控制组件


5.1 PureComponent

shouldComponentUpdate 原理基本一致,通过对 props state 的浅比较结果来实现 shouldComponentUpdate

if (this._compositeType === CompositeTypes.PureClass) {
shouldUpdate = !shallowEqual(prevProps, nextProps) || ! shallowEqual(inst.state, nextState);
}

shallowEqual 对应方法大致如下:

const hasOwnProperty = Object.prototype.hasOwnProperty;

/**
* is 方法来判断两个值是否是相等的值,为何这么写可以移步 MDN 的文档
* https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/is
*/
function is(x: mixed, y: mixed): boolean {
if (x === y) {
return x !== 0 || y !== 0 || 1 / x === 1 / y;
} else {
return x !== x && y !== y;
}
}

function shallowEqual(objA: mixed, objB: mixed): boolean {
// 首先对基本类型进行比较
if (is(objA, objB)) {
return true;
}

if (typeof objA !== 'object' || objA === null ||
typeof objB !== 'object' || objB === null) {
return false;
}

const keysA = Object.keys(objA);
const keysB = Object.keys(objB);

// 长度不相等直接返回false
if (keysA.length !== keysB.length) {
return false;
}

// key相等的情况下,再去循环比较
for (let i = 0; i < keysA.length; i++) {
if (
!hasOwnProperty.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false;
}
}

return true;
}

5.2 shouldComponentUpdate

通过shouldComponentUpdate生命周期函数来比对 stateprops,确定是否要重新渲染。默认情况下返回 true 表示重新渲染,如果不希望组件重新渲染,返回 false 即可