从头开始构建一个只支持函数式组件和最基本的 useState 功能的简易版 React,不包含diff算法和fiber架构,这两部分后续单独输出。
1. 设计目标
我们将构建一个极简的前端框架,具备以下基本功能:
- 创建虚拟 DOM(VDOM)。
- 渲染虚拟 DOM 到真实 DOM。
- 支持函数式组件。
- 简单的状态管理(
useState)。
2. 创建虚拟 DOM
虚拟 DOM 是 React 的核心之一,它是一个 JavaScript 对象,描述了 UI 结构。React 会先在内存中更新虚拟 DOM,再与实际 DOM 比对,最终高效地更新浏览器的页面。
在我们的简易版中,虚拟 DOM 只是一个普通的 JavaScript 对象。每个元素对象包含:
type: 元素的类型,如div,h1,button等。props: 元素的属性,如className,onclick,style,children等。
这里举一个虚拟dom节点的示例:
{
type: 'div',
props: {
className: 'container',
style: { color: 'blue' },
children: [
{
type: 'h1',
props: {
children: ['你好']
}
},
{
type: 'button',
props: {
onclick: handleClick,
children: ['按钮']
}
}
]
}
}
- 声明
createElement函数用于创建虚拟 DOM。它的第一个参数是元素类型(如div),第二个是属性对象,第三个及以后的参数是子元素(可以是其他元素或文本内容)。
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.flat(),
},
};
}
3. 渲染虚拟 DOM 到实际 DOM
将虚拟 DOM 渲染成实际的 DOM 是 React 的核心功能之一。我们通过 render 函数遍历虚拟 DOM,生成实际 DOM 元素,并将它们插入到页面中。
render函数接受虚拟 DOM 和容器元素,生成真实的 DOM 元素并插入到容器中。
function render(vdom, container) {
// 如果 vdom 是字符串(文本节点),直接创建并插入文本节点
if (typeof vdom === "string") {
container.appendChild(document.createTextNode(vdom));
return;
}
// 创建实际的 DOM 元素
const domNode = document.createElement(vdom.type);
// 遍历虚拟 DOM 的属性(props),并设置到真实 DOM 元素上
for (const [key, value] of Object.entries(vdom.props)) {
if (key === "children") {
// 如果属性是 "children",递归渲染子节点并插入到当前元素
value.forEach(child => render(child, domNode));
} else {
// 否则直接设置 DOM 元素的属性,如 className、id、onclick 等
domNode[key] = value;
}
}
// 将当前生成的真实 DOM 元素添加到容器中
container.appendChild(domNode);
}
4. 简化版 useState 实现
- 每次渲染时,
useState返回当前组件的状态,并在状态更新后重新渲染视图
let componentsState = []; // 存储所有组件的状态
let currentComponentIndex = 0; // 当前组件的索引
function useState(initialValue) {
// 获取当前组件的状态,若没有则使用初始值
const componentState = componentsState[currentComponentIndex] || { state: initialValue };
function setState(newValue) {
// 更新状态并触发重新渲染
componentState.state = newValue;
componentsState[currentComponentIndex] = componentState;
renderApp(); // 重新渲染组件
}
componentsState[currentComponentIndex] = componentState; // 存储状态
currentComponentIndex++; // 更新索引,确保状态独立
return [componentState.state, setState]; // 返回状态和更新函数
}
5. 函数组件
React 中的组件是函数式的,每个组件都可以接收 props,并返回一个虚拟 DOM。
function MyComponent(props) {
const [count, setCount] = useState(0);
return createElement("div", null,
createElement("h1", null, `Count: ${count}`),
createElement("button", { onclick: () => setCount(count + 1) }, "Increment")
);
}
6. 渲染应用
我们使用 renderApp 来启动和更新应用。每次组件状态更新时,renderApp 会触发整个页面的重新渲染。
function renderApp() {
currentComponentIndex = 0; // 每次重新渲染时重置组件索引
const vdom = MyComponent();
const container = document.getElementById("root");
container.innerHTML = ''; // 清空之前的渲染
render(vdom, container);
}
7. 完整的 HTML 文件
完整的 HTML 文件如下所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My React</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
}
button {
padding: 8px 12px;
font-size: 16px;
cursor: pointer;
}
</style>
</head>
<body>
<div id="root"></div>
<script>
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.flat()
}
};
}
function render(vdom, container) {
if (typeof vdom === 'string') {
container.appendChild(document.createTextNode(vdom));
return;
}
const domNode = document.createElement(vdom.type);
for (const [key, value] of Object.entries(vdom.props)) {
if (key === 'children') {
value.forEach(child => render(child, domNode));
} else {
domNode[key] = value;
}
}
container.appendChild(domNode);
}
let componentsState = [];
let currentComponentIndex = 0;
function useState(initialValue) {
const componentState = componentsState[currentComponentIndex] || { state: initialValue };
function setState(newValue) {
componentState.state = newValue;
componentsState[currentComponentIndex] = componentState;
renderApp();
}
componentsState[currentComponentIndex] = componentState;
currentComponentIndex++;
return [componentState.state, setState];
}
function MyComponent(props) {
const [count, setCount] = useState(0);
return createElement(
'div',
null,
createElement('h1', null, `次数: ${count}`),
createElement('button', { onclick: () => setCount(count + 1) }, '点击')
);
}
function renderApp() {
currentComponentIndex = 0;
const vdom = MyComponent();
const container = document.getElementById('root');
container.innerHTML = '';
render(vdom, container);
}
renderApp();
</script>
</body>
</html>