The course of the youth training camp combat class is also over. Today, let's roll out the mini Vue course brought by village head Yang on Friday. If you miss the course, you must not miss this super detailed and slightly expanded note~
The relevant code can see mine gitee warehouse
For some information about Vue3, please refer to my previous blog Get you started quickly, Vue3
1, Front end frame design concept
1. Use Vue as an example
-
Simple and easy to use
-
Data driven
- Reduce DOM operations
- data-driven
- Data responsive (reactive, effect ive)
- Declarative rendering, render
- vdom
- patch
-
progressive (min, vuex, router, element3)
2. Vue3's motivation
- (type support) why use functions?
-
Functions: vue3, react
-
class: Angular, vue2(decorator)
The function signature is clear, so the input and output contents are clear
- Advantages of Composition API
- Destroy this
- Declarative responsive data
- Reusability, readability and maintainability
<div id="app">{{title}}</div> <script src="http://unpkg.com/vue@next"></script> <script> const app = Vue.createApp({ data() { return { title: 'vue3, YK bacterium, data' } }, setup() { // Advantages of Composition API // Destroy this // Declarative responsive data // Reusability, readability and maintainability const state = Vue.reactive({ title: 'vue3, YK bacterium, setup' }) return state } }) app.mount('#app') </script>
As a result, you can see that setup has a higher priority
3. Summary
- Type support
- Instance method and attribute tree shaking
- Reusable hook
- Maintainability Composition API
- API simplification
- Consistency (the lifecycle in an instruction is different from that of a component)
- Delete API s with the same function (v-model,. sync (Vue3))
<comp v-model="foo"></comp> <comp :foo.sync="foo"></comp> <comp :value="foo" @update:value="foo = $event"></comp>
- render
// Vue2 writing method render(h){ return h('div', { attrs: { title: this.title } }, 'xxx') } // Vue3 writing method render(){ return Vue.h('div', { title: this.title }, 'xxx') }
- Extensibility: Custom renderer Vue.createRenderer()
- Performance optimization - responsive Proxy based
- Recursive efficiency problem
- Array problem (implemented in a separate set)
- API impact (adding and deleting Vue.delete / set of dynamic attributes)
- class collection data structures are not supported
- Compatibility (vue2.7)
2, Implementation of mini Vue
It's hard to read the source code directly. Novices often can't distinguish the key points. They are easy to encounter obstacles in some corners and waste a lot of time. Therefore, today's Mini Vue only focuses on the core part
1. Initialization
<div id="app">{{title}}</div> <script> // Code to be filled </script> <script> const app = Vue.createApp({ data() { return { title: 'hello, vue3 YK bacterium' } } }) app.mount('#app') </script>
① Basic structure
const Vue = { createApp(options) { // Return app object return { mount(selector) { // Code to be filled [how to mount] } } } }
② How to mount elements
What we should think is what Vue's mount did?
- Host element found
- Render Page
- Processing template: compile using compile
- Users write render directly
- Append to host
According to the above ideas, let's write the code
// 1. Basic structure const Vue = { createApp(options) { // Return app object return { mount(selector) { // 1. Find host element const parent = document.querySelector(selector) // 2. Render page if (!options.render) { // 2.1 processing template: Compiling options.render = this.compile(parent.innerHTML) } // 2.2 users write render directly // Execute the render function (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) { // Returns a render function // parse -> ast // generate: ast -> render return function render() { const h3 = document.createElement('h3') h3.textContent = this.title return h3 } } } } }
As shown in the figure below, we have completed the step of displaying the data in data
③ Compatibility processing
If the user writes data and setup at the same time, Vue3 will give priority to the data in setup
const app = Vue.createApp({ data() { return { title: 'hello, vue3 YK bacterium data' } }, setup() { return { title: 'hello, vue3 YK bacterium setup' } } }) app.mount('#app')
So before rendering, we need to deal with the compatibility between setup and other options
First, we collect setup and other options
if(options.setup){ this.setupState = options.setup() } if(options.data){ this.data = options.data() }
We create a proxy and specify priorities in getter s and setter s
// Before rendering, handle the compatibility of setup and other options const proxy = new Proxy(this, { get(target, key) { // Get from setup first. If not, get from data again // If setup exists and the 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) } } })
Before mounting the element, set the context of render to proxy
const el = options.render.call(proxy)
④ Extensibility processing (custom renderer)
In the code we wrote above, the returned app instance is strongly coupled with the web platform, because the operations all use document. To improve scalability, we should use high-order components to pass in platform related operations through parameters, so as to achieve the effect of decoupling, and make our framework independent of the platform~
// 1. Find host element const parent = document.querySelector(selector) // 3. Append to host parent.innerHTML = '' parent.appendChild(el)
Change to
// 1. Find host element const parent = querySelector(selector) // 3. Append to host insert(el, parent)
How? We need to provide a new API for users to choose
const Vue = { // Expansibility createRenderer({ querySelector, insert }) { // Return to renderer return { createApp(options) { // Return app object return { mount(selector) { // 1. Find host element const parent = querySelector(selector) // 2. Render 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) } }
After initialization, the next thing we need to consider is how to implement responsive data
2. Responsive
I believe many small partners who have not contacted Vue3 have heard that Vue3's response is based on Proxy
However, Vue2 implements data response based on Object.defineProperty, which has many disadvantages: (for this content, please refer to my previous blog post [Vue source code] data response principle)
- Recursive efficiency problem
- Array problem (implemented in a separate set)
- API impact (adding and deleting Vue.delete / set of dynamic attributes)
- class collection data structures are not supported
① reactive
Let's explore. First, use reactive to create a responsive object. Then, after two seconds, the value of title will change and the page will change.
const app = Vue.createApp({ setup() { const state = reactive({ title: 'hello, vue3 YK bacterium' }) setTimeout(() => { state.title = '2 See a new one in seconds YK bacterium' }, 2000) return state } }) app.mount('#app')
We create a reactive function to intercept the user's access to the proxy object, so as to respond when the value changes.
// The content intercepts the user's access to the proxy object, so as to respond when the value changes function reactive(obj) { // Returns 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 } }) }
We add an update function to the returned app object, write the code before rendering the page, and then call it once.
// 2.2 users write render directly this.update = function () { // Execute the render function (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()
As you can see, our page is responsive. The content will change in two seconds
Take a break. The village head teacher sings a song for everyone~~~~
② Dependency collection
There is a line of app.update() in the above written reactive function, where the method of app is called, which is strongly coupled with our app. We need a mechanism (publish and subscribe) to decouple such behavior: establish dependencies between those responsive data and their associated update functions
Establish mapping relationship: rely on dep - > component update function
Vue2 uses watcher. See my previous blog for specific implementation [Vue source code] data response 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 obtain dependencies in set
Let's implement the following:
First, we define a side effect function and receive a function as the parameter fn. The first step is to execute the fn function
function effect(fn) { // 1. Execute once fn fn() }
But there is a problem here, that is, what should we do if the function reports an error when executing fn, so we write a higher-order function to wrap our fn
First, create a stack effectStack to temporarily store side-effect functions
const effectStack = []
Why use the stack here? Because nesting may occur when calling the side effect function effect, you can collect eff well by using a data structure such as stack
Upgrade our effect function
function effect(fn) { // 1. Execute once fn // fn() const eff = function () { try { effectStack.push(eff) fn() } finally { effectStack.pop() } } // Call once immediately eff() // Return this function return eff }
We also need to build such a dependency data structure
const targetMap = { // Such data and dependencies should be stored in it // state: { // 'title': [update] // } }
Then define a kick function to establish the relationship between target, key and side-effect functions stored in effectStack
// Establish the relationship between target,key and side effect functions stored in effectStack function track(target, key) { // Take out the last element that stores the side effect function const effect = effectStack[effectStack.length - 1] // This is the case when writing is dead, but you can't write like this, or you'll create a new object every time // targetMap[target] = {} // targetMap[target][key] = [effect] // Therefore, you should first judge whether the object with target key exists let map = targetMap[target] if (!map) { // get this target for the first time [initialize the map if it doesn't exist] map = targetMap[target] = {} } let deps = map[key] if (!deps) { deps = map[key] = [] } // Mapping relationship establishment if (deps.indexOf(effect) === -1) { deps.push(effect) } }
Then 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()) } } }
We need to add dependencies in the getter and trigger updates in the setter, so we set this 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 update trigger(target, key) } }) }
Finally, let's test it with test cases
// Create responsive data obj const obj = reactive({ foo: 'foo' }) // Create a side effect function to internally trigger responsive data effect(() => { // Trigger responsive data console.log(obj.foo) }) // Change foo attribute in obj obj.foo = 'foo Changed~~~'
Let's deal with the relationship
Finally, we need to improve our code. We need to create side effects from the update function we wrote before, so that when the data changes, we can execute the update function again
Therefore, we only need to wrap the previously written function with a layer of effect higher-order function
// 2.2 users write render directly this.update = effect(() => { // Execute the render function (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()
At this point, we delete our test cases to see the effect of actual use in setup:
In fact, in the source code, the data structure adopted by targetMap is not an object, but a Map, and it is a WeakMap
const targetMap = new WeakMap()
Some operations can be changed to get and set operations of the corresponding Map
// let map = targetMap[target] let map = targetMap.get(target)
// map = targetMap[target] = {} map = targetMap.set(target, {})
When storing dependencies, you should use a data structure such as Set, which can automatically remove duplicates
// deps = map[key] = [] deps = map[key] = new Set() // Mapping relationship establishment // if (deps.indexOf(effect) === -1) { // deps.push(effect) // } deps.add(effect)
The above response has been completed, but it has a serious efficiency problem, that is, we use the full update method to update our DOM, which must be bad, so this leads to the virtual Dom and diff algorithm we want to talk about below
3. Virtual DOM
What is virtual DOM (vnode)?
- vnode is a js object used to describe the view
Why introduce vnode?
- Reduce DOM operations
- Efficient update
- Cross platform, compatibility
Let's define our virtual DOM - vnode
We want to turn the real DOM in the render function returned in the compile function into a virtual dom
return function render() { // const h3 = document.createElement('h3') // h3.textContent = this.title // return h3 // 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), // ]) }
The function of defining an h function is to represent a DOM with a js object
// Incoming information, return vnode, description view function h(tag, props, children) { return { tag, props, children } }
The update function will also change
this.update = effect(() => { const vnode = options.render.call(proxy) // Convert vnode to dom // Initialize the creation of the entire tree if (!this.isMounted) { // Implement createElm, overall creation, vnode - > el const el = this.createElm(vnode) parent.innerHTML = '' insert(el, parent) // init initialization, set mounted ID this.isMounted = true } })
Next, the createElm function is implemented in the form of recursion
createElm({ tag, props, children }) { // Traverse vnode and create the whole tree const el = createElement(tag) // If there are properties, set them (omitted) // el.setAttribute(key, val) // recursion // Determine whether children is a string if (typeof children === 'string') { el.textContent = children } else { children.forEach(child => insert(this.createElm(child), el)) } return el }
4. diff
See my previous blog for more details [Vue source code] graphical diff algorithm
To save a real node in createElm
// The real DOM should be saved in vnode for future updates vnode.el = el
Modify the update function, complete the first mount part, and add the update logic
this.update = effect(() => { const vnode = options.render.call(proxy) // Convert vnode to dom // Initialize the creation of the entire tree if (!this.isMounted) { // Implement createElm, overall creation, vnode - > el const el = this.createElm(vnode) parent.innerHTML = '' insert(el, parent) // init initialization, set mounted ID this.isMounted = true } else { this.patch(this._vnode, vnode) } this._vnode = vnode })
patch update
patch(oldNode, newNode) { const el = newNode.el = oldNode.el // 1. Update: the same node must be updated // What are the same nodes if (oldNode.tag === newNode.tag && oldNode.key === newNode.key) { // Update same node, update 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 the text with a set of child elements, empty, 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 different nodes, replace } },
Set this to update the data: from string to array
setup() { const state = reactive({ title: 'hello, vue3 YK bacterium' }) setTimeout(() => { state.title = '2 See a new one in seconds YK bacterium'.split("") }, 2000) return state }
effect
diff comparison
Vue3 is basically the same as Vue2. In my previous blog post, I also drew a comparison diagram of diff in Vue2. You can take a look at that article [Vue source code] graphical diff algorithm
Here we simplify and update in the simplest and crudest way
updateChildren(el, oldChild, newChild) { // 1. Obtain the shorter one of newCh and oldCh const len = Math.min(oldChild.length, newChild.length) // Forced update for (let i = 0; i < len; i++) { this.patch(oldChild[i], newChild[i]) } // Process remaining elements // Many new array elements if (newChild.length > oldChild.length) { // Batch create and append // Intercept 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) }) } },