各位React开发者,大家好!
在上一篇文章中,我们探讨了重新渲染的机制,以及如何通过"将状态下移"来解决性能问题。我们通过将状态隔离到更小的组件中,解决了"慢速模态框"问题,同时让应用程序的其余部分保持原状。
但当你无法将状态下移时会发生什么?
想象你仍是那家FAANG公司的开发者。刚优化完模态框,产品经理又提出新需求:要在整个主内容区域外包裹一个"滚动进度条"模块。
问题
要求非常严格:
- 一个追踪滚动位置的区块。
- 它包裹着
VerySlowComponent、BunchOfStuff和OtherStuffAlsoComplicated。 - 当用户滚动时,包装器内部的小型UI元素需根据滚动位置进行位移/动画效果。
你的第一反应可能是用带onScroll处理器的div包裹所有内容(或尝试将逻辑隐藏在自定义钩子中,但这通常无济于事):
const App = () => {
// We need this state for the moving block
const [scrollPosition, setScrollPosition] = useState(0);
const handleScroll = (e) => {
setScrollPosition(e.target.scrollTop);
};
return (
<div className="scrollable-block" onScroll={handleScroll}>
<MovingBlock position={scrollPosition} />
{/* 😱 These will re-render on EVERY scroll event! */}
<VerySlowComponent />
<BunchOfStuff />
<OtherStuffAlsoComplicated />
</div>
);
};
运行此代码时,应用程序将出现卡顿现象。每次滚动像素都会触发 App 组件的状态更新。由于 App 组件重新渲染,React 会重新渲染其内部所有内容。由于 div 包裹了内容,"状态下移"的技巧在此难以奏效。状态需要置于较高层级。
React.memo 是唯一的解决途径吗?并非如此。存在一种更优雅的组合模式,它依赖于理解组件与元素的本质区别。
解决方案:子节点属性
让我们将滚动逻辑提取到独立组件中,但稍作调整:不再硬编码内部的低效组件,而是将其作为子节点接受。
const ScrollableWithMovingBlock = ({ children }) => {
const [position, setPosition] = useState(0);
const handleScroll = (e) => {
setPosition(e.target.scrollTop);
};
return (
<div className="scrollable-block" onScroll={handleScroll}>
<MovingBlock position={position} />
{children}
</div>
);
};
现在,重写 App:
const App = () => {
return (
<ScrollableWithMovingBlock>
{/* These are now passed as props! */}
<VerySlowComponent />
<BunchOfStuff />
<OtherStuffAlsoComplicated />
</ScrollableWithMovingBlock>
);
};
结果:滚动动画流畅如丝。即使这些重量级组件看似位于每秒更新60次的组件内部,它们也不会重新渲染。
这样为什么有效?(深度解析)
要理解这一点,我们需要区分组件(Component) 和 元素(Element)。
组件是一个函数(例如 App 或 ScrollableWithMovingBlock)。
元素是一个描述渲染内容的对象(例如 { type: 'div', props: ... })。
当 App 渲染时,它会创建 <VerySlowComponent /> 及其关联元素。这些 Element 对象通过 children 属性传递给 ScrollableWithMovingBlock。
现在观察渲染时间线:
- 用户滚动:
ScrollableWithMovingBlock更新其位置状态。 - 重新渲染:
ScrollableWithMovingBlock重新执行其函数。 - 返回值:它返回包含新
MovingBlock的div元素。但{children}属性仍使用从App接收的原始引用。 - 协调阶段:React 检查
children属性,询问:"该对象是否发生变化?" - 验证结果:由于
App(拥有者)未重新渲染,children对象在引用上完全相同(===)。React 判定:"无需修改此子树。"
语法糖
需要牢记的是,children 只是其他任何道具一样的存在。"嵌套"语法只是 JSX 的糖衣包装。创建元素只是 JavaScript 表达式,而非语句。这段代码:
<Parent>
<Child />
</Parent>
严格等同于以下内容:
<Parent children={<Child />} />
你甚至可以为 props 的内容或 body 命名。只要 Element 对象在父级作用域(未重新渲染)中创建并传递下去,子级就会被保留。
总结
在上一篇文章中,我们通过将状态下移到子级来优化性能。今天通过将繁重的 UI 作为 props 传递下去来解决问题。
这两种策略都实现了相同的目标:将变化的部分与不变的部分分离。