feat(runtime-core): 添加 Vue 渲染器和 h 函数实现

- 实现了 render 函数用于 Vue 组件渲染
- 实现了 h 函数用于创建虚拟节点(VNode)
- 添加类型定义和相关工具函数
This commit is contained in:
zzy
2026-05-02 16:34:48 +08:00
parent 35b4b22093
commit 5760d63c3a
3 changed files with 158 additions and 0 deletions

View File

@@ -1,4 +1,8 @@
render in vue ./packages/runtime-core/src/renderer.ts
h in vue ./packages/runtime-core/src/h.ts
- stage00 纯html
- stage01 命令式 DOM
- stage02 声明式渲染

14
stage07/index.html Normal file
View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang='zh-cn'>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<title>Stage 07: 全量重新渲染的问题</title>
</head>
<body>
<input type="number" id="userId" placeholder="用户 ID" />
<button id="loadBtn">加载用户</button>
<div id="minivue"></div>
<script src="./index.js"></script>
</body>
</html>

140
stage07/index.js Normal file
View File

@@ -0,0 +1,140 @@
/**
* Stage 07全量重新渲染的问题
* 体验每次都 destroy/create 带来的性能与状态损失
*/
/**
* @typedef {string|number|VNode} VNodeChild
*/
/**
* @typedef {Object} VNode
* @property {string} type
* @property {Object<string, any>} [props]
* @property {VNodeChild[]} [children]
* @property {HTMLElement | null} [el]
*/
/**
* 创建虚拟节点(VNode)
* @param {string} type - 标签名,如 'div'
* @param {Object<string, any> | null} props - 属性或 null
* @param {...(string|VNode)} children - 子节点,字符串会自动转为文本节点
* @returns {VNode}
*/
function h(type, props, ...children) {
const normalizedChildren = children.flat(Infinity);
return {
type,
props: props || {},
children: normalizedChildren,
};
}
/**
* 将 VNode 挂载到真实 DOM递归
* @param {VNode} vnode
* @param {HTMLElement} container
*/
function mount(vnode, container) {
const el = document.createElement(vnode.type);
if (vnode.props) {
for (const key in vnode.props) {
el.setAttribute(key, vnode.props[key]);
}
}
if (vnode.children) {
vnode.children.forEach((child) => {
if (typeof child === "string" || typeof child === "number") {
el.appendChild(document.createTextNode(String(child)));
} else {
mount(child, el);
}
});
}
container.appendChild(el);
}
/**
* 使用数据创建VNode
* @param {{ name: string, age: number, tag: string }} user
* @returns
*/
function constructUserVNode(user) {
return h(
"div",
{ class: "user-card" },
h("h2", null, "姓名: ", user.name),
h("p", null, "年龄: ", String(user.age)),
h("span", null, user.tag),
);
}
/**
* 使用数据创建VNode
* @param {{ name: string, age: number, tag: string }[]} users
* @returns
*/
function constructUserListVNode(users) {
return h(
"div",
{ id: "user-list" },
...users.map((u) => constructUserVNode(u)),
);
}
// 模拟数据库
const multiUser = [
{ name: "小明", age: 18, tag: "学生" },
{ name: "小王", age: 19, tag: "学生" },
{ name: "小李", age: 20, tag: "学生" },
{ name: "小孙", age: 56, tag: "校长" },
{ name: "小赵", age: 43, tag: "老师" },
];
/**
* 模拟异步获取用户数据
* @param {number} id
* @returns {Promise<{ name: string, age: number, tag: string }[]>}
*/
async function getUser(id) {
await new Promise((resolve) => setTimeout(resolve, 500));
const count = Math.min(Math.max(1, id), multiUser.length);
return multiUser.slice(0, count);
}
const root = document.getElementById("minivue");
const loadBtn = /** @type {HTMLButtonElement} */ (
document.getElementById("loadBtn")
);
const userIdInput = /** @type {HTMLInputElement} */ (
document.getElementById("userId")
);
userIdInput.value = "1";
loadBtn.addEventListener("click", async () => {
const rawValue = userIdInput.value.trim();
const id = parseInt(rawValue, 10);
if (isNaN(id) || id < 1) {
alert("请输入一个大于 0 的数字 ID");
return;
}
loadBtn.disabled = true;
loadBtn.textContent = "加载中...";
try {
const users = await getUser(id);
mount(constructUserListVNode(users), root);
} catch (err) {
console.error("加载失败", err);
alert("加载失败,请重试");
} finally {
loadBtn.disabled = false;
loadBtn.textContent = "加载用户";
}
});