This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.
The youth training camp actual combat class course has also ended, today we will first wank the Mini-vUE course brought by Village head Yang on Friday. If you missed the class, don’t miss this super detailed and slightly expanded note
See my Gitee repository for the code
For more information on Vue3, check out my previous blog post to get you started on Vue3
First, front-end framework design concept
Use Vue as an example
-
Simple and easy to use
-
Data driven
- Reduce DOM manipulation
- data-driven
- Data Reactive, effect
- Declarative render
- vdom
- patch
-
Progressive (MIN, VUex, Router, element3)
2. Motivation for Vue3
- (Type support) Why function?
-
Functions: vue3, react
-
Class: Angular, vue2 (decorator)
The function signature is unambiguous so that the input and output are unambiguous
- Advantages of the Composition API
- To eliminate
this
- Declare reactive data
- Reusability, readability, maintainability
<div id="app">{{title}}</div>
<script src="http://unpkg.com/vue@next"></script>
<script>
const app = Vue.createApp({
data() {
return {
title: 'Vue3, YK bacteria, data'}},setup() {
// Advantages of Composition API
/ / this
// Declare reactive data
// Reusability, readability, maintainability
const state = Vue.reactive({
title: 'Vue3, YK bacteria, setup'
})
return state
}
})
app.mount('#app')
</script>
Copy the code
As a result, setup has a higher priority
3. Summary
- Type of support
- Instance methods and properties tree-shaking
- reusability
hook
- maintainability
Composition API
- The API to simplify
- Conformance (different life cycles in directives and components)
- Delete apis with the same functionality (
v-model
,.sync
(Vue3 deprecated)
<comp v-model="foo"></comp> <comp :foo.sync="foo"></comp> <comp :value="foo" @update:value="foo = $event"></comp> Copy the code
- render
/ / Vue2 writing render(h){ return h('div', { attrs: { title: this.title } }, 'xxx')}/ / Vue3 writing render(){ return Vue.h('div', { title: this.title }, 'xxx')}Copy the code
- Extensibility: Custom renderers
Vue.createRenderer()
- Performance optimization — reactive based
Proxy
- Recursive efficiency problem
- Array problems (a separate set of implementations)
- API Impact (Dynamic attributes added or deleted
Vue.delete
/set
) - Class Collection data structures are not supported
- Compatibility (Vue2.7)
Two, to achieve mini-Vue
Reading source code directly is still a difficult thing to start, novice often confused, easy to encounter obstacles in some corners, waste a lot of time, so today we will write mini-Vue only focus on the most core part
1. Initialization
<div id="app">{{title}}</div>
<script>
// Code to be filled
</script>
<script>
const app = Vue.createApp({
data() {
return {
title: 'Hello, Bacteria Vue3 YK'
}
}
})
app.mount('#app')
</script>
Copy the code
① Basic structure
const Vue = {
createApp(options) {
// Return the app object
return {
mount(selector) {
// Code to be filled}}}}Copy the code
② How to mount elements
What we should be thinking about is what does Vue’s mount do?
- Finding the host element
- To render the page
- To deal with
template
Use:compile
compile - User written
render
- To deal with
- Append to host
Let’s write the code along these lines
// 1. Basic structure
const Vue = {
createApp(options) {
// Return the app object
return {
mount(selector) {
// 1. Find the host element
const parent = document.querySelector(selector)
// 2. Render the page
if(! options.render) {// 2.1 Handle template: compile
options.render = this.compile(parent.innerHTML)
}
// 2.2 User writes render directly
// Execute the render function (we specify the context of this in the render function, which is the return value of the data function in the configuration item)
const el = options.render.call(options.data())
// 3. Append to host
parent.innerHTML = ' '
parent.appendChild(el)
},
compile(template) {
// Return a render function
// parse -> ast
// generate: ast -> render
return function render() {
const h3 = document.createElement('h3')
h3.textContent = this.title
return h3
}
}
}
}
}
Copy the code
We are done showing the data in data, as shown in the figure below
3 Compatibility Processing
If the user writes data and setup at the same time, Vue3 uses the data in setup in preference
const app = Vue.createApp({
data() {
return {
title: 'Hello, vue3 YK bacteria data'}},setup() {
return {
title: 'Hello, vue3 YK bacteria setup'
}
}
})
app.mount('#app')
Copy the code
So we have to deal with compatibility issues between setup and other options before rendering
First, we collect setup and other options
if(options.setup){
this.setupState = options.setup()
}
if(options.data){
this.data = options.data()
}
Copy the code
We create a proxy that specifies the priority in the getter and setter
// Handle setup and other options compatibility before rendering
const proxy = new Proxy(this, {
get(target, key) {
// Get it from setup first, or from data if not
// If setup exists and key is defined in setup
if (target.setupState && key in target.setupState) {
// return target.setupState[key]
return Reflect.get(target.setupState, key)
} else {
// return target.data[key]
return Reflect.get(target.data, key)
}
},
set(target, key, val) {
if (target.setupState && key in target.setupState) {
return Reflect.set(target.setupState, key, val)
} else {
return Reflect.set(target.data, key, val)
}
}
})
Copy the code
Before mounting the element, set render’s context to proxy
const el = options.render.call(proxy)
Copy the code
④ Extensibility processing (custom renderer)
In the code we wrote above, the app instance returned is strongly coupled with the Web platform, because the operations are using document, to improve scalability, we should adopt the way of higher-order components, pass in the platform-related operations through parameters, to achieve the effect of decoupling, so that our framework is platform-independent ~
// 1. Find the host element
const parent = document.querySelector(selector)
// 3. Append to host
parent.innerHTML = ' '
parent.appendChild(el)
Copy the code
to
// 1. Find the host element
const parent = querySelector(selector)
// 3. Append to host
insert(el, parent)
Copy the code
How do you do that? We need to provide a new API for users to choose from
const Vue = {
/ / expanding
createRenderer({ querySelector, insert }) {
// Return the renderer
return {
createApp(options) {
// Return the app object
return {
mount(selector) {
// 1. Find the host element
const parent = querySelector(selector)
// 2. Render the page
/ /...
// 3. Append to host
insert(el, parent)
},
}
}
}
},
createApp(options) {
// Create a web platform-specific renderer
const renderer = Vue.createRenderer({
querySelector(sel) {
return document.querySelector(sel)
},
insert(el, parent) {
parent.innerHTML = ' '
parent.appendChild(el)
}
})
return renderer.createApp(options)
}
}
Copy the code
After initialization, the next thing we need to think about is how do we implement reactive data
2. The response type
I believe that many people who have not been exposed to Vue3 have heard that Vue3’s response is based on Proxy
However, Vue2 implements data responsiveness based on object.defineproperty, which has many disadvantages :(see my previous post on data responsiveness)
- Recursive efficiency problem
- Array problems (a separate set of implementations)
- API Impact (Dynamic attributes added or deleted
Vue.delete
/set
) - Class Collection data structures are not supported
1) reactive
Let’s explore this by creating a reactive object using Reactive, and after two seconds, the title value will change and the page will change.
const app = Vue.createApp({
setup() {
const state = reactive({
title: 'Hello, Bacteria Vue3 YK'
})
setTimeout(() = > {
state.title = 'New YK bacteria seen in 2 seconds'
}, 2000)
return state
}
})
app.mount('#app')
Copy the code
We create a reactive function that intercepts user access to the proxy object and responds when the value changes.
// Content intercepts user access to proxy objects to respond to value changes
function reactive(obj) {
// Return the object of the proxy
return new Proxy(obj, {
get(target, key) {
console.log('get key:', key)
return Reflect.get(target, key)
},
set(target, key, val) {
console.log('set key:', key)
const result = Reflect.set(target, key, val)
// Notification update
app.update()
return result
}
})
}
Copy the code
We add an update function to the returned app object, write in the code that rendered the page earlier, and call it once
// 2.2 User writes render directly
this.update = function () {
// Execute the render function (we specify the context of this in the render function, which is the return value of the data function in the configuration item)
const el = options.render.call(proxy)
// 3. Append to host
insert(el, parent)
}
// call once
this.update()
Copy the code
As you can see, our page is responsive ~ the content will change after two seconds
Take a rest, the village head teacher sing a song to everyone pull ~~~~
② Rely on collection
Reactive has a line app.update() that calls the app’s methods. This is strongly coupled to our app, and we need a mechanism (publish and subscribe) to decouple this behavior by creating dependencies between reactive data and their associated update functions
Establish the mapping: rely on the DEP -> component update function
Vue2 uses Watcher, see my previous blog [Vue source] data responsive principle
Vue3 creates a data structure like Map to establish dependencies {target, {key: [update1, update2]}}. The next thing to do is to establish dependencies in get and get dependencies in set
Let’s implement it:
First, we define a side effect function that takes a function fn, and the first step is to execute the fn function
function effect(fn) {
// 1. Run fn once
fn()
}
Copy the code
But there is a problem here, which is that when we execute fn, the function gives an error, what do we do, so we write it as a higher order function, and wrap our fn
Start by creating a stack effectStack that temporarily stores the side effect functions
const effectStack = []
Copy the code
Why use a stack here? Because nesting can occur when the side effect function is called, using data structures such as stacks is a good way to collect eff
Update our Effect function
function effect(fn) {
// 1. Run fn once
// fn()
const eff = function () {
try {
effectStack.push(eff)
fn()
} finally {
effectStack.pop()
}
}
// Call once immediately
eff()
// Return this function
return eff
}
Copy the code
We also need to build such dependency data structures
const targetMap = {
// We should store such data in it, dependencies
// state: {
// 'title': [update]
// }
}
Copy the code
Then define a tarck function that establishes the relationship between the target, key, and effectStack side effects stored in the function
// Establish the relationship between target,key, and effectStack side effects
function track(target, key) {
// Take out the last element that stores the side effect function
const effect = effectStack[effectStack.length - 1]
// This is true when written to death, but it is not possible to write this way, otherwise a new object is created each time
// targetMap[target] = {}
// targetMap[target][key] = [effect]
// Check whether the object with target as key exists
let map = targetMap[target]
if(! map) {// Get the target for the first time
map = targetMap[target] = {}
}
let deps = map[key]
if(! deps) { deps = map[key] = [] }// The mapping relationship is established
if (deps.indexOf(effect) === -1) {
deps.push(effect)
}
}
Copy the code
Define a trigger function to trigger the update
function trigger(target, key) {
const map = targetMap[target]
if (map) {
const deps = map[key]
if (deps) {
deps.forEach(dep= > dep())
}
}
}
Copy the code
We’re going to add dependencies in the getter and trigger updates in the setter, so we’re going to set this up in the proxy
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
console.log('get key:', key)
// Establish dependencies
track(target, key)
return Reflect.get(target, key)
},
set(target, key, val) {
console.log('set key:', key)
Reflect.set(target, key, val)
// Trigger the update
trigger(target, key)
}
})
}
Copy the code
Finally, let’s test it using test cases
// Create reactive data obj
const obj = reactive({
foo: 'foo'
})
// Create a side effect function that internally triggers reactive data
effect(() = > {
// Trigger responsive data
console.log(obj.foo)
})
// Change the foo property in obj
obj.foo = 'Foo has changed ~~~'
Copy the code
Let’s figure out what the relationship is
Finally, we need to improve our code. We need to create a side effect of the update function we wrote earlier, so that when the data changes, the update function will be executed again
So we just need to wrap the function we wrote earlier with a layer of effect higher-order function
// 2.2 User writes render directly
this.update = effect(() = > {
// Execute the render function (we specify the context of this in the render function, which is the return value of the data function in the configuration item)
const el = options.render.call(proxy)
// 3. Append to host
insert(el, parent)
})
// this.update()
Copy the code
At this point we delete our test case and see what it actually looks like using setup:
In fact, in the source code, targetMap uses a data structure that is not an object, but a Map, and a WeakMap
const targetMap = new WeakMap(a)Copy the code
Some operations can be changed to the corresponding Map get and set operations
// let map = targetMap[target]
let map = targetMap.get(target)
Copy the code
// map = targetMap[target] = {}
map = targetMap.set(target, {})
Copy the code
When storing dependencies, you should use data structures such as Set, which are automatically de-duplicated
// deps = map[key] = []
deps = map[key] = new Set(a)// The mapping relationship is established
// if (deps.indexOf(effect) === -1) {
// deps.push(effect)
// }
deps.add(effect)
Copy the code
The above response is complete, but it has a serious efficiency problem, which is that we update our DOM in full update mode, which is definitely not good, so this leads us to the virtual DOM and diff algorithm
3. The virtual DOM
What is the virtual DOM (VNode)?
- A VNode is a JS object that describes a view
Why vNode?
- Reduce DOM manipulation
- Efficient updates
- Cross-platform, compatibility
Let’s define our virtual DOM — vnode
We will change the real DOM from the render function returned in the compile function to a virtual DOM
return function render() {
// const h3 = document.createElement('h3')
// h3.textContent = this.title
// return h3
// The virtual DOM should be generated
return h('h3'.null.this.title)
// return h('h3', null, [
// h('p', null, this.title),
// h('p', null, this.title),
// h('p', null, this.title),
// ])
}
Copy the code
Define an H function that represents a DOM in js objects
// Pass in the information, return vNode, describe the view
function h(tag, props, children) {
return {
tag,
props,
children
}
}
Copy the code
The update function is also changed
this.update = effect(() = > {
const vnode = options.render.call(proxy)
// Convert vNode to DOM
// Initializes the creation of the entire tree
if (!this.isMounted) {
// implement createElm, create whole, vnode -> el
const el = this.createElm(vnode)
parent.innerHTML = ' '
insert(el, parent)
// init initializes and sets the mount flag
this.isMounted = true}})Copy the code
The createElm function is implemented recursively
createElm({ tag, props, children }) {
// Walk through the vNode to create the entire tree
const el = createElement(tag)
// If there are attributes, set them (omitted)
// el.setAttribute(key, val)
/ / recursion
// Check that children is a string
if (typeof children === 'string') {
el.textContent = children
} else {
children.forEach(child= > insert(this.createElm(child), el))
}
return el
}
Copy the code
4. diff
For more details, see my previous blog post illustrating the DIff algorithm
Save the real node in createElm
// Keep the real DOM in vNode for future updates
vnode.el = el
Copy the code
Modify the update function, which was first mounted earlier, to add the update logic
this.update = effect(() = > {
const vnode = options.render.call(proxy)
// Convert vNode to DOM
// Initializes the creation of the entire tree
if (!this.isMounted) {
// implement createElm, create whole, vnode -> el
const el = this.createElm(vnode)
parent.innerHTML = ' '
insert(el, parent)
// init initializes and sets the mount flag
this.isMounted = true
} else {
this.patch(this._vnode, vnode)
}
this._vnode = vnode
})
Copy the code
Patch updates
patch(oldNode, newNode) {
const el = newNode.el = oldNode.el
// 1. Update: The same node must be updated
// What is the same node
if (oldNode.tag === newNode.tag && oldNode.key === newNode.key) {
// Update the same node
const oldChild = oldNode.children
const newChild = newNode.children
if (typeof oldChild === 'string') {
if (typeof newChild === 'string') {
// Text update
if(oldChild ! == newChild) { el.textContent = newChild } }else {
// Replace text with a set of child elements, empty and then create and append
el.textContent = ' '
newChild.forEach(child= > insert(this.createElm(child), el))
}
} else {
if (typeof newChild === 'string') {
// Replace a set of child elements with text
el.textContent = newChild
} else{}}}else {
// replace Replace different nodes}},Copy the code
Set to update data from string to array
setup() {
const state = reactive({
title: 'Hello, Bacteria Vue3 YK'
})
setTimeout(() = > {
state.title = 'New YK bacteria seen in 2 seconds'.split("")},2000)
return state
}
Copy the code
The effect
Contrast the diff
Vue3 and Vue2 are basically the same. In my previous blog post, I also drew a comparison diagram of Vue2 diff algorithm
So let’s simplify and update it in the simplest and most crude way possible
updateChildren(el, oldChild, newChild) {
// 1. Get the shorter of newCh and oldCh
const len = Math.min(oldChild.length, newChild.length)
// Force an update
for (let i = 0; i < len; i++) {
this.patch(oldChild[i], newChild[i])
}
// Process the remaining elements
// The new array has many elements
if (newChild.length > oldChild.length) {
// Batch create and append
// Intercepts the part after len in newCh
newChild.slice(len).forEach(child= > {
insert(this.createElm(child), el)
})
} else if (newChild.length < oldChild.length) {
// Batch delete
oldChild.slice(len).forEach(child= > {
remove(child.el, el)
})
}
},
Copy the code