大家好,我是一个刚刚吃完 肯德基 🐔 的 前端开发者 村头一只鹅鹅 😉
感谢阅读我写的文章,你的支持是我更新的动力
1、背景
最近阅读了《Vue.js 设计与实现》这本书,不得不佩服大佬太厉害了,膜拜,膜拜,膜拜。
阅读了前3章之后,看到了 渲染器 与 编译器 这一块,我就按照自己的思路来实现一个很简单的渲染器,来加深印象
2、代码实现
javascript
// utils
const isArray = Array.isArray;
const isObject = (value) => value && typeof value === "object";
const isVNode = (value) => value && value.tag;
const appendText = (element, text) =>
element.appendChild(document.createTextNode(text));
const returnVNode = (tag, props, children) => ({
tag,
props,
children,
});
// 处理 props
const propsTree = [
// 处理 style
[
(key) => key === "style",
(params) => {
const { element, propsValue } = params;
Reflect.ownKeys(propsValue).forEach((key) => {
element.style[key] = propsValue[key];
});
},
],
// 处理 class
[
(key) => key === "class",
(params) => {
const { element, propsValue } = params;
if (typeof propsValue === "string") {
element.className = propsValue;
} else if (isObject(propsValue)) {
Reflect.ownKeys(propsValue).forEach((key) => {
if (propsValue[key]) element.classList.add(key);
});
}
},
],
// 处理 事件
[
(key) => /^on/.test(key),
(params) => {
const { element, propsValue, key } = params;
element.addEventListener(key.slice(2).toLocaleLowerCase(), propsValue);
},
],
];
const strategiesHandler = (strategies, flag, params) => {
const target = strategies.find((item) => item[0](flag));
if (target) {
return target[1](params);
} else {
throw new Error("error, don't find");
}
};
// h函数
function h(type, propsOrChildren, children) {
const argLength = arguments.length;
if (argLength === 2) {
if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
// 第二个参数是 vnode
if (isVNode(propsOrChildren)) {
return returnVNode(type, null, [propsOrChildren]);
}
// 第二个参数为 props
return returnVNode(type, propsOrChildren);
} else {
// 第二个参数为 vnode
return returnVNode(type, null, propsOrChildren);
}
} else {
return returnVNode(type, propsOrChildren, children);
}
}
// 挂载元素
function mountElement(vnode, container) {
const element = document.createElement(vnode.tag);
// 处理 props
for (const key in vnode.props) {
strategiesHandler(propsTree, key, {
key: key,
element: element,
propsValue: vnode.props[key],
});
}
// 处理 children
if (typeof vnode.children === "string") {
appendText(element, vnode.children);
} else if (isObject(vnode.children) && !isArray(vnode.children)) {
renderer(vnode.children, element);
} else if (isArray(vnode.children)) {
vnode.children.forEach((child) => {
typeof child === "string"
? appendText(element, child)
: renderer(child, element);
});
}
container.appendChild(element);
}
// 挂载组件
function mountComponent(vnode, container) {
const subtree = vnode();
renderer(subtree, container);
}
// 渲染函数
function renderer(vnode, container) {
if (typeof vnode.tag === "string") {
mountElement(vnode, container);
} else if (typeof vnode === "function") {
mountComponent(vnode, container);
}
}
javascript
// 示例
// Hello 组件
const Hello = () => {
return h("h3", "我是 Component - Hello");
};
const vnode = h(
"div",
{
class: "main",
style: { backgroundColor: "#5c7cfa", width: "300px", padding: "10px" },
},
[
h("h1", { style: { color: "#fa5252" } }, "Hello Vue!"),
Hello,
h(
"button",
{
onClick: function () {
console.log("这是一个虚拟组件");
},
},
"点击我吧~"
),
h("h4", "渲染器实现一部分"),
]
);
const idEl = document.querySelector("#app");
renderer(vnode, idEl);
3、思路解读
3.1 h
1、首先,我们要编写一个h
函数。这个函数的参数列表,我们可以直接在官方网站上找到。使用这个h
函数,我们可以输出一个类似{ tag: "div", props: { ... }, children: [ ... ] }
形式的对象,这就是 虚拟DOM。通过操作这些对象,我们可以实现页面的更新,再加上diff算法,响应式等处理方式,就是一个简化版本的Vue了
2、h
函数的编写,我认为其中难的部分是如何用最好的方式判断 vnode 的输出形式。我认为最关键的是isVNode
函数的实现。在Vue的源码中,判断一个节点是否为vnode是通过检查它是否拥有__v_isVNode
这个属性。目前,我通过判断是否存在tag
参数来简化这一判断过程,这样不准确
3.2 renderer
1、我们来看看renderer
函数。这个函数会判断传入的节点是否为组件。在Vue中,组件可以是函数或对象,它们经过编译后会转化为虚拟节点(vnode)进行渲染。为了简化处理,我们在这里假设组件都是函数形式的。renderer
函数会识别这些函数组件,并调用它们,将返回的vnode纳入渲染流程中。这样,无论是普通的vnode还是组件,都能通过renderer
函数得到妥善处理。
2、接下来,mountElement
函数会根据虚拟节点(vnode)来创建真实的DOM元素。在处理过程中,首先会处理节点的属性(props)。为了提高代码的可维护性,我采用了策略模式来处理props,避免了大量的条件判断语句(if语句)的堆积。处理完props后,接下来会处理子节点(children)。根据vnode的结构,递归地创建并挂载这些子节点对应的DOM元素。
3、还实现了一个mountComponent
函数。这个函数的本质就是执行组件函数,并输出虚拟节点(vnode)以供渲染。Vue的实现方式更为复杂,能够支持向组件传递props等参数。这里,我就只服务渲染功能的实现,采用了简化。通过mountComponent
函数,我们可以将组件函数转化为可渲染的vnode,从而实现了组件的挂载和渲染
4、总结
这样我们就实现了一个很简单的渲染器了,下面就是实现的页面,并且含有事件监听
如果要写完整的实现,还需要考虑很多其他因素,比如组件的props传递、插槽机制、生命周期钩子、指令系统等等,当前的主要聚焦是渲染功能的实现。下图是这一节的核心思想
写原创文章不易!!!如果讲的好,请给出你手动的点赞👍👍👍,如果文章有问题,也可以在评论区留言哦!