实现渲染器的基本步骤
-
- 通过h函数生成虚拟DOM(vnode)
-
- 通过mount函数将DOM挂载到页面中去
-
- 通过patch函数对比新旧节点之间的差别,重新生成新的DOM挂在到页面
创建h函数
- h函数的主要用处为生成虚拟dom,虚拟dom即包含tag(dom标签名),props(标签的属性,绑定的事件等),childrens(主要包含该节点的字元素可能是文本类型也可能是dom节点)
const h = (tag, props, childrens) => {
return {
tag,
props,
childrens,
};
};
创建mount函数
- 首先要明确mount函数中作了哪些事情
-
- 生成真实的dom节点
-
- 为dom节点绑定属性以及事件等
-
- 遍历所有的子节点,完成1,2两步后添加为父节点的子元素
-
- 将生成的dom挂载到指定的dom节点上
-
// 将虚拟dom映射为真实的dom且挂载到dom中去
const mount = (vnode, container) => {
const { tag, props, childrens } = vnode;
// 创建标签
const el = (vnode.el = document.createElement(tag));
// 处理props
if (props) {
Object.keys(props).forEach((key) => {
if (key.startsWith("on")) {
el.addEventListener(key.slice(2).toLowerCase(), props[key]);
} else {
el.setAttribute(key, props[key]);
}
});
}
// 处理childrens
if (childrens) {
if (typeof childrens === "string") {
el.innerText = childrens;
} else {
childrens.forEach((child) => {
mount(child, el);
});
}
}
// 挂载
container.appendChild(el);
};
创建patch函数
-
patch函数的主要用处为比较新旧节点之间的差异,然后生成新的节点挂载到dom中去,此处咋就简单处理一下
-
-
比较两个节点的标签是否一致:不一致直接将原节点从dom移除,重新挂载新的节点
-
-
- 两个节点的标签一致则比较标签的属性以及事件
-
- 对比后代元素
-
const patch = (v1, v2) => {
// 首先比较两个dom的标签是否一致
if (v1.tag !== v2.tag) {
const v1ParentEl = v1.el.parentElement;
v1ParentEl.removeChild(v1.el);
mount(v2, v1ParentEl);
} else {
// 取出element对象,保存到v2
const el = (v2.el = v1.el);
// 处理新旧节点的props
const newProps = v2.props || {};
const oldProps = v1.props || {};
// 将旧的props中的属性删除,新的props属性添加到元素中
Object.keys(newProps).forEach((key) => {
if (key.startsWith("on")) {
el.addEventListener(key.slice(2).toLowerCase(), newProps[key]);
} else {
el.setAttribute(key, newProps[key]);
}
});
Object.keys(oldProps).forEach((key) => {
if (key.startsWith("on")) {
el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key]);
} else {
el.removeAttribute(key, oldProps[key]);
}
});
// 处理childrens中的元素
const newChilds = v2.childrens;
const oldChilds = v1.childrens;
// 如果newChilds为String
if (typeof newChilds === "string") {
// oldChilds也为String
if (typeof newChilds === "string") {
if (newChilds !== oldChilds) {
el.innerText = newChilds;
}
} else {
el.innerText = newChilds;
}
} else {
// 如果newChilds为Array
if (typeof oldChilds === "string") {
el.innerHTML = "";
newChilds.forEach((item) => {
mount(item, el);
});
} else {
/**
* newChilds: [v1, v2, v3, v4]
* oldChilds: [v1, v6]
* **/
// 首先将前面相同的节点进行patch
const commonLength = Math.min(newChilds.length, oldChilds.length);
for (let i = 0; i < commonLength; i++) {
patch(oldChilds[i], newChilds[i]);
}
// 当newChilds.length > oldChilds.length
if (newChilds.length > oldChilds.length) {
newChilds.slice(commonLength).forEach((item) => {
mount(item, el);
});
} else {
oldChilds.slice(commonLength).forEach((item) => {
el.removeChild(item.el);
});
}
}
}
}
};
写在最后
这样一个简易版的渲染器就实现了,几个函数中都有很多需要处理的边界情况,在这就例举了几种case,只是为了帮助个人学习过程中的消化吸收,更好的理解渲染器的执行过程。
源码
- html部分
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./render.js"></script>
<script>
const vnode1 = h(
"div",
{
id: "wrap",
class: "wrap-v1",
onClick: function () {
alert("Hello mini-vue! vnode1");
},
},
[
h("button", null, "按钮v-1"),
h("button", null, "按钮v-2"),
h("button", null, "按钮v-3"),
]
);
mount(vnode1, document.querySelector("#app"));
const vnode2 = h(
"div",
{
id: "wrap",
class: "wrap-v1",
onClick: function () {
alert("Hello mini-vue! vnode2");
},
},
[h("div", null, "vnode2"), h("button", null, "按钮")]
);
setTimeout(() => {
patch(vnode1, vnode2);
}, 3000);
</script>
</body>
</html>
- js 部分
// 生成虚拟dom
const h = (tag, props, childrens) => {
return {
tag,
props,
childrens,
};
};
// 将虚拟dom映射为真实的dom且挂载到dom中去
const mount = (vnode, container) => {
const { tag, props, childrens } = vnode;
// 创建标签
const el = (vnode.el = document.createElement(tag));
// 处理props
if (props) {
Object.keys(props).forEach((key) => {
if (key.startsWith("on")) {
el.addEventListener(key.slice(2).toLowerCase(), props[key]);
} else {
el.setAttribute(key, props[key]);
}
});
}
// 处理childrens
if (childrens) {
if (typeof childrens === "string") {
el.innerText = childrens;
} else {
childrens.forEach((child) => {
mount(child, el);
});
}
}
// 挂载
container.appendChild(el);
};
// 更新dom
const patch = (v1, v2) => {
// 首先比较两个dom的标签是否一致
if (v1.tag !== v2.tag) {
const v1ParentEl = v1.el.parentElement;
v1ParentEl.removeChild(v1.el);
mount(v2, v1ParentEl);
} else {
// 取出element对象,保存到v2
const el = (v2.el = v1.el);
// 处理新旧节点的props
const newProps = v2.props || {};
const oldProps = v1.props || {};
// 将旧的props中的属性删除,新的props属性添加到元素中
Object.keys(newProps).forEach((key) => {
if (key.startsWith("on")) {
el.addEventListener(key.slice(2).toLowerCase(), newProps[key]);
} else {
el.setAttribute(key, newProps[key]);
}
});
Object.keys(oldProps).forEach((key) => {
if (key.startsWith("on")) {
el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key]);
} else {
el.removeAttribute(key, oldProps[key]);
}
});
// 处理childrens中的元素
const newChilds = v2.childrens;
const oldChilds = v1.childrens;
// 如果newChilds为String
if (typeof newChilds === "string") {
// oldChilds也为String
if (typeof newChilds === "string") {
if (newChilds !== oldChilds) {
el.innerText = newChilds;
}
} else {
el.innerText = newChilds;
}
} else {
// 如果newChilds为Array
if (typeof oldChilds === "string") {
el.innerHTML = "";
newChilds.forEach((item) => {
mount(item, el);
});
} else {
/**
* newChilds: [v1, v2, v3, v4]
* oldChilds: [v1, v6]
* **/
// 首先将前面相同的节点进行patch
const commonLength = Math.min(newChilds.length, oldChilds.length);
for (let i = 0; i < commonLength; i++) {
patch(oldChilds[i], newChilds[i]);
}
// 当newChilds.length > oldChilds.length
if (newChilds.length > oldChilds.length) {
newChilds.slice(commonLength).forEach((item) => {
mount(item, el);
});
} else {
oldChilds.slice(commonLength).forEach((item) => {
el.removeChild(item.el);
});
}
}
}
}
};