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

  1. Parse the Template template into an abstract syntax tree (AST).
  2. Optimize AST.
  3. Generate the render function based on the AST.

Rendering system

  1. The reder function returns vNode
  2. A tree structure vDOM is formed between vNodes
  3. Generate the real DOM from the VDOM and render it to the browser

Responsive system

  1. The old and new VNodes are compared using diff algorithm
  2. 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 oneageAttributes, even thoughchangeIt’s not used in the functionage, we also triggerchangeFunction. 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