Basic steps to implement the renderer
-
- Generate virtual DOM (vNode) from h function
-
- The DOM is mounted to the page using the mount function
-
- Through the patch function to compare the difference between the old and new nodes, a new DOM is generated to hang on the page
Create h function
- Function h is mainly used to generate the virtual DOM. The virtual DOM contains tags (DOM tag names), props (tag attributes, bound events, etc.), and childrens (consisting mainly of the word elements of the node, which may be text types or DOM nodes).
const h = (tag, props, childrens) = > {
return {
tag,
props,
childrens,
};
};
Copy the code
Create the mount function
- The first step is to clarify what the mount function does
-
- Generate real DOM nodes
-
- Bind attributes, events, and so on to dom nodes
-
- Iterate through all the child nodes and add the child elements of the parent node after 1,2, and 2 steps
-
- Mounts the generated DOM to the specified DOM node
-
// Map the virtual DOM to the real DOM and mount it to the DOM
const mount = (vnode, container) = > {
const { tag, props, childrens } = vnode;
// Create a label
const el = (vnode.el = document.createElement(tag));
/ / processing 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]); }}); }/ / deal with childrens
if (childrens) {
if (typeof childrens === "string") {
el.innerText = childrens;
} else {
childrens.forEach((child) = >{ mount(child, el); }); }}/ / a mount
container.appendChild(el);
};
Copy the code
Create patch function
-
The patch function is mainly used to compare the differences between the old and new nodes, and then generate new nodes to mount into the DOM
-
-
Compare the labels of two nodes. If they are inconsistent, remove the original node from the DOM and mount the new node
-
-
- If the tags of two nodes are consistent, the attributes and events of the tags are compared
-
- Contrast descendant element
-
const patch = (v1, v2) = > {
// Compare the two DOM tags to see if they match
if(v1.tag ! == v2.tag) {const v1ParentEl = v1.el.parentElement;
v1ParentEl.removeChild(v1.el);
mount(v2, v1ParentEl);
} else {
// Take the Element object and save it to v2
const el = (v2.el = v1.el);
// Handle props for old and new nodes
const newProps = v2.props || {};
const oldProps = v1.props || {};
// Remove the properties in the old props, and add the new props properties to the element
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]); }});// Handle elements in Childrens
const newChilds = v2.childrens;
const oldChilds = v1.childrens;
// If newChilds is String
if (typeof newChilds === "string") {
// oldChilds is also String
if (typeof newChilds === "string") {
if (newChilds !== oldChilds) {
el.innerText = newChilds;
}
} else{ el.innerText = newChilds; }}else {
// If newChilds is Array
if (typeof oldChilds === "string") {
el.innerHTML = "";
newChilds.forEach((item) = > {
mount(item, el);
});
} else {
/** * newChilds: [v1, v2, v3, v4] * oldChilds: [v1, v6] * **/
// Patch the same node first
const commonLength = Math.min(newChilds.length, oldChilds.length);
for (let i = 0; i < commonLength; i++) {
patch(oldChilds[i], newChilds[i]);
}
// when 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); }); }}}}};Copy the code
Write in the last
Such a simple version of the renderer is realized, several functions have a lot of need to deal with the boundary case, in this case, just to help the individual learning process of digestion and absorption, a better understanding of the renderer’s execution process.
The source code
- HTML part
<! 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."Button" v - 1),
h("button".null."Button v - 2"),
h("button".null."Button 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."Button")]);setTimeout(() = > {
patch(vnode1, vnode2);
}, 3000);
</script>
</body>
</html>
Copy the code
- Js part
// Generate the virtual DOM
const h = (tag, props, childrens) = > {
return {
tag,
props,
childrens,
};
};
// Map the virtual DOM to the real DOM and mount it to the DOM
const mount = (vnode, container) = > {
const { tag, props, childrens } = vnode;
// Create a label
const el = (vnode.el = document.createElement(tag));
/ / processing 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]); }}); }/ / deal with childrens
if (childrens) {
if (typeof childrens === "string") {
el.innerText = childrens;
} else {
childrens.forEach((child) = >{ mount(child, el); }); }}/ / a mount
container.appendChild(el);
};
/ / update the dom
const patch = (v1, v2) = > {
// Compare the two DOM tags to see if they match
if(v1.tag ! == v2.tag) {const v1ParentEl = v1.el.parentElement;
v1ParentEl.removeChild(v1.el);
mount(v2, v1ParentEl);
} else {
// Take the Element object and save it to v2
const el = (v2.el = v1.el);
// Handle props for old and new nodes
const newProps = v2.props || {};
const oldProps = v1.props || {};
// Remove the properties in the old props, and add the new props properties to the element
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]); }});// Handle elements in Childrens
const newChilds = v2.childrens;
const oldChilds = v1.childrens;
// If newChilds is String
if (typeof newChilds === "string") {
// oldChilds is also String
if (typeof newChilds === "string") {
if (newChilds !== oldChilds) {
el.innerText = newChilds;
}
} else{ el.innerText = newChilds; }}else {
// If newChilds is Array
if (typeof oldChilds === "string") {
el.innerHTML = "";
newChilds.forEach((item) = > {
mount(item, el);
});
} else {
/** * newChilds: [v1, v2, v3, v4] * oldChilds: [v1, v6] * **/
// Patch the same node first
const commonLength = Math.min(newChilds.length, oldChilds.length);
for (let i = 0; i < commonLength; i++) {
patch(oldChilds[i], newChilds[i]);
}
// when 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); }); }}}}};Copy the code