(a) Reading must see

Technology stack: Typescript + Jest + Rollup

The code in the article is not complete code, for the convenience of understanding to make appropriate simplification, detailed code visible warehouse.

Github:github.com/Merlin218/m… Gitee:gitee.com/merlin218/m… Learning reference: github.com/cuixiaorui/… Cui big mini – vue

In retrospect, we completed the relevant implementation of VUE responsiveness. In this installment, we will complete the core part of VUE at runtime and build a VUE application step by step.

Past the link

[Handwritten VUE3 series] responsive implementation: juejin.cn/post/702861…

(2) Preparation of raw materials/tools

(1) Raw materials

Index.html: container for Vue applications. We usually mount Vue applications on the node of “#app”.

 <! DOCTYPEhtml>
 <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="./main.js" type="module"></script>
 </body>
 ​
 </html>
Copy the code

Main.js: entry file embedded in index.html as a module

 import { App } from "./App.js";
 import { createApp } from ".. /.. /lib/mini-vue.esm.js";
 const app = document.querySelector("#app");
 createApp(App).mount(app);
Copy the code

App.js: the root component that creates a Vue application

 import { h } from ".. /.. /lib/mini-vue.esm.js";
 ​
 export const App = {
   render() {
     return h("div", {}, "hi! " + this.msg);
   },
   setup() {
     return {
       msg: "mini-vue"}; }};Copy the code

(2) Tools

When writing VUE3, we used typescript as the coding language, which provides good type checking and code hints, and rollup as the packaging tool.

 npm i typescript rollup @rollup/plugin-typescript tslib -D
Copy the code

(3) Configuration

 //package.json
 {
   / /...
   "main": "/lib/mini-vue.cjs.js"."module": "/lib/mini-vue.esm.js"."scripts": {
     "build": "rollup -c rollup.config.js --watch"
   },
   / /...
 }
Copy the code
 //rollup.config.js
 import typescript from "@rollup/plugin-typescript";
 export default {
   input: "./src/index.ts"./ / the entry
   output: [ / / export
     {
       format: "cjs"./ / CommonJs specification
       file: "lib/mini-vue.cjs.js"}, {format: "es"./ / ES the Module specification
       file: "lib/mini-vue.esm.js"],},plugins: [typescript()],  / / the plugin
 };
Copy the code
 //tsconfig.json
 {
   "compilerOptions": {"module":"esnext"}}Copy the code

Three) principle analysis

(1) Flow chart

(2) Process explanation

First, we can see that main.js imports a function called createApp, so we can start from there.

//createApp.ts
import { render } from "./renderer";
import { createVNode } from "./vnode";
​
export function createApp(rootComponent) {
  // Receives a root component object and returns an object containing the mount method
  return {
    mount(rootContainer) {
      // Convert Component to vNode
      // All subsequent logical operations will be processed based on vNode
      const vnode = createVNode(rootComponent);
      // Render the virtual node after it has been convertedrender(vnode, rootContainer); }}; }Copy the code

Following the logic, we create a root vNode for the root component

With vNodes, there are two possible cases:

  • One is a component

    • As the root component
  • One is a specific element tag, Element

    • As the h function in app.js’s render function acceptsdiv
 //vnode.ts
 export function createVNode(type, props? , children?) {
   const vnode = {
     type./ / component or element
     props, 
     children,
   };
   return vnode;
 }
Copy the code

The h function actually returns a VNode of type Element

 //h.ts
 import { createVNode } from "./vnode";
 ​
 export function h(type, props? :Object, children? :String | Array<Object>) {
   return createVNode(type, props, children);
 }
Copy the code

To create the end point, we render it.

 //renderer.ts
 export function render(vnode, container) {
   patch(vnode, container);
 }
 ​
 // Process the virtual node vnode
 function patch(vnode, container) {
   // Determine the type
   if (typeof vnode.type === "string") {
     processElement(vnode, container);
   } else if(isObject(vnode.type)) { processComponent(vnode, container); }}Copy the code

For element type nodes, this is relatively simple.

 //renderer.ts
 / / processing element
 function processElement(vnode: any, container) {
   / / mount element
   mountElement(vnode, container);
 }
 ​
 function mountElement(vnode: any, container: any) {
   // By binding the DOM instance to vNode, we can directly access the DOM instance for subsequent business
   const el = (vnode.el = document.createElement(vnode.type));
 ​
   const { props, children } = vnode;
   // Check whether child nodes are included. If so, perform the patch operation
   // This is a recursive process
   if (typeof vnode.children === "string") {
     el.textContent = children;
   } else if (Array.isArray(children)) {
     mountChildren(vnode, el);
   }
 ​
   // Set the properties of this node
   for (const key in props) {
     const value = props[key];
     el.setAttribute(key, value);
   }
   // Add to the container
   container.append(el);
 }
 ​
 // Mount the child node
 function mountChildren(vnode: any, container: any) {
   vnode.children.forEach((v) = > {
     patch(v, container);
   });
 }
Copy the code

For nodes of Component:

 //renderer.ts
 / / processing component
 function processComponent(vnode: any, container: any) {
   // Mount the component
   mountComponent(vnode, container);
 }
 ​
 function mountComponent(initialVNode: any, container) {
   // Create a component instance
   const instance = createComponentInstance(initialVNode);
 ​
   // Initialize the component instance
   setupComponent(instance);
 ​
   // Render the component instance first
   setupRenderEffect(instance, initialVNode, container);
 }
Copy the code

Create a component real 🌰 :

 //component.ts
 export function createComponentInstance(vnode) {
   const componentInstance = {
     vnode, // Virtual node
     type: vnode.type, // Component type
     render: Function./ / render function
     setupState: {}, // Component status
     proxy: Proxy.// Component proxy object
   };
   return componentInstance;
 }
Copy the code

Initialize component real 🌰 :

 //component.ts
 export function setupComponent(instance) {
   //TODO
   //initProps()
   //initSlots()
 ​
   // Process the setup return value to initialize a stateful Component
   setupStatefulComponent(instance);
 }
 ​
 function setupStatefulComponent(instance: any) {
   const Component = instance.type;
   
   // We define a proxy object that provides the basis for subsequent data access through this.xxx
   instance.proxy = new Proxy({_:instance},
     PublicInstanceProxyHandlers
   );
 ​
   const { setup } = Component;
 ​
   if (setup) {
     // Setup () may return function or object
     // If it is function, we assume it is the render function of the component
     // If it is object, the returned content is injected into the context
     const setupResult = setup();
 ​
     // Process the result returned by setuphandleSetupResult(instance, setupResult); }}Copy the code
 //componentPublicInstance.ts
 const PublicPropertiesMap = {
   $el: (i) = > i.vnode.el,
   / /...
 };
 ​
 export const PublicInstanceProxyHandlers = {
   get({ _: instance }, key) {
     const { setupState } = instance;
     //setupState
     if (key in setupState) {
       return setupState[key];
     }
     const publicGetter = PublicPropertiesMap[key];
     if (publicGetter) {
       returnpublicGetter(instance); }}};Copy the code

Process the result returned by setup:

 //component.ts
 function handleSetupResult(instance, setupResult: any) {
   //function
   //TODO function
 ​
   if (typeof setupResult === "object") {
     instance.setupState = setupResult;
   }
 ​
   finishComponentSetup(instance);
 }
 // Complete the component initialization
 function finishComponentSetup(instance: any) {
   const Component = instance.type;
 ​
   if(Component.render) { instance.render = Component.render; }}Copy the code

After initialization, render the component for the first time:

 //renderer.ts
 function setupRenderEffect(instance: any, initialVNode, container) {
   // We take the instance proxy and point this in the render function to proxy
   // Then the getter method of proxy is called in the subsequent use of this. XXX to obtain the value
   // Because we already defined the getter for the proxy when we initialized the component
   // so we can use this.xxx to easily get the values we need
   const { proxy } = instance;
   const subTree = instance.render.call(proxy);
 ​
   // recursive call
   patch(subTree, container);
 ​
   // Bind the $el root node after subTree rendering is complete
   initialVNode.el = subTree.el;
 }
Copy the code

At this point, we have implemented a basic component initialization rendering process. We can then create a new index.ts as a package entry file and export the creation functions that the user needs.

//index.ts
export { createApp } from "./createApp";
export { h } from "./h";
Copy the code

Run YARN Build to package and we get the mini-vue.esm.js file.

Import the file to main.js and app.js, and the browser runs the index.html file.

Believe you can see our hi! The mini – vue!

Continue to update ~ welcome to pay attention to my nuggets and Github, if you feel good, remember to give my project 🌟 oh ~