'# 助你理解 React 工作的机制——实现一个简单的自定义渲染器
一、背景与问题
React 作为现代前端开发的核心框架,其核心机制中蕴含着大量值得深入研究的底层逻辑。其中,React 渲染器(Renderer)的实现是理解 React 工作原理的关键入口。通过实现一个简单的自定义渲染器,我们可以深入理解虚拟 DOM 的工作原理、diff 算法的实现逻辑,以及 React 如何在不同平台(如 Web、Native)上保持统一的 API。
在实际开发中,我们可能需要自定义渲染器的场景包括:
- 开发表单库时需要控制输入元素的样式
- 构建数据可视化库时需要自定义图表渲染逻辑
- 在特殊环境(如 WebAssembly、Canvas)中实现 DOM 兼容
- 需要与非标准 DOM 接口进行交互
但需要注意,自定义渲染器存在以下限制:
- 性能开销较大,不适合高频更新场景
- 需要处理兼容性问题(如事件系统、样式处理)
- 无法直接使用 React 生态的大部分工具链
二、基本原理
React 的核心机制包含三个关键部分:
- 虚拟 DOM 构造:通过
createElement生成树形结构 - 差异算法:通过 diff 算法计算节点变更
- 渲染器接口:将差异结果应用到真实 DOM
React 的 diff 算法遵循以下原则:
- 比较同层节点类型
- 优先复用相同 key 的节点
- 通过深度优先遍历处理子节点
- 利用双向链表结构优化性能
三、环境准备
# 创建项目目录
mkdir custom-renderer
cd custom-renderer
# 初始化项目
npx create-react-app . --template typescript四、核心实现
1. 虚拟 DOM 节点结构
// src/CustomNode.ts
type CustomNode = {
type: string | null; // 节点类型(null 表示文本)
props: Record<string, any>; // 属性
children: CustomNode[]; // 子节点
key?: string; // 唯一标识
};2. 基础渲染器实现
// src/CustomRenderer.ts
function createElement(
type: string | null,
props: Record<string, any> | null,
children?: any[]
): CustomNode {
const node: CustomNode = {
type,
props: props || {},
children: [],
};
if (children) {
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (child != null) {
node.children.push(
typeof child === 'string'
? createTextElement(child)
: createElement(child)
);
}
}
}
return node;
}
function createTextElement(text: string): CustomNode {
return {
type: null,
props: {},
children: [],
key: Math.random().toString(36).substr(2, 9),
text: text,
};
}3. 核心 diff 算法
// src/DiffAlgorithm.ts
function diff(oldNode: CustomNode, newNode: CustomNode): CustomNode[] {
const changes: CustomNode[] = [];
if (oldNode.type !== newNode.type) {
changes.push({
type: 'remove',
node: oldNode,
});
changes.push({
type: 'add',
node: newNode,
});
return changes;
}
const oldProps = oldNode.props;
const newProps = newNode.props;
// 比较属性
for (const key in newProps) {
if (oldProps[key] !== newProps[key]) {
changes.push({
type: 'propChange',
key,
value: newProps[key],
});
}
}
// 比较子节点
const oldChildren = oldNode.children;
const newChildren = newNode.children;
// 优化:先处理 key 的节点
const keyMap = new Map<string, CustomNode>();
for (const child of oldChildren) {
if (child.key) {
keyMap.set(child.key, child);
}
}
for (const child of newChildren) {
if (child.key) {
const existing = keyMap.get(child.key);
if (existing) {
keyMap.delete(child.key);
// 深度比较子节点
const childChanges = diff(existing, child);
if (childChanges.length > 0) {
changes.push(...childChanges);
}
} else {
changes.push({
type: 'add',
node: child,
});
}
} else {
changes.push({
type: 'add',
node: child,
});
}
}
// 处理剩余未匹配的节点
for (const [key, child] of keyMap.entries()) {
changes.push({
type: 'remove',
node: child,
});
}
return changes;
}五、完整案例
1. 计数器应用实现
// src/App.tsx
import React, { useState, useEffect } from 'react';
interface CustomNode {
type: string | null;
props: Record<string, any>;
children: CustomNode[];
key?: string;
}
const App: React.FC = () => {
const [count, setCount] = useState(0);
// 自定义渲染器
const render = (node: CustomNode) => {
if (node.type === null) {
return <span>{node.text}</span>;
}
const { type, props, children } = node;
const Component = type;
return (
<Component {...props}>
{children.map((child, index) => render(child))}
</Component>
);
};
return (
<div>
<h1>Custom Renderer Demo</h1>
<div>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<p>Count: {count}</p>
</div>
<div>
<CustomComponent count={count} />
</div>
</div>
);
};
// 自定义组件
const CustomComponent: React.FC<{ count: number }> = ({ count }) => {
const [value, setValue] = useState(count);
useEffect(() => {
setValue(count);
}, [count]);
return (
<div>
<h2>Custom Component</h2>
<p>Value: {value}</p>
</div>
);
};
export default App;2. 自定义渲染器实现
// src/CustomRenderer.ts
function renderCustom(node: CustomNode, container: HTMLElement) {
const { type, props, children } = node;
if (type === null) {
const textNode = document.createTextNode(node.text);
container.appendChild(textNode);
return textNode;
}
const element = document.createElement(type);
// 设置属性
for (const key in props) {
const value = props[key];
if (key === 'children') continue;
if (key.startsWith('on') && typeof value === 'function') {
element.addEventListener(key.substring(2).toLowerCase(), value);
} else {
element.setAttribute(key, value);
}
}
// 渲染子节点
const fragment = document.createDocumentFragment();
for (const child of children) {
const childNode = renderCustom(child, fragment);
fragment.appendChild(childNode);
}
element.appendChild(fragment);
container.appendChild(element);
return element;
}六、源码解析
1. 虚拟 DOM 构造
function createElement(
type: string | null,
props: Record<string, any> | null,
children?: any[]
): CustomNode {
const node: CustomNode = {
type,
props: props || {},
children: [],
};
if (children) {
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (child != null) {
node.children.push(
typeof child === 'string'
? createTextElement(child)
: createElement(child)
);
}
}
}
return node;
}这段代码实现了虚拟 DOM 的构造,通过递归处理子节点。特别需要注意:
- 对文本节点的特殊处理(
createTextElement) - 对 children 的遍历处理
- 属性的类型判断(直接属性 vs 事件处理)
2. 差异算法实现
function diff(oldNode: CustomNode, newNode: CustomNode): CustomNode[] {
const changes: CustomNode[] = [];
if (oldNode.type !== newNode.type) {
changes.push({
type: 'remove',
node: oldNode,
});
changes.push({
type: 'add',
node: newNode,
});
return changes;
}
const oldProps = oldNode.props;
const newProps = newNode.props;
// 比较属性
for (const key in newProps) {
if (oldProps[key] !== newProps[key]) {
changes.push({
type: 'propChange',
key,
value: newProps[key],
});
}
}
// 比较子节点
const oldChildren = oldNode.children;
const newChildren = newNode.children;
// 优化:先处理 key 的节点
const keyMap = new Map<string, CustomNode>();
for (const child of oldChildren) {
if (child.key) {
keyMap.set(child.key, child);
}
}
for (const child of newChildren) {
if (child.key) {
const existing = keyMap.get(child.key);
if (existing) {
keyMap.delete(child.key);
// 深度比较子节点
const childChanges = diff(existing, child);
if (childChanges.length > 0) {
changes.push(...childChanges);
}
} else {
changes.push({
type: 'add',
node: child,
});
}
} else {
changes.push({
type: 'add',
node: child,
});
}
}
// 处理剩余未匹配的节点
for (const [key, child] of keyMap.entries()) {
changes.push({
type: 'remove',
node: child,
});
}
return changes;
}这段代码实现了核心的 diff 算法:
- 先比较节点类型
- 然后比较属性变更
- 最后进行子节点的深度比较
- 使用 key 优化节点匹配
七、进阶使用
1. 与 React 生态集成
// src/CustomRenderer.ts
function renderCustom(node: CustomNode, container: HTMLElement) {
const { type, props, children } = node;
if (type === null) {
const textNode = document.createTextNode(node.text);
container.appendChild(textNode);
return textNode;
}
const element = document.createElement(type);
// 设置属性
for (const key in props) {
const value = props[key];
if (key === 'children') continue;
if (key.startsWith('on') && typeof value === 'function') {
element.addEventListener(key.substring(2).toLowerCase(), value);
} else {
element.setAttribute(key, value);
}
}
// 渲染子节点
const fragment = document.createDocumentFragment();
for (const child of children) {
const childNode = renderCustom(child, fragment);
fragment.appendChild(childNode);
}
element.appendChild(fragment);
container.appendChild(element);
return element;
}2. 性能优化方案
// src/PerformanceOptimizations.ts
function batchRender(operations: () => void) {
if (isBatching) {
batchedOperations.push(operations);
} else {
operations();
isBatching = true;
requestAnimationFrame(() => {
batchedOperations.forEach(op => op());
isBatching = false;
});
}
}八、性能与工程实践
1. 性能优化方法
- 使用
requestAnimationFrame进行批量更新 - 使用
useMemo和useCallback避免不必要的重计算 - 对高频更新的组件进行防抖处理
- 对复杂结构使用
key属性优化 diff 算法
2. 安全风险分析
- XSS 攻击:需要对用户输入进行转义处理
- 事件劫持:需规范事件处理函数的注册方式
- 命名冲突:需避免与原生 DOM 属性冲突
九、常见问题与踩坑
1. 典型错误示例
// 错误示例:未处理文本节点
const node = {
type: null,
props: {},
children: ['Hello World'],
};问题:未正确处理文本节点,导致渲染异常
解决:应使用 createTextElement 创建文本节点
2. 常见错误解决方案
- 未处理文本节点:使用
createTextElement创建 - 未正确处理 key 属性:在 diff 算法中使用 key 优化
- 未处理事件委托:需规范事件处理函数的注册方式
十、最佳实践
- 对复杂结构使用
key属性 - 对高频更新的组件进行防抖处理
- 使用
useMemo和useCallback优化性能 - 对用户输入进行转义处理
- 避免直接操作 DOM,使用 React 的 API
十一、总结
通过实现一个简单的自定义渲染器,我们深入理解了 React 的核心机制。虚拟 DOM 的构建、diff 算法的实现、渲染器接口的设计是 React 可移植性的关键。在实际开发中,我们需要根据具体场景选择合适的实现方式,既要充分利用 React 的能力,又要避免不必要的性能开销。
自定义渲染器的实现虽然复杂,但其核心思想与 React 的设计哲学保持一致。理解这些原理,不仅能帮助我们更好地使用 React,还能在需要时构建定制化的解决方案。在实际开发中,我们需要权衡性能、兼容性和开发效率,选择最适合项目需求的实现方案。