If you have any thoughts on my blog or articles and you want to let me know, you can either post a comment below(public) or tell me via this i_kkkp@163.com

Implementation Principles of Vue.js Components

Preface

In a previous discussion, we explored the Vue renderer, where the renderer is primarily responsible for rendering the virtual DOM into the real DOM. We only need to use the virtual DOM to describe the final content to be rendered.

However, when creating more complex pages, the code for the virtual DOM used to describe the page structure becomes larger, or we can say that the page template becomes more extensive. In such cases, the ability to modularize components becomes essential.

With components, we can break down a large page into multiple parts, each part serving as an independent component. These components collectively form the complete page. The implementation of components also requires support from the renderer.

Rendering Components

From the user’s perspective, a stateful component is simply an options object, as shown in the code below:

// MyComponent is a component, and its value is an options object
const MyComponent = {
  name: 'MyComponent',
  data() {
    return { foo: 1 }
  }
}

However, from the internal implementation of the renderer, a component is a special type of virtual DOM node. For example, to describe a regular tag, we use the vnode.type property of the virtual node to store the tag name, as shown in the code below:

// This vnode is used to describe a regular tag
const vnode = {
  type: 'div'
  // ...
}

To describe a fragment, we set the vnode.type property of the virtual node to ‘Fragment’, and to describe text, we set the vnode.type property to ‘Text’.

Do you recall the patch function we discussed earlier in the renderer?

In the patch function, Vue processes different logic based on the differences between the new and old virtual DOM nodes. If it’s the initial rendering, it executes the logic for the initial rendering; if it’s an update rendering, it updates the actual DOM nodes based on the differences.

function patch(n1, n2, container, anchor) {
  // 02. If the types of new and old nodes are different, perform unmount operation
  if (n1 && n1.type !== n2.type) {
    unmount(n1);
    n1 = null;
  }

  // 07. Get the node type
  const { type } = n2;

  // 09. Check the node type
  if (typeof type === 'string') {
    // 10. Process regular elements
    // TODO: Logic for regular elements
  } else if (type === Text) {
    // 11. Process text nodes
    // TODO: Logic for text nodes
  } else if (type === Fragment) {
    // 13. Process fragments
    // TODO: Logic for fragments
  }

  // Return the actual DOM element of the new node
  return n2.el;
}

As you can see, the renderer uses the vnode.type property of the virtual node to differentiate its type. Different types of nodes require different methods for mounting and updating.

In reality, this applies to components as well. To use virtual nodes to describe components, we can use the vnode.type property to store the component’s options object.

function patch(n1, n2, container, anchor) {
  // 02. If the types of new and old nodes are different, perform unmount operation
  if (n1 && n1.type !== n2.type) {
    unmount(n1);
    n1 = null;
  }

  // 07. Get the node type
  const type = n2.type;

  // 09. Check the node type
  if (typeof type === 'string') {
    // 10. Process regular elements
    // TODO: Logic for regular elements
  } else if (type === Text) {
    // 11. Process text nodes
    // TODO: Logic for text nodes
  } else if (type === Fragment) {
    // 13. Process fragments
    // TODO: Logic for fragments
  } else if (typeof type === 'object') {
    // 16. The value of vnode.type is an options object, treated as a component
    if (!n1) {
      // 17. Mount the component
      mountComponent(n2, container, anchor);
    } else {
      // 21. Update the component
      patchComponent(n1, n2, anchor);
    }
  }

  // Return the actual DOM element of the new node
  return n2.el;
}

In the above code, we added an else if branch to handle the case where the vnode.type property value is an object, treating it as a virtual node describing a component. We call the mountComponent and patchComponent functions to mount and update the component.

Now let’s focus on the basics of writing components—what the user should write when creating components, what the options object of a component must include, and what capabilities a component possesses.

Therefore, a component must include a rendering function, i.e., the render function, and the return value of the render function should be a virtual DOM. In other words, the render function of a component is an interface for describing the content that the component renders, as shown in the code below:

// Definition of the MyComponent component
const MyComponent = {
  // Component name, optional
  name: 'MyComponent',

  // Rendering function of the component, its return value must be a virtual DOM
  render() {
    // Return a virtual DOM
    return {
      type: 'div',
      children: ['text content']
    };
  }
};

// Vnode object describing the component, type property value is the options object of the component
const CompNode = {
  type: MyComponent
};

// Call the renderer to render the component
renderer.render(CompNode, document.querySelector('#app'));

// The mountComponent function in the renderer is responsible for the actual rendering of the component
function mountComponent(vnode, container, anchor) {
  // Get the options object of the component, i.e., vnode.type
  const componentOptions = vnode.type;

  // Get the rendering function of the component, render
  const render = componentOptions.render;

  // Execute the rendering function, get the content the component should render, i.e., the virtual DOM returned by the render function
  const subTree = render();

  // Finally, call the patch function to mount the content described by the component, i.e., subTree
  patch(null, subTree, container, anchor);
}

Conclusion

Let’s recap the basics of components. We explicitly declared an object instance for a component, and the options object of the component must include a rendering function, i.e., the render function. The return value of the render function should be a virtual DOM. We then call render and patch to update and replace components. **In other words, the render function of a component is an interface for describing

Implementation of Vue.js Component Events and Emit Browser Default Behaviors

Comments