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:
- I created a
id
forapp
thediv
The element - Page introduces a
main.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:
- through
createApp
Create an application instance - Will create a good application instance, through
mount
Method is mounted toid
forapp
On the elements of
So we introduce a few to-do items:
createApp
Is derived from thevue
, so first createvue
object- implementation
createApp
methods - implementation
mount
methods - In addition
creatApp
To accept aApp
We 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
template
I did it very briefly2
A:
h1
The content of the element is the attribute passed when the component is usedmsg
The value of thebutton
The element binds an event when the button isclick
letcount++
- while
script
To import a configuration object, declare it2
A:
msg
attribute- Response data
count
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:
createApp
Is derived from theVue
Are we going to have aconst Vue = {... }
- through
createApp
createapp
The instance - through
mount
Methods the mount
Manually implement createApp and mount
- First, create a
Vue
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
- is
mount
What 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
- 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
- Find the host element
const parent = document.querySelector(selector)
Copy the code
- Using the render function
render
getdom
And mixed with related configuration data
if(! ops.render) { ops.render =this.compile(parent.innerHTML)
}
const el = ops.render.call(ops.data())
Copy the code
- Will the
dom
Append 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 worked
createApp
,createRenderer
,mount
,compile
Methods 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 element
innerHTML
As a templatetemplate
And then compile into a render function by executing the render functionrender
You can get realdom
Node, 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 finaldom
The node is appended to
end~