优化 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;

❌ 问题:

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


🔍 分析问题根源

目前的更新逻辑如下:

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>;
}

同样方式应用到 BarApp


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

我们发现虽然已经可以局部更新,但仍可能会在 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 特性奠定了良好基础。

相关推荐
工一木子11 分钟前
URL时间戳参数深度解析:缓存破坏与前端优化的前世今生
前端·缓存
半点寒12W2 小时前
微信小程序实现路由拦截的方法
前端
某公司摸鱼前端3 小时前
uniapp socket 封装 (可拿去直接用)
前端·javascript·websocket·uni-app
要加油哦~3 小时前
vue | 插件 | 移动文件的插件 —— move-file-cli 插件 的安装与使用
前端·javascript·vue.js
小林学习编程3 小时前
Springboot + vue + uni-app小程序web端全套家具商场
前端·vue.js·spring boot
柳鲲鹏3 小时前
WINDOWS最快布署WEB服务器:apache2
服务器·前端·windows
weixin-a153003083164 小时前
【playwright篇】教程(十七)[html元素知识]
java·前端·html
ai小鬼头4 小时前
AIStarter最新版怎么卸载AI项目?一键删除操作指南(附路径设置技巧)
前端·后端·github
一只叫煤球的猫5 小时前
普通程序员,从开发到管理岗,为什么我越升职越痛苦?
前端·后端·全栈
vvilkim5 小时前
Electron 自动更新机制详解:实现无缝应用升级
前端·javascript·electron