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

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

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


💡 当前问题现状

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

tsx 复制代码
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;

❌ 问题:

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


🔍 分析问题根源

目前的更新逻辑如下:

tsx 复制代码
function update() {
  nextWorkOfUnit = {
    dom: currentRoot.dom,
    props: currentRoot.props,
    alternate: currentRoot
  };
  wipRoot = nextWorkOfUnit;
}

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


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

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

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

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

然后改造 React.update 方法为:

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

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


✍️ 改造 App.jsx 使用方式

tsx 复制代码
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>;
}

同样方式应用到 Bar 和 App。


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

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

tsx 复制代码
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 特性奠定了良好基础。

相关推荐
鶴哥只手遮天10 分钟前
Vue 3 核心源码深度剖析系列(v3.5.44)-第 1 章 项目全景:monorepo 架构与工程化体系
前端·vue.js·开源
流星。。方16 分钟前
小团队官网SEO落地:前端语义化与静态生成的工程实践
前端·状态模式
用户5304297657924 分钟前
盘点危化品搬运诊断的6类工具与自处理边界判断
前端
curtain28 分钟前
从古法编程到 Agent:我入行这两年,AI 改变了什么
前端·openai
zyfts36 分钟前
10分钟自制Codex钉钉通知Skill:AI干完活自动钉钉提醒你
前端
大孜然1 小时前
孜然QQ频道自动化程序机器人配置教程
前端
用户469384921551 小时前
AIUI 焦点高亮切换失效排查与规避:为什么按钮不变换形态呢?
前端
派大鑫wink1 小时前
纯前端实现软考中级·软件设计师备考学习网站
前端·学习
kyriewen1 小时前
我扒了 npm 三年数据:AI 没把依赖变胖,是把包变多了
前端·人工智能·程序员
liqingdi4371 小时前
[SWPUCTF 2023 秋季新生赛]Easy SMC
服务器·前端·javascript