141 lines
3.3 KiB
JavaScript
141 lines
3.3 KiB
JavaScript
/**
|
||
* 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 = "加载用户";
|
||
}
|
||
});
|