React避免子组件无效刷新的三种解决方案
前言
一个很常见的场景,React
中父组件和子组件在一起,子组件不依赖于父组件任何数据,但是会一起发生变化。
在探究原理之前,先回忆一下,React
中的Diff算法会将更新前后的两棵虚拟DOM树做对比,但这并不会决定组件是否更新,只会决定是否要复用老的节点。
举个简单的例子:
import { useState } from 'react'; const Child = () => { console.log('child render'); return null; }; const App = () => { const [name, setName] = useState(1); return ( <div onClick={() => setName(2)}> <Child /> </div> ); };
Child
组件没有接收来自父组件的值,每次点击父组件元素让name
更新,Child
组件会更新吗?答案是会的,你一定会好奇,子组件没有接收任何的props
,为什么也会更新呢?
首先,父组件经过了Diff阶段,会判断Child组件是否发生变化,在本案例中Child
内部的元素结构和状态无任何变化,React
还会对比Child
组件前后的props是否相同,在本案例中,前后props
不相同。
说到这里,你一定忍不住了,我都没传props
,为啥不相同?原因是React
内部对于props
的对比只进行了浅层比较,通过 !== 来判断,这样即使没传props
,每次生成的props
对象都是新的指针,即使为空,也会生成不同的props
空对象,就像这样:
const oldProps = current.memoizedProps; // 更新前老的props const newProps = workInProgress.pendingProps; // 待比较更新后的props if (oldProps !== newProps) { didReceiveUpdate = true; // 标记为发生变化,需要更新 }
那有什么方法可以避免这样的无效更新呢?一共有三种方案。
- 使用
React.memo
,可以指定在Diff时对于被memo
包裹的组件只做浅层比较; - 使用
React.useMemo
或React.useCallback
来包住子组件,让每次更新子组件都为同一个JSX对象,这也props
的比较就会相同; - 将子组件作为
children
来传递;
React.memo
对于方案1,React.memo
的原理其实来源于源码中的shallowEqual
函数,该函数会接收两个对象,分别对应老的props
和新的props
,一共有四种比较策略,如果四种策略都通过,则判定新旧为同一个对象,不做更新,复用老的节点。
- 判断两者是否为同一对象,不是同一对象则返回false;
- 判断两者的值不为
object
或为null
,则返回false; - 对比两者key的数量,不一致则返回false;
- 对比两者key的值是否相同,不一致则返回false;
源码如下:
function shallowEqual(objA: mixed, objB: mixed): boolean { // 一样的对象返回true if (Object.is(objA, objB)) { return true; } // 不是对象或者为null返回false if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) { return false; } const keysA = Object.keys(objA); const keysB = Object.keys(objB); // key数量不同返回false if (keysA.length !== keysB.length) { return false; } // 对应key的值不相同返回false for (let i = 0; i < keysA.length; i++) { if (!hasOwnProperty.call(objB, keysA[i]) || !Object.is(objA[keysA[i]], objB[keysA[i]])) { return false; } } return true; }
可以看到浅比较props
的实现原理很简单,对应着上述四种策略。
React.useMemo & React.useCallBack
对于方案2,如果你不了解React.useMemo
和React.useCallback
,没有关系,先看一下这段代码块:
import { useMemo } from 'react'; const Child = () => { console.log('child render'); return null; }; const App = () => { const [name, setName] = useState(1); const child = useMemo(() => <Child />, []); return <div onClick={() => setName(2)}>{child}</div>; };
React.useMemo
接收两个参数,第一个参数为返回值,第二个参数为依赖项,当依赖项数组中的值发生变化,则返回值会重新计算,也就是说第二个依赖项传空数组,则依赖项永远都不会发生变化,则Child
组件经过React.useMemo
包裹后一直不会被React
去计算Diff,就实现了父组件更新,子组件不触发更新。
但对于React.useMemo
的使用,如果传给了子组件的值,但是未声明依赖项,会导致子组件一直不发生变化,就像这样:
import { useMemo } from 'react'; const Child = ({ name }) => { console.log('child render'); return name; }; const App = () => { const [name, setName] = useState(1); const child = useMemo(() => <Child name={name} />, []); return <div onClick={() => setName(2)}>{child}</div>; };
像这种情况,父组件将name
传给了子组件,但是由于子组件未声明name
为改变依赖项,因此当name
发生变化,子组件依然会永远返回初始值1,因此对于React.useMemo
的缓存策略在优化时也需要充分考虑意外事故发生。
向上提炼 & 向下移动
对于方案3,可以简单理解成向上提炼和向下移动state,先看一个案例:
const App = () => { const [color, setColor] = useState('red'); return ( <div> <input value={color} onChange={(e) => setColor(e.target.value)} /> <p style={{ color }}>Hello, world!</p> <Child /> </div> ); };
Input
的onChange
事件是一个频繁触发的颜色指示器,一秒会触发上百次,而Child
组件是一个固定渲染不依赖父组件状态的子组件,如何通过状态向下移动的方式来避免Child
组件被渲染呢?
const App = () => { return ( <div> <Form /> <Child /> </div> ); };
我们只需要将这段性能消耗大的代码抽离到单独的一个Form
组件中,同时把color
状态单独交给Form
组件去管理,这样App
父组件一直没有发生重渲染,Child
子组件也不会被影响,只有Form
子组件在单独发生交互,这种方案更像是一个状态下移 + 隔离
。
还有一种解法就是状态提升,我们可以把这段性能消耗严重的代码同样单独封装成一个组件,将Child
子组件的内容传递给Form子组件,就像这样:
const Form = ({ children }) => { const [color, setColor] = useState('red'); return ( <div style={{ color }}> <input value={color} onChange={(e) => setColor(e.target.value)} /> {children} </div> ); }; const App = () => { return ( <div> <Form> <p>Hello, world</p> <Child /> </Form> </div> ); };
其实思路是和状态向下提升是一样的,把性能消耗严重的一部分单独抽离到一个组件中,将相对不期望被影响的一部分通过特定形式渲染,因此Child
子组件在这种情况也不会被重新渲染。
结尾
本文主要记录了博主在日常开发用到比较多的三种优化策略,微笑的细节差带来的优化提升手段,希望对你有帮助哦~
以上就是React避免子组件无效刷新的三种解决方案的详细内容,更多关于React避免子组件无效刷新的资料请关注脚本之家其它相关文章!
相关文章
利用React Router4实现的服务端直出渲染(SSR)
这篇文章主要介绍了利用React Router4实现的服务端直出渲染(SSR),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧2019-01-01
最新评论