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

  1. 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.

  1. 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

  1. 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
  1. 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
  1. 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
  1. 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

  1. Create A DOM element and mount the DOM element to the parent node
  2. Mount the props property onto the DOM node
  3. 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!!!!!