熟悉 React 的同学们都知道,每次数据更新都会重新渲染 fiber 树,匹配渲染优先级组件及其所有子组件都会重新渲染,存在心智负担,可能开发中很多同学会掏出 useMemo,useCallback, memo, pureComponent, sholdComponentUpdate 组合拳来优化避免组件重复渲染。
首先来段示例,当 App 组件触发会影响 Child 重新渲染。
function Child() {
console.log('Child::render');
return <div>child</div>
}
funcction App() {
const [data, setData] = useState(0);
return (
<>
<p>{ data }</p>
<button onClick={handleClick}>click</button>
<Child/>
</>
);
function handleClick() {
setData(data => data + 1);
}
}
有哪些方法可以避免Child渲染呢?
const Child1 = () => {
console.log('Child1::render');
return <div>child</div>
}
const child1 = <Child1/>;
const Child6 = memo(() => {
console.log('Child6::render');
return <div>child6</div>
})
funcction App({ children, render }) {
const [data, setData] = useState(0);
return (
<>
<p>{ data }</p>
<button onClick={handleClick}>click</button>
{ child1 }
{
useMemo(() => {
console.log('Child2::render');
return <div>child2</div>
}, [])
}
{
useState(() => {
console.log('Child3::render');
return <div>child3</div>
})[0]
}
{ children }
{ render }
<Child6/>
</>
);
function handleClick() {
setData(data => data + 1);
}
}
let child5 = () => {
console.log('Child5::render');
return <div>child5</div>
}
<App render={child5}>
{ React.createElement(() => {
console.log('Chil4::render');
return <div>child4</div>
}) }
</App>
// 首次渲染
print -> star
Child2::render
Child3::render
Child1::render
Child4::render
Child6::render
print -> end
通过点击 button 可以观察到控制台并无 Child 打印内容输出, 子组件并没有被重复渲染;
也就说除了常规包裹 useMemo 与 memo 外,还有很多其他的方式也可以达到类似效果;
可能会有同学疑惑(后面会有解释)
children 不是子节点吗,为什么没有重新渲染useState 为什么也可以避免 Child 渲染React 组件会根据 state(useState|setState), props, context 来判断当前 fiber 是否可以复用
找到源码中 beginWork 方法,
beginWork 主要用于生成子 filber 以及打上对应 flags
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const updateLanes = workInProgress.lanes;
// current 存在 update更新
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
// 判断新旧props是否全等 或者 上下文是否有变化
if (
oldProps !== newProps ||
hasLegacyContextChanged()
) {
didReceiveUpdate = true;
// 当渲染优先级不包含workInProgress的优先级,复用旧fiber
} else if (!includesSomeLane(renderLanes, updateLanes)) {
didReceiveUpdate = false;
...
// 复用fiber
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
} else {
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
...
didReceiveUpdate = true;
} else {
...
didReceiveUpdate = false;
}
}
} else {
didReceiveUpdate = false;
}
乍一看不是很合理吗,那么为何App组件更新会导致Child组件刷新呢?
首先我们得知道workInProgress.pendingProps到底是啥?
// 源码中
const pendingProps = element.props;
// 那么这个element又是啥呢?
// 格式如下
{
_owner:null
_store:{validated: true}
$$typeof:Symbol(react.element)
key:null
props:{}
ref:null
type:() => {\\n console.log('Child::RENDER');\\n return /*#__PURE...
}
// 是不是很熟悉,这就是常写得Jsx数据
// 可以打印看看
const Child1 = () => {
console.log('Child1::render');
return <div>child</div>;
};
const child1 = <Child1 />;
console.log(child1);
// 换个方式可以证明
let props1 = child1.props;
let props2 = child1.props;
console.log(props1 === props2);
print -> true
// props3 与 props4 生成得Jsx 不是同一引用因此不相等
let props3 = (<Child1 />).props;
let props4 = (<Child1 />).props;
console.log(props3 === props4);
print -> false
// 这个概念类似
const a = { "test": 1 };
const b = { "test": 1'};
console.log(a === b);
print -> false
const c = a; // "c" 仅仅是 "a" 的引用
console.log(a === c);
print -> true