23_ Implementation of mini Vue for vue3 source code learning

Posted by mogster on Sun, 20 Feb 2022 09:44:30 +0100

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;

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;

Topics: Javascript Vue Vue.js