在我们自研的 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;
❌ 问题:
每当点击任意一个组件内的按钮,都会导致 App
、Foo
和 Bar
全部重新渲染。
🔍 分析问题根源
目前的更新逻辑如下:
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>;
}
同样方式应用到 Bar
和 App
。
🧠 更进一步:避免重复执行兄弟节点
我们发现虽然已经可以局部更新,但仍可能会在 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 特性奠定了良好基础。