This is the 12th day of my participation in the More text Challenge. For details, see more text Challenge

Without further ado, let’s get straight to the point and simulate the Vue3 initialization process!

Vue3 initializes the process

Let’s first take a look at the Vue3 initialization process before implementing by hand. For the sake of observation, let’s build a Vue3 project directly

Create a Vue3 project

There are many official ways to build, and I choose to use Vite here, as follows:

$ npm init vite-app mini-vue3
$ cd mini-vue3
$ npm install
$ npm run dev
Copy the code

If the following information is displayed, the operation is successful

Analyze and initialize the entire process

First, we go into the project’s index.html file

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link rel="icon" href="/favicon.ico" />
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>Vite App</title>
</head>
<body>
  <div id="app"></div>
  <script type="module" src="/src/main.js"></script>
</body>
</html>
Copy the code

You can see the index. HTML code is just two things:

  1. I created aidforappthedivThe element
  2. Page introduces amain.js, but its type ismodule, indicating that there are some modularized things in the file

So, we follow this up to the main.js file in the SRC directory. The details are as follows

//src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

createApp(App).mount('#app')
Copy the code

You can see that main.js only does two things:

  1. throughcreateAppCreate an application instance
  2. Will create a good application instance, throughmountMethod is mounted toidforappOn the elements of

So we introduce a few to-do items:

  1. createAppIs derived from thevue, so first createvueobject
  2. implementationcreateAppmethods
  3. implementationmountmethods
  4. In additioncreatAppTo accept aAppWe need to see what’s inside

There’s no rush. We’ll go from simple to complicated, step by step. Look at the./ app.vue file first

/ / App. Vue file
<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <HelloWorld msg="Hello Vue 3.0 + Vite" />
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App'.components: {
    HelloWorld
  }
}
</script>
Copy the code

In fact, there is a very common vue component, but there is also another component HelloWorld, let’s go all the way to the end, and then look at helloworld.vue

/ / HelloWorld. Vue file
<template>
  <h1>{{ msg }}</h1>
  <button @click="count++">count is: {{ count }}</button>
  <p>Edit <code>components/HelloWorld.vue</code> to test hot module replacement.</p>
</template>

<script>
export default {
  name: 'HelloWorld'.props: {
    msg: String
  },
  data() {
    return {
      count: 0}}}</script>
Copy the code

You can see that the helloWorld.vue component consists of two parts: Template and Script

  1. templateI did it very briefly2A:
  • h1The content of the element is the attribute passed when the component is usedmsgThe value of the
  • buttonThe element binds an event when the button isclickletcount++
  1. whilescriptTo import a configuration object, declare it2A:
  • msgattribute
  • Response datacount

In fact, MSG and count here will correspond to MSG and count in the template. Some may wonder why MSG and count in the template know to look for the data in the script. These are the default mechanisms for Vue to always find the data according to the rules and mechanisms.

Through the above process, we can roughly summarize the core initialization process of Vue3:

Create an application instance using the createApp method in vue and mount the instance to the corresponding host element using the mount method of the application instance.

So, we’re going to analyze and implement the core functions createApp and mount

Implementing core functions

To keep things simple, let’s go step by step, first creating vue, then implementing createApp, and finally implementing the mount method

The test case

Let’s just create a separate file, like mini.html. I wrote a basic test case like this:

const { createApp } = Vue
const app = createApp({
    data() {
        return {
            count: 0}}}); app.mount('#app');
Copy the code

As shown above, let’s think about it in several steps:

  1. createAppIs derived from theVueAre we going to have aconst Vue = {... }
  2. throughcreateAppcreateappThe instance
  3. throughmountMethods the mount

Manually implement createApp and mount

  1. First, create aVue
const Vue = { }
Copy the code

Think: What does the application instance returned by createApp look like?

First, when createApp is called, it returns the application instance with at least one mount method in it, so our basic structure is clear, as follows

const Vue = {
  createApp: function (ops) {
    return {
      mount(){... }}}Copy the code

The mount method, which takes a selector, allows us to mount the referenced instance into the corresponding element

At this point, we still need to answer a few questions

  1. ismountWhat exactly did it do, or what was its goal?

In fact, recall the mounting process of the app instance, we want our configuration to be rendered to the host associated with #app! So before we do that we need to parse the component’s configuration into the DOM, that is, component configuration —-> Parse —-> DOM —–> render the DOM as the host element

  1. Where will the data in the configuration component be stored in the future?

Since the browser only treats {{“count:”+count}} as a string, we need to add an important operation here: compile and match data. In fact, what compilation does is compile the above template into a rendering function

Our structure looks like this

const Vue = {
  createApp: function (ops) {
    return {
      mount(selector){... },compile(template){... }}}Copy the code

So, let’s implement the compile function first

We know that compile takes a template and turns it into a render function that can be executed when the application instance is mounted to render the interface.

This is a temporary simplification, but in a real VUE, this would become a virtual DOM. This is simplified to describe the view directly, which is equivalent to the compiled results in VUE

compile(template) {
    return function render() {
        / / to simplify
        const h1 = document.createElement('h1')
        h1.textContent = this.count
        returnh1; }}Copy the code

With Compile, I start back to the main logic

  1. Find the host element
const parent = document.querySelector(selector)
Copy the code
  1. Using the render functionrendergetdomAnd mixed with related configuration data
if(! ops.render) { ops.render =this.compile(parent.innerHTML)
}
const el = ops.render.call(ops.data())
Copy the code
  1. Will thedomAppend to the page
parent.innerHTML = ' '
parent.appendChild(el)
Copy the code

The complete code is as follows

const Vue = { createApp: function (ops) { return { mount(selector) { const parent = document.querySelector(selector) if (! ops.render) { ops.render = this.compile(parent.innerHTML) } const el = ops.render.call(ops.data()) parent.innerHTML = ''  parent.appendChild(el) }, compile(template) { return function render() { const h1 = document.createElement('h1') h1.textContent = this.count return h1; } } } } }Copy the code

Compatible with Vue2 options API and Vue3 Composition API

Add composition API to test case

The following

const { createApp } = Vue
const app = createApp({
    data() {
        return {
            count: 0}},//composition API
    setup() {
        return {
            count: 1}}}); app.mount('#app');
Copy the code

Identify data sources by proxy

Here we have to decide whether the data is coming from data or from setup

if (ops.setup) {
    this.setupState = ops.setup()
} else {
    this.data = ops.data()
}
Copy the code

When ops.setup is true, the vue3 composition API is used, so the data comes from ops.setup(), otherwise from ops.data().

How does the render function know whether the data is from data or setup

this.proxy = new Proxy(this, {
    get(target, key) {
        if (key in target.setupState) {
            // Setup has a higher priority
            return target.setupState[key]
        } else {
            // If not, use the Options API
            return target.data[key]
        }
    },
    set(target, key, val) {
        if (key in target.setupState) {
            target.setupState[k] = val
        } else {
            target.data[key] = val
        }
    }
})
Copy the code

The proxy above is passed in as the context of the Render function

Since the current instance is proxied, access to this in the render function is equivalent to access to the GE function

const el = ops.render.call(this.proxy)
Copy the code

Implement createRenderer

CreateRenderer is mainly used to achieve multi-platform scalability, which is actually a mechanism to implement a renderer.

Let’s go back to our createApp function, which uses browser platform-specific code such as Document.querySelector, appendChild, and so on. So what we want to do is give the user a set of apis for creating a renderer like createRenderer, and then the user creates the renderer through that SET of apis. So the general logic inside the renderer is the same, but the actual work, we write inside the createRenderer, tells the renderer what to do. This way, I can easily extend those common logic.

This might be a little hard to talk about, but what do we do in code

First, in order to be able to implement extensions, it is common to make createApp a higher-order function.

Then, we create a function called createRenderer that creates a custom renderer. This function will take parameters and perform a series of operations, including various node operations, but the node operations will vary from platform to platform, so that it can be extended across multiple platforms.

So, we’re moving the generic code into createRenderer, which returns a custom renderer, and the custom renderer that’s returned is actually doing the same thing as the createApp that we wrote before, except we’re pulling out the platform-specific code inside, Platform-specific code is provided by parameters passed by createRenderer, so the function is implemented as a whole

createRenderer({ querySelector, insert }) {
    return {
        createApp(ops) {
            return {         
                mount(selector) {
                    const parent = querySelector(selector)
                    if(! ops.render) { ops.render =this.compile(parent.innerHTML)
                    }                   
                    if (ops.setup) {
                        this.setupState = ops.setup()
                    } else {
                        this.data = ops.data();
                    }                   
                    this.proxy = new Proxy(this, {
                        get(target, key) {
                            if (key in target.setupState) {
                                return target.setupState[key]
                            } else {
                                return target.data[key]
                            }
                        },
                        set(target, key, val) {
                            if (key in target.setupState) {
                                target.setupState[k] = val
                            } else {
                                target.data[key] = val
                            }
                        }
                    })
                    const el = ops.render.call(this.proxy)
                    parent.innerHTML = ' '
                    insert(el, parent)

                },
                compile(template) {                    
                    return function render() {                        
                        const h1 = document.createElement('h1')
                        h1.textContent = this.count
                        return h1;
                    }
                }
            }
        }
    }
}
Copy the code

However, our createApp takes this createRenderer and provides some web platform related operations. The following

createApp(ops) {
    const renderer = Vue.createRenderer({
        querySelector(selector) {
            return document.querySelector(selector)
        },
        insert(child, parent, anchor) {
            parent.insertBefore(child, anchor || null)}})return renderer.createApp(ops)
}
Copy the code

So we have scalability across multiple platforms

The final code looks like this

<! 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>mini-vue3</title>
</head>

<body>
    <div id="app">
        <! -- <h1>{{"count:"+count}}</h1> -->
    </div>
    <script>
        // How is the interface exposed to the outside
        // Create a Vue
        const Vue = {
            // Need to consider:
            // 1. What does the application instance returned by createApp look like
            // First, when createApp is called, it returns the application instance with at least one mount method, so
            createApp(ops) {
                // Exposed to the Web browser platform, so it focuses on the browser platform. It calls createRenderer, which needs to pass the node operations that are used by the corresponding platform,
                // Only the node operations used in this example are passed here
                const renderer = Vue.createRenderer({
                    querySelector(selector) {
                        return document.querySelector(selector)
                    },
                    insert(child, parent, anchor) {
                        parent.insertBefore(child, anchor || null)}})return renderer.createApp(ops)
            },


            // To enable extensions, it is common to make createApp a higher-order function.
            // We create a function createRenderer that creates a custom renderer. This function will take parameters and perform a series of operations, including various node operations and so on
            //, but the operation of this node will vary from platform to platform, so that it can scale across multiple platforms.
            createRenderer({ querySelector, insert }) {
                // Return the custom renderer
                return {
                    createApp(ops) {
                        // Return the app instance object
                        return {
                            // There is a mount method that accepts a selector that allows us to mount the referenced instance to the corresponding element
                            mount(selector) {
                                // What does mount do, or what is its goal?
                                // Recall the app instance mounting process. We want our configuration to be rendered to the host associated with #app, so we need to parse the component's configuration into the DOM before this
                                // that is, component configuration ----> Parse ---->dom-----> render the DOM as the host element
                                // However, there is still the question of where to put the data in the configuration component.
                                // The browser only treats {{"count:"+count}} as a string. So here we need and I have one operation, compile
                                // Compile is used to compile the above template into a rendering function
                                // 1. Find the host element
                                // const parent = document.querySelector(selector)
                                const parent = querySelector(selector)
                                // 2. Use the render function
                                if(! ops.render) {// If the render function does not exist
                                    ops.render = this.compile(parent.innerHTML)
                                }
                                //3. The render function is called, and in this operation, we need to execute the data function in the instance. The data returned is the data we want, and we get el

                                3.1 is compatible with VUE2 and VUE3
                                if (ops.setup) {
                                    this.setupState = ops.setup()
                                } else {
                                    this.data = ops.data();
                                }
                                // Where is the data retrieved from the render function?
                                this.proxy = new Proxy(this, {
                                    get(target, key) {
                                        // console.log(key, target)
                                        if (key in target.setupState) {
                                            // Setup has a higher priority
                                            return target.setupState[key]
                                        } else {
                                            // If not, use the Options API
                                            return target.data[key]
                                        }
                                    },
                                    set(target, key, val) {
                                        if (key in target.setupState) {
                                            target.setupState[k] = val
                                        } else {
                                            target.data[key] = val
                                        }
                                    }
                                })
                                // This proxy is passed as the context of the render function
                                }}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
                                const el = ops.render.call(this.proxy)

                                //4. With the DOM element EL, append it to the page
                                parent.innerHTML = ' '
                                // parent.appendChild(el)
                                insert(el, parent)

                            },
                            compile(template) {
                                // compile receives a template and turns it into a render function that can be executed when the application instance is mounted to render the interface.
                                // that is data --> real DOM (this is a temporary simplification, in real vUE, will become virtual DOM)
                                return function render() {
                                    // Note: Since the process of compiling the template is a bit complicated, this is simplified and describes the view directly, which is equivalent to the compiled results in vue
                                    const h1 = document.createElement('h1')
                                    h1.textContent = this.count
                                    return h1;
                                }
                            }
                        }

                    }
                }
            }

        }

    </script>
    <script>
        // Test the following example
        // First, createApp comes from Vue
        const { createApp } = Vue
        // Then use createApp to create the app instance
        const app = createApp({
            data() {
                return {
                    count: 0}},// We'll add a new vue3 function: setup, the composition API entry function
            setup() {
                let count = 1
                return { count }
            }
        });
        / / a mount
        app.mount('#app');
    </script>
</body>
</html>
Copy the code

Test the code and run it successfully!

Through the above series of processes, we have manually implemented the Vue3 initialization process

conclusion

  • We started at 0 to implement the Vue3 initialization process by hand, and it workedcreateApp,createRenderer,mount,compileMethods such as
  • Here’s a brief summary of what mount does. It actually gets the current host element based on the selector passed in by the user, and then gets the current host elementinnerHTMLAs a templatetemplateAnd then compile into a render function by executing the render functionrenderYou can get realdomNode, and it is during the execution of the render function that the data and state configured by the user are passed in to get the finaldomThe node is appended to

end~