优化 Mini React:实现组件级别的精准更新

20 阅读2分钟

在我们自研的 Mini React 框架中,最初每一次状态更新都会导致整颗组件树自顶向下重新渲染。虽然在功能上没有问题,但从性能角度看,这显然是极大的浪费,尤其当我们只需要更新某一个子组件时,却重渲染了所有组件。

本篇文章将带你一步步优化这个过程,实现组件级别的更新调度(Fine-grained Rendering) ,让每个组件可以独立刷新,最大限度地提升渲染性能。


💡 当前问题现状

我们来看一段初始的 App.jsx 代码:

import React from "./core/React.js";
​
let countFoo = 1;
function Foo() {
  console.log("foo rerun");
  function handleClick() {
    countFoo++;
    React.update();
  }
  return <div><h1>foo</h1>{countFoo}<button onClick={handleClick}>click</button></div>;
}
​
let countBar = 1;
function Bar() {
  console.log("bar rerun");
  function handleClick() {
    countBar++;
    React.update();
  }
  return <div><h1>bar</h1>{countBar}<button onClick={handleClick}>click</button></div>;
}
​
let countRoot = 1;
function App() {
  console.log("app rerun");
  function handleClick() {
    countRoot++;
    React.update();
  }
  return <div>hi-mini-react count: {countRoot}<button onClick={handleClick}>click</button><Foo /><Bar /></div>;
}
​
export default App;

❌ 问题:

每当点击任意一个组件内的按钮,都会导致 AppFooBar 全部重新渲染。


🔍 分析问题根源

目前的更新逻辑如下:

function update() {
  nextWorkOfUnit = {
    dom: currentRoot.dom,
    props: currentRoot.props,
    alternate: currentRoot
  };
  wipRoot = nextWorkOfUnit;
}

可以看到,我们每次更新都从 currentRoot 出发,重头开始执行整个工作单元(fiber 树)。


✅ 优化目标:只更新触发的组件

🎯 方法:记录当前组件 Fiber 并通过闭包实现精准更新

我们引入一个 wipFiber 变量,来记录当前正在执行的函数组件 Fiber 节点:

let wipFiber = null;
​
function updateFunctionComponent(fiber) {
  wipFiber = fiber;
  const children = [fiber.type(fiber.props)];
  reconcileChildren(fiber, children);
}

然后改造 React.update 方法为:

function update() {
  let currentFiber = wipFiber;
  return () => {
    console.log(`currentFiber`, currentFiber);
    wipRoot = {
      ...currentFiber,
      alternate: currentFiber
    };
    nextWorkOfUnit = wipRoot;
  };
}

这样,每个组件执行时都会拿到自己独立的 update 方法,封装了当前组件的 Fiber 节点。


✍️ 改造 App.jsx 使用方式

function Foo() {
  console.log("foo rerun");
  const update = React.update(); // 拿到当前组件的更新函数
  function handleClick() {
    countFoo++;
    update(); // 只更新自己
  }
  return <div><h1>foo</h1>{countFoo}<button onClick={handleClick}>click</button></div>;
}

同样方式应用到 BarApp


🧠 更进一步:避免重复执行兄弟节点

我们发现虽然已经可以局部更新,但仍可能会在 workLoop 中重复处理兄弟节点。于是优化 workLoop

function workLoop(deadline) {
  let shouldYield = false;
  while (!shouldYield && nextWorkOfUnit) {
    nextWorkOfUnit = performWorkOfUnit(nextWorkOfUnit);
    
    // 如果 nextWorkOfUnit 与 root 的兄弟节点是同一个,说明重复了
    if (wipRoot?.sibling?.type === nextWorkOfUnit?.type) {
      console.log('hit', wipRoot, nextWorkOfUnit);
    }
​
    shouldYield = deadline.timeRemaining() < 1;
  }
​
  if (!nextWorkOfUnit && wipRoot) {
    commitRoot();
  }
​
  requestIdleCallback(workLoop);
}

📷 效果预览

  • 初始加载只打印一次每个组件
  • 点击 Foo 组件按钮时,仅 Foo 组件 rerun
  • 点击 Bar 时,仅 Bar rerun
  • 完美避开了不必要的全局更新

✅ 总结

通过维护一个当前正在执行的 Fiber 节点并借助闭包传递更新函数,我们成功实现了组件级的局部更新,大大提升了 Mini React 的性能:

  • ✅ 精准更新单个组件
  • ✅ 减少不必要的虚拟 DOM 比对
  • ✅ 构建响应式、可维护的渲染系统基础

这一机制也为后续实现 React 的 useState 等 Hook 特性奠定了良好基础。