Implementing a mini-VUE can deepen your understanding of Vue, including the rendering system module, the responsive system module, and the application entry module (no compilation system module is implemented).
Vue core module
Vue has three core modules: compilation system, rendering system and responsive system.
Build system
- Parse the Template template into an abstract syntax tree (AST).
- Optimize AST.
- Generate the render function based on the AST.
Rendering system
- The reder function returns vNode
- A tree structure vDOM is formed between vNodes
- Generate the real DOM from the VDOM and render it to the browser
Responsive system
- The old and new VNodes are compared using diff algorithm
- The rendering system regenerates the DOM from vNode and renders it to the browser
Rendering system
Generate a VNode using the h function
The h function contains three arguments, elements, attributes, and child elements. The resulting VNode is a javascript object
function h(tag, props, children) {
// vnode --> javascript object
return {
tag,
props,
children
}
}
Copy the code
// 1. Use the h function to create a vnode
const vnode = h("div", {class: 'lin'}, [
h("span".null.'I'm a pretty boy'),
h("button", {onClick: function() {}}, 'change')])Copy the code
Documentation: H function
Mount vnode to div#app using the mount function
mount(vnode, document.querySelector("#app"))
const h = (tag, props, children) = > {
// vnode --> javascript object
return {
tag,
props,
children
}
}
const mount = (vnode, container) = > {
// 1. Create a real element and save el on vNode
const el = vnode.el = document.createElement(vnode.tag)
// 2. Handle props
if (vnode.props) {
for (let key in vnode.props) {
const value = vnode.props[key]
if (key.startsWith("on")) { // Whether it is an event
el.addEventListener(key.slice(2).toLowerCase(), value)
} else {
el.setAttribute(key, value)
}
}
}
// 3. Handle subelement strings, arrays
if (vnode.children) {
if (typeof vnode.children === "string") {
el.textContent = vnode.children
} else {
// Array multiple child recursion
vnode.children.forEach(element= > {
mount(element, el)
})
}
}
// 4. Mount the EL to the container
container.appendChild(el)
}
Copy the code
Patch Compares the old and new nodes
When a node changes, compare the old and new nodes and update them. Using the new VNode as a baseline, modify the old oldVNode to look like the new VNode (patched)
setTimeout(() = > {
const vnode2 = h("div", {class: 'jin'.onClick: function() {console.log("I'm a pretty boy.")}}, [
h("button", {class: "zhang"}, 'button')
])
patch(vnode, vnode2)
}, 2000)
Copy the code
const patch = (n1, n2) = > {
// n1 old n2 new
// If the parent element is different, replace it directly
if(n1.tag ! == n2.tag) {// Get the parent element
const n1Elparent = n1.el.parentElement
// Remove the old node
n1Elparent.removeChild(n1.el)
// Remount the new node
mount(n2, n1Elparent)
} else {
// reference, modify one and the other will change, n1.el is assigned when n1 is mounted
const el = n2.el = n1.el
/ / contrast props
const oldProps = n1.props || {}
const newProps = n2.props || {}
// Add new props to el
for (let key in newProps) {
const oldValue = oldProps[key]
const newValue = newProps[key]
if(newValue ! == oldValue) {if (key.startsWith("on")) {
el.addEventListener(key.slice(2).toLowerCase(), newValue)
} else {
el.setAttribute(key, newValue)
}
}
}
// remove the old props to remove the listener, property
for (let key in oldProps) {
if (key.startsWith("on")) {
el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key])
} else {
if(! keyin newProps) {
el.removeAttribute(key)
}
}
}
/ / the children
// Children can be strings, arrays, objects (slots). Strings and arrays are more common
// n1 [v1, v2, v3, v4, v5]
// n2 [v1, v7, v8]
const oldChildren = n1.children || []
const newChidlren = n2.children || []
if (typeof newChidlren === 'string') { // If newChidlren is a string
if (typeof oldChildren === "string") {
if(newChidlren ! == oldChildren) {// the textContent property represents the textContent of a node and its descendants
el.textContent = newChidlren
}
} else {
// innerHTML Returns an HTML textContent that usually has better performance because the text will not be parsed into HTML. Using textContent protects against XSS attacks.
el.innerHtml = newChidlren
}
} else { // If newChidlren is an array
const oldLength = oldChildren.length
const newLength = newChidlren.length
const minLength = Math.min(oldLength, newLength)
// Compare parts of the same length first
for (let i = 0; i < minLength; i++) {
patch(oldChildren[i], newChidlren[i])
}
// If the new node is longer, mount the new node
if (newLength > oldLength) {
newChidlren.slice(minLength).forEach(item= > {
mount(item, el)
})
}
// If the old node is longer, remove the redundant node
if (newLength < oldLength) {
oldChildren.slice(minLength).forEach(item= > {
el.removeChild(item.el)
})
}
}
}
}
Copy the code
Responsive system
As the data changes, so should all the places where it is used
let obj = {
name: 'lin'
}
const change = () = > {
console.log('Output :', obj.name)
}
change()
obj.name = 'jin'
// When obj changes, the places where obj is used also change accordingly
change()
Copy the code
Define a dependency collection class
class Depend {
constructor() {
The Set object allows you to store unique values of any type without duplication
this.reactiveFns = new Set()}addDepend(reactiveFn) {
this.reactiveFns.add(reactiveFn)
}
notify() {
this.reactiveFns.forEach(item= > {
item()
})
}
}
let obj = {
name: 'lin'
}
const change = () = > {
console.log('Output :', obj.name)
}
const dep = new Depend()
dep.addDepend(change)
obj.name = 'jin'
dep.notify()
Copy the code
Automatically listens for changes in objects
We need to call notify again every time the object changes, and we can use proxy to listen for changes in the object.
Response function
Not every function needs to become a reactive function. We can define a function to receive functions that need to be converted to a reactive function.
let activeReactiveFn = null
function watchFn(fn) {
activeReactiveFn = fn
fn() // Invoke get once (see code below)
activeReactiveFn = null
}
Copy the code
Changes to the listening object
Vue2 and VUe3 are implemented differently:
- Vue2 uses object.defineProperty () to hijack objects to listen for changes in data
- Cannot listen for array changes
- Each property of the object must be traversed
- Nested objects must be traversed deeply
- Vue3 uses proxy to listen for object changes
- Object: For the whole object, rather than an attribute of the object, so there is no need to iterate over keys.
- Support array: Proxy does not need to overload the array method, eliminating many hacks, reducing the amount of code is equal to reducing the maintenance cost, and the standard is the best.
- The second argument to Proxy can be intercepted in 13 ways, which is much richer than object.defineProperty ()
- Proxy is a new standard that is getting a lot of attention and performance optimization from browser vendors, while Object.defineProperty() is an existing, older method.
const reactive = (obj) = > {
let depend = new Depend()
// Returns a proxy object that manipulates the proxy object, and if the proxy object changes, the original object changes as well
return new Proxy(obj, {
get: (target, key) = > {
// Collect dependencies
depend.addDepend()
return Reflect.get(target, key)
},
set: (target, key, value) = > {
Reflect.set(target, key, value)
// Triggered when the value changes
depend.notify()
}
})
}
// Modify addDepend in the Depend class
// addDepend() {
// if (activeReactiveFn) {
// this.reactiveFns.push(activeReactiveFn)
// }
// }
let obj = {
name: 'lin'
}
let proxyObj = reactive(obj)
const foo = () = > {
console.log(proxyObj.name)
}
watchFn(foo)
proxyObj.name = 'jin'
Copy the code
Documents:
- Reflect
Collect dependencies correctly
Whenever we change the proxy object (vue2 object), for example, we add a new oneage
Attributes, even thoughchange
It’s not used in the functionage
, we also triggerchange
Function. So we need to collect dependencies correctly, how do we collect dependencies correctly.
- Different objects are stored separately
- Properties of the same object must be stored separately
- We can use WeakMap to store objects
A WeakMap object is a set of key/value pairs, where the key is weakly referenced (the original object can be garbage collected when destroyed). The key must be an object, and the value can be arbitrary.
- Map can be used to store different attributes of an object
The Map object holds key-value pairs and can remember the original insertion order of the keys. Any value (object or raw value) can be a key or a value.
const targetMap = new WeakMap(a)const getDepend = (target, key) = > {
// Get the Map based on the target object
let desMap = targetMap.get(target)
if(! desMap) { desMap =new Map()
targetMap.set(target, desMap)
}
// Get the Depend class by key
let depend = desMap.get(key)
if(! depend) { depend =new Depend()
desMap.set(key, depend)
}
return depend
}
Copy the code
const reactive = (obj) = > {
return new Proxy(obj, {
get: (target, key) = > {
// Collect dependencies
const depend = getDepend(target, key)
depend.addDepend()
return Reflect.get(target, key)
},
set: (target, key, value) = > {
const depend = getDepend(target, key)
Reflect.set(target, key, value)
// Triggered when the value changes
depend.notify()
}
})
}
Copy the code
Application entry module
Create a new HTML file and import all the js files of the created function
<script>
// Create root component
const App = {
// Reactive data is required
data: reactive({
counter: 0
}),
render() {
// function h renders the node
return h("div", {class: 'lin'}, [
h("div", {class: 'text'}, `The ${this.data.counter}`),
h("button", {onClick: () = > {
this.data.counter++
console.log(this.data.counter)
}}, '+')]}}// Mount the root component
const app = createApp(App)
app.mount("#app")
</script>
Copy the code
Create a new js file that holds the createApp function, which returns an object with a mount method inside
const createApp = (rootComponent) = > {
return {
mount(selector) {
const container = document.querySelector(selector)
// response function
watchEffect(function() {
const vNode = rootComponent.render()
// Mount the node to #div
mount(vNode, container)
})
}
}
}
Copy the code
There’s a little bit of a problem with that, every click+
Buttons add nodes
Mount for the first time –> Value change –> Patch
const createApp = (rootComponent) = > {
return {
mount(selector) {
const container = document.querySelector(selector)
// isMounted specifies whether it isMounted
let isMounted = false
let oldVNode = null
watchEffect(function() {
if(! isMounted) { oldVNode = rootComponent.render()// First mount
mount(oldVNode, container)
isMounted = true
} else {
const newVNode = rootComponent.render()
// Compare old and new nodes
patch(oldVNode, newVNode)
oldVNode = newVNode
}
})
}
}
}
Copy the code
Write of very dish, wait me to become bald again come back improvement improvement!!
Reference documentation
Inside Vue3 + typescript by Hongyuan Wang