一、前言
渲染(Render)
React 基于虚拟 DOM 和高效 Diff 算法的完美配合,实现了对 DOM 最小粒度的更新。
react 处理 render 的基本思维模式是每次一有变动就会去重新渲染整个应用。会将 render 函数返回的虚拟 DOM 树与老的进行比较,从而确定 DOM 要不要更新、怎么更新。
何时触发渲染(Render)
- 组件挂载
- setState() 方法被调用 ( 当 setState 传入 null 的时候,并不会触发 render )
二、React.memo()
import React, { useState } from 'react';
import ChildComponent from './ChildComponent';
export default function IndexPage() {
const [ name, setName ] = useState('父组件');
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>
count++: {count}
</button>
<br />
<ChildComponent name={name} />
</div>
);
}
import React, { useState } from 'react';
const ChildComponent = ({ name }) => {
console.log('Child');
const [childCount, setChildCount] = useState(0);
return (
<div>
<button onClick={() => setChildCount(childCount + 1)}>
childCount++ :{childCount}
</button>
</div>
)
}
export default ChildComponent;
子组件中有条 console.log('Child') 语句,每当子组件被渲染时,都会在控制台看到一条打印信息。 这时点击父组件中的 button,会修改 count 变量的值,触发父组件render,此时子组件没有任何变化(props、childCount),但在控制台中仍然看到子组件被渲染的打印信息。
问题:组件在相同 props 的情况下只渲染相同的结果一次,即便父组件渲染,也不要渲染子组件。
解决:子组件用 React.memo() 包裹,如果组件在相同 props 的情况下渲染相同的结果,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。
import React, { memo, useState } from 'react';
const ChildComponent = ({ name }) => {
console.log('Child');
const [childCount, setChildCount] = useState(0);
return (
<div>
<button onClick={() => setChildCount(childCount + 1)}>
childCount++ :{childCount}
</button>
</div>
)
}
export default memo(ChildComponent);
此时再次点击按钮,可以看到控制台没有打印子组件被渲染的信息了。
React.memo 仅检查 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useState,useReducer 或 useContext 的 Hook,当 state 或 context 发生变化时,它仍会重新渲染。
三、React.useCallback()
上面的例子中,父组件只是简单调用子组件,并未给子组件传递任何属性。 看一个父组件给子组件传递属性的例子。
import React, { useState } from 'react';
import ChildComponent from './ChildComponent';
export default function IndexPage() {
const [name, setName] = useState('父组件');
const [count, setCount] = useState(0);
const changeName = (el) => setName(el)
return (
<div>
<button onClick={() => setCount(count + 1)}>
count++: {count}
</button>
<br />
<p>{name}</p>
<ChildComponent name={name} changeName={changeName} />
</div>
);
}
import React, { memo, useState } from 'react';
const ChildComponent = ({ name }) => {
console.log('Child');
const [childCount, setChildCount] = useState(0);
return (
<div>
<button onClick={() => changeName('子组件')}>changeName</button>
<button onClick={() => setChildCount(childCount + 1)}>
childCount++ :{childCount}
</button>
</div>
)
}
export default memo(ChildComponent);
这时点击父组件中的 button,会修改 count 变量的值,触发父组件render,此时调用子组件时 传递了 name 属性和 onClick 属性,在控制台中会看到子组件被渲染的打印信息。 原因:父组件 render 时,会重新创建 changeName() 函数,此时子组件接收的 props 发生改变,所以子组件 React.memo() 检查 props 变更也会 render。
解决:父组件的 changeName() 方法,用 useCallback 钩子函数包裹一层,会把内联回调函数及依赖项数组作为参数传入 useCallback ,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。
import React, { useState } from 'react';
import ChildComponent from './ChildComponent';
export default function IndexPage() {
const [name, setName] = useState('父组件');
const [count, setCount] = useState(0);
const changeName = useCallback((el) => setName(el), [])
return (
<div>
<button onClick={() => setCount(count + 1)}>
count++: {count}
</button>
<br />
<p>{name}</p>
<ChildComponent name={name} changeName={changeName} />
</div>
);
}
三、React.useMemo()
上面例子中,父组件调用子组件时传递的 name 属性是个字符串,若将其换成传递对象:
import React, { useState } from 'react';
import ChildComponent from './ChildComponent';
export default function IndexPage() {
const [name, setName] = useState('父组件');
const [count, setCount] = useState(0);
const obj = { name: '父组件', name2: '子组件' };
const changeName = useCallback((el) => setName(el), [])
return (
<div>
<button onClick={() => setCount(count + 1)}>
count++: {count}
</button>
<br />
<p>{name}</p>
<ChildComponent obj={obj} name={name} changeName={changeName} />
</div>
);
}
import React, { memo, useState } from 'react';
const ChildComponent = ({ name, obj }) => {
console.log('Child');
const [childCount, setChildCount] = useState(0);
return (
<div>
<button onClick={() => changeName('子组件')}>changeName</button>
<button onClick={() => setChildCount(childCount + 1)}>
childCount++ :{childCount}
</button>
</div>
)
}
export default memo(ChildComponent);
影响:点击父组件按钮,父组件 render 时,const obj = { name: '父组件', name2: '子组件' } 一行会重新生成一个新对象,导致传递给子组件的 obj 属性值变化,进而导致子组件重新渲染。
解决:把“obj 对象属性作为参数传入 useMemo ,它仅会在某个依赖项改变时才重新计算 memoized 值。有助于避免在每次渲染时都进行高开销的计算。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。
如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。
import React, { useState } from 'react';
import ChildComponent from './ChildComponent';
export default function IndexPage() {
const [name, setName] = useState('父组件');
const [count, setCount] = useState(0);
const obj = useMemo(() => ({ name: '父组件', name2: '子组件' }), [name,name2])
const changeName = useCallback((el) => setName(el), [])
return (
<div>
<button onClick={() => setCount(count + 1)}>
count++: {count}
</button>
<br />
<p>{name}</p>
<ChildComponent obj={obj} name={name} changeName={changeName} />
</div>
);
}
再次点击父组件按钮,控制台中不再打印子组件被渲染的信息了。
|