【翻译】Elements & Children 属性

12 阅读3分钟

原文链接:shramko.dev/blog/react-…

各位React开发者,大家好!

上一篇文章中,我们探讨了重新渲染的机制,以及如何通过"将状态下移"来解决性能问题。我们通过将状态隔离到更小的组件中,解决了"慢速模态框"问题,同时让应用程序的其余部分保持原状。

但当你无法将状态下移时会发生什么?

想象你仍是那家FAANG公司的开发者。刚优化完模态框,产品经理又提出新需求:要在整个主内容区域外包裹一个"滚动进度条"模块。

问题

要求非常严格:

  • 一个追踪滚动位置的区块。
  • 它包裹着 VerySlowComponentBunchOfStuffOtherStuffAlsoComplicated
  • 当用户滚动时,包装器内部的小型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)

组件是一个函数(例如 AppScrollableWithMovingBlock)。

元素是一个描述渲染内容的对象(例如 { type: 'div', props: ... })。

App 渲染时,它会创建 <VerySlowComponent /> 及其关联元素。这些 Element 对象通过 children 属性传递给 ScrollableWithMovingBlock

现在观察渲染时间线:

  • 用户滚动: ScrollableWithMovingBlock 更新其位置状态。
  • 重新渲染: ScrollableWithMovingBlock 重新执行其函数。
  • 返回值:它返回包含新 MovingBlockdiv 元素。但 {children} 属性仍使用从 App 接收的原始引用。
  • 协调阶段:React 检查 children 属性,询问:"该对象是否发生变化?"
  • 验证结果:由于 App(拥有者)未重新渲染,children 对象在引用上完全相同(===)。React 判定:"无需修改此子树。"

语法糖

需要牢记的是,children 只是其他任何道具一样的存在。"嵌套"语法只是 JSX 的糖衣包装。创建元素只是 JavaScript 表达式,而非语句。这段代码:

<Parent>
  <Child />
</Parent>

严格等同于以下内容:

<Parent children={<Child />} />

你甚至可以为 props 的内容或 body 命名。只要 Element 对象在父级作用域(未重新渲染)中创建并传递下去,子级就会被保留。

总结

在上一篇文章中,我们通过将状态下移到子级来优化性能。今天通过将繁重的 UI 作为 props 传递下去来解决问题。

这两种策略都实现了相同的目标:将变化的部分与不变的部分分离