purpose
The main purpose of this time is to implement a preliminary simple implementation of the main vNode virtual nodes in the VUe3 and React frameworks, and how to render hanging onto real DOM nodes
Related blog related video
Virtual dom
- Why do vue and React frameworks use virtual DOM?
This has been covered in many articles, reducing unnecessary DOM rendering, improving performance, and using the data binding syntax provided by the framework, developers can just focus on code implementation logic and the like. Here is a blog post to record the virtual DOM(VDOM) at the heart of VUE.
- vnode
Create the returned object using either h or createElement. It is not a real DOM object, but stores all the information about the real DOM Node to be rendered. It tells the framework how to render the real DOM Node and its children. We describe such nodes as Virtual nodes, often abbreviated as vNodes. The virtual DOM is the name we use for the entire VNode tree built from the Vue component tree.
VNode
Here we simply implement the following four types
Type vNodes
- Element
Element corresponds to a normal element and is created using document.createElement. Type is a signature, props is an element attribute, and children is a child element, which can be a string or an array. Is a string, indicating that there is only one text child node.
// Type definition
{
type: string,
props: Object.children: string | Vnode[]
}
/ / for
{
type: 'div'.props: {class: 'a'},
children: 'hello'
}
Copy the code
- Text
Text corresponds to the Text node, which is created using Document.createTextNode. Type should therefore be defined as a Symbol,props is empty, and children is a string that refers to the specific content.
// Type definition
{
type: Symbol.props: null.children: string
}
Copy the code
- Fragment
Fragment is a node that doesn’t actually render. Equivalent to the Fragment in Template and React we use in Vue. Type should be set to a Symbol, props to be empty, and the children node to be an array. The child node is mounted to the Fragment’s parent node when it is finally rendered
// Type definition
{
type: Symbol.props: null.children: Vnode[]
}
Copy the code
- Component
Component is a Component that has its own special rendering method, but the final parameters of the Component are also a collection of the above three vNodes. The type of the component is the object that defines the component, the props is the data that is passed in externally, and the children is the slot of the component.
{
type: Object.props: Object.children: null
}
/ / sample
{
type: {
template: `{{msg}} {{name}}`.props: ['name'].setup(){
return {
msg: 'hello'}}},props: {name: 'world'}}Copy the code
ShapeFlags
Give each Vnode a label for its type and children type. The bit operation is used to identify different Vnode nodes with different left shifts, the | operation is used to identify the type of child nodes it has, and the & operation is used to identify the type and child node types of Vnode, as shown below
//runtime/vnode.js
export const ShapeFlags = {
ELEMENT: 1./ / 00000001
TEXT: 1 << 1./ / 00000010
FRAGMENT: 1 << 2./ / 00000100
COMPONENT: 1 << 3./ / 00001000
TEXT_CHILDREN: 1 << 4./ / 00010000
ARRAY_CHILDREN: 1 << 5./ / 00100000
CHILDREN: (1 << 4) | (1 << 5), / / 00110000
};
Copy the code
We also define two global symbols for Text and Fragment that represent the VNode Text and Fragment types
//runtime/vnode.js
export const Text = Symbol('Text');
export const Fragment = Symbol('Fragment');
Copy the code
h()
implementation
The h function, or createVNodeElement, is a method used to create a Vnode, receiving properties of Type, props, and children, and returning a Vnode object
//runtime/vnode.js
/ * * *@param {string | Object | Text | Fragment} type ;
* @param {Object | null} props
* @param {string |number | Array | null} children
* @returns VNode* /
export function h(type, props, children) {
// Determine the type
let shapeFlag = 0;
// Whether it is a string, identified as Element if it is
if (isString(type)) {
shapeFlag = ShapeFlags.ELEMENT;
// It is a text type
} else if (type === Text) {
shapeFlag = ShapeFlags.TEXT;
} else if (type === Fragment) {
// Whether the type is Fragment
shapeFlag = ShapeFlags.FRAGMENT;
} else {
// That leaves the Component type
shapeFlag = ShapeFlags.COMPONENT;
}
// Check whether children is a text or an array type
if (isString(children) || isNumber(children)) {
shapeFlag |= ShapeFlags.TEXT_CHILDREN;
children = children.toString();
} else if (isArray(children)) {
shapeFlag |= ShapeFlags.ARRAY_CHILDREN;
}
// Return the VNode object
return {
type,
props,
children,
shapeFlag,
};
}
//utils/index.js
export function isString(target) {
return typeof target === 'string';
}
export function isNumber(target) {
return typeof target === 'number';
}
export function isBoolean(target) {
return typeof target === 'boolean';
}
Copy the code
render()
implementation
Once we have implemented the VNode build, we need to parse the VNode to render the real DOM node and mount it on top of the real DOM node
//runtime/render.js
/** * Mount the virtual DOM node to the real DOM node *@param {Object} vnode
* @param {HTMLElement} Container Dom node of the parent container */
export function render(vnode, container) {
mount(vnode, container);
}
Copy the code
Implement mount() to mount the virtual node VNode to the container, use shapeFlag to determine the VNode type, and then render the VNode into different nodes based on the type
//runtime/render.js
/** * Mount the virtual DOM node to the real DOM node *@param {Object} vnode
* @param {HTMLElement} Container Dom node of the parent container */
function mount(vnode, container) {
// Fetch the node type
const { shapeFlag } = vnode;
// Is an Element type
if (shapeFlag & ShapeFlags.ELEMENT) {
mountElement(vnode, container);
} else if (shapeFlag & ShapeFlags.TEXT) {
// It is a text type
mountTextNode(vnode, container);
// Whether the type is Fragment
} else if (shapeFlag & ShapeFlags.FRAGMENT) {
mountFragment(vnode, container);
} else {
// The rest is of the Component typemountComponent(vnode, container); }}Copy the code
The next step is to implement specific mount methods for each type. First, implement simple text node mount
//runtime/render.js
function mountTextNode(vnode, container) {
// Create a text DOM node
const textNode = document.createTextNode(vnode.children);
// Mount to the real parent container
container.appendChild(textNode);
}
Copy the code
The mountFragment() method is then implemented
//runtime/render.js
function mountFragment(vnode, container) {
// Mount the child node to the fragment parent
mountChildren(vnode, container);
}
function mountChildren(vnode, container) {
const { shapeFlag, children } = vnode;
// Determine whether the child node is a text node
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
mountTextNode(vnode, container);
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// If the child is an array, mount the array elements to the fragment parent using mount
children.forEach((child) = >{ mount(child, container); }); }}Copy the code
The mountElement() method is then mounted
- Create A DOM element and mount the DOM element to the parent node
- Mount the props property onto the DOM node
- Mount child nodes
//runtime/render.js
function mountElement(vnode, container) {
// Fetch the VNode type and property objects
const { type, props } = vnode;
// Create the corresponding DOM element
const el = document.createElement(type);
// Mount the props property on the DOM object
mountProps(props, el);
// Mount the child node
mountChildren(vnode, el);
// Mount itself to the parent node
container.appendChild(el);
}
Copy the code
The mountProps() method is used to mount the props object to a DOM node
//runtime/render.js
{
class: 'a b'.style: {
color: 'red'.fontSize: '14px',},onClick: () = > console.log('click'),
checked: ' '.custom: false
}
Copy the code
Mount the above class, style, and click events to the corresponding DOM node. In this case, we only take the string type of class, style can only be the object type, vue events can only start with on, and the first letter of the event name can be capitalized.
//runtime/render.js
function mountProps(props, el) {
for(const key in props) {
const value = props[value];
switch(key) {
case 'class':
el.className = value;
break;
case 'style':
for(const styleName in value){
el.style[styleName] = value[styleName];
}
break;
default:
if(/^on[^a-z]/.test(key)) {
const eventName = key.slice(-2).toLowerCase();
el.addEventListener(eventName, value);
}
break; }}}Copy the code
But what about the built-in DOM properties of dom elements like checked and the attributes you set yourself like Cutsom? After taking a look at the renderer mount Attributes and DOM Properties handling, we set up the following Settings for both
//runtime/render.js
const domPropsRE = /[A-Z]|^(value | checked | selected | muted | disabled)$/;
function mountProps(props, el) {
for(const key in props) {
const value = props[value];
switch(key) {
case 'class':
el.className = value;
break;
case 'style':
for(const styleName in value){
el.style[styleName] = value[styleName];
}
break;
default:
if(/^on[^a-z]/.test(key)) {
const eventName = key.slice(-2).toLowerCase();
el.addEventListener(eventName, value);
} else if(domPropsRE.test(key)) {
// Determine if it is a special DOM attribute
el[key] = value;
} else {
// If not, use setAttribute()
el.setAttribute(key, value);
}
break; }}}Copy the code
But if you hit the following node
<input type="checkbox" checked />
Copy the code
His props object is:
{ "checked": "" }
Copy the code
It’s going to be an empty string, so I’m going to do that and I’m going to set checked to be an empty string, the empty string is going to be false but checked is going to be true so THERE’s a special check
// Assign to domProp that satisfies the regulars
if(domPropsRE.test(key)) {
// for example {ckecked: ""} and check if its original value is a Boolean
if(value === ' ' && typeof el[key] === 'boolean') {
value = true;
}
el[key] = value;
}
Copy the code
Also consider the case where we want to set custom to false if we pass in the following props
{ "custom": false }
Copy the code
Using setAttribute will change false to “false” and the result will still be true, so we still need to make the following determination and remove custom using removeAttribute
if (domPropsRE.test(key)) {
...
} else {
// For example, a custom attribute {custom: "} should be set to .
{custom: null} apply removeAttribute to
if (value == null || value === false) {
el.removeAttribute(key);
} else{ el.setAttribute(key, value); }}Copy the code
This is a simple way to create and mount the virtual DOM. So let’s test that out by writing some code and first exporting the code
//runtime/index.js
export { h, Text, Fragment } from './vnode';
export { render } from './render';
Copy the code
//index.js
import { render, h, Text, Fragment } from './runtime';
const vnode = h(
'div',
{
class: 'a b'.style: {
border: '1px solid'.fontSize: '14px'.backgroundColor: 'white',},onClick: () = > console.log('click'),
id: 'foo'.checked: ' '.custom: false,
},
[
h('ul'.null, [
h('li', { style: { color: 'red'}},1),
h('li'.null.2),
h('li', { style: { color: 'green'}},3),
h(Fragment, null, [h('li'.null.'4'), h('li')]),
h('li'.null, [h(Text, null.'Hello Wrold')]]),]); render(vnode,document.body);
Copy the code
The results of
OK!!!!!