Implementation of mini Vue for Vue3 source code learning
Implement Mini Vue
Here we implement a concise version of the mini Vue framework, which includes three modules:
- Rendering system module;
- Responsive system module;
- Application entry module;
1, Implementation of rendering system
Rendering system, this module mainly includes three functions:
- Function 1: h function, used to return a VNode object;
- Function 2: Mount function, which is used to mount VNode to DOM;
- Function 3: patch function, which is used to compare two vnodes and decide how to deal with new vnodes;
h function – generate VNode
Implementation of h function: directly return a VNode object
Mount function – mount VNode
Implementation of mount function:
Step 1: create HTML elements according to tag and store them in el of vnode;
Step 2: handle props attribute
- If it starts with on, monitor the event;
- Ordinary attributes can be added directly through setAttribute;
Step 3: process child nodes
- If it is a string node, set textContent directly;
- If it is an array node, the traversal calls the mount function;
Patch function – compare two vnodes
The implementation of patch function can be divided into two cases:
n1 and n2 are different types of nodes:
- Find the El parent node of n1 and delete the el of the original n1 node;
- Mount n2 node to the el parent node of n1;
n1 and n2 nodes are the same:
Handling props:
- First, mount all props of the new node to el;
- Judge whether the props of the old node do not need to be on the new node. If not, delete the corresponding attribute;
Handling children:
- If the new node is a string type, call el textContent = newChildren;
- If the new node has a different string type:
- The old node is a string type
- l set the textContent of el to an empty string;
- If the old node is a string type, directly traverse the new node and mount it on el;
- The old node is also an array type
- Get the minimum length of the array;
- Traverse all nodes, new nodes and old nodes for path operation;
- If the length of the new node is longer, mount the remaining new nodes;
- If the length of the old node is longer, unload the remaining old nodes;
- The old node is a string type
const h = (tag, props, children) => { // Vnode -- > JavaScript object -- > {} return { tag, props, children } } const mount = (vNode, container) => { // vNode-->element // 1. Create a real original sound and keep it on vNode const el = vNode.el = document.createElement(vNode.tag) // 2. Processing props if (vNode.props) { for (const key in vNode.props) { const value = vNode.props[key] if (key.startsWith('on')) { // Binding event el.addEventListener(key.slice(2).toLowerCase(), value) } else { el.setAttribute(key, value) } } } // 3. Dealing with children if (vNode.children) { if (typeof vNode.children === 'string') { el.textContent = vNode.children } else { vNode.children.forEach(item => { mount(item, el) }) } } // 4. Mount el to the container container.appendChild(el) } const patch = (n1, n2) => { // 1. If n1 and n2 nodes have different type if (n1.tag !== n2.tag) { const n1ElParent = n1.el.parentElement n1ElParent.removeChild(n1.el) mount(n2, n1ElParent) } else { // 2.n1 and n2 nodes are of the same type // 2.1 take out the element object and save it in n2 const el = n2.el = n1.el // 2.2 processing props const newProps = n2.props || {} const oldProps = n1.props || {} // 2.2.1 processing newProps for (const key in newProps) { // For newProps, add attributes const newValue = newProps[key] const oldValue = oldProps[key] if (newValue !== oldValue) { if (key.startsWith('on')) { el.addEventListener(key.slice(2).toLowerCase(), newValue) } else { el.setAttribute(key, newValue) } } } // 2.2.2 handling oldProps for (const key in oldProps) { // For oldProps, simply delete the old attribute if (key.startsWith('on')) { el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key]) } if (!(key in newProps)) { el.removeAttribute(key) } } // 3. Dealing with children const newChildren = n2.children const oldChildren = n1.children // 3.1 newchildren itself is a string if (typeof newChildren === 'string') { // Boundary judgment if (typeof oldChildren === 'string') { if (newChildren !== oldChildren) { el.textContent = newChildren } } else { el.innerHTML = newChildren } } else { // 3.2 newchildren itself is an array // 3.2.1 oldchildren is a string if (typeof oldChildren === 'string') { el.innerHTML = '' newChildren.forEach(item => { mount(item, el) }) } else { // 3.2.2oldChildren is also an array // oldChildren: [v1, v2, v3, v8, v9] // newChildren: [v1, v5, v6] // 1. Take out the minimum length of the oldChildren and newChildren arrays and perform the patch operation // This corresponding diff algorithm has no key operation and is inefficient const commonLength=Math.min(oldChildren.length,newChildren.length) for(let i=0;i<commonLength;i++){ patch(oldChildren[i],newChildren[i]) } // 2.newChildren. length>oldChildren. Length -- > Add node if(newChildren.length>oldChildren.length){ newChildren.slice(oldChildren.length).forEach(item=>{ mount(item,el) }) } // 3.newChildren. length<oldChildren. Length -- > delete node if(newChildren.length<oldChildren.length){ oldChildren.slice(newChildren.length).forEach(item=>{ el.removeChild(item.el) }) } } } } }
2, Responsive system module
Dependent collection system
Vue2 implementation of responsive system
Implementation of responsive system Vue3
class Dep{ constructor(){ this.subscribes=new Set() } depend(){ if(activeEffect){ this.subscribes.add(activeEffect) } } notify(){ this.subscribes.forEach(effect=>{ effect() }) } } // Automatically collect dependencies let activeEffect=null function watchEffect(effect){ activeEffect=effect effect() activeEffect=null } const targetMap=new WeakMap() function getDep(target,key){ // 1. Take out the corresponding Map object according to the object (target) let depsMap=targetMap.get(target) if(!depsMap){ depsMap=new Map() targetMap.set(target,depsMap) } // 2. Take out the specific dep object let dep=depsMap.get(key) if(!dep){ dep=new Dep() depsMap.set(key,dep) } return dep } // Vue2 responsive data hijacking function reactive(raw){ Object.keys(raw).forEach(key=>{ const dep=getDep(raw,key) let value=raw[key] Object.defineProperty(raw,key,{ get(){ dep.depend() return value }, set(newValue){ if(value!==newValue){ value=newValue dep.notify() } } }) }) return raw } // Test code const info=reactive({counter:100,name:'coder'}) const foo=reactive({height:1.88}) // watchEffect1 watchEffect(function(){ console.log('effect1:',info.counter*2,info.name) }) // watchEffect2 watchEffect(function(){ console.log('effect2:',info.counter*info.counter) }) // watchEffect3 watchEffect(function(){ console.log('effect3:',info.counter+10,info.name) }) // watchEffect4 watchEffect(function(){ console.log('effect4:',foo.height) }) info.counter++ // info.name='kobe' // foo.height=2
Why did Vue3 choose Proxy?
Object.definedProperty is the property of the hijacked object. If an element is added:
- Then Vue2 needs to call definedProperty again, and the Proxy hijacks the whole object without special treatment;
Different of modified objects:
- When using defineProperty, we can trigger interception by modifying the original obj object;
- When using Proxy, the Proxy object must be modified, that is, the instance of Proxy can trigger interception;
Proxy can observe more types than defineProperty
- has: catcher of in operator;
- deleteProperty: the catcher of the delete operator;
- And other operations;
As a new standard, Proxy will be continuously optimized by browser manufacturers;
Disadvantages: Proxy is not compatible with ie, and there is no Polyfill. Defineproperty can support IE9
3, Framework API design
In this way, we know that from the perspective of framework, we need to have two parts:
- createApp is used to create an app object;
- The app object has a mount method, which can mount the root component to a dom element;