1. Introduction

It can be seen from vUE source code that VUE mainly contains the following three core modules:

  1. Compiler module: Compiler module (to compile a template into a render function)
  2. Renderer: rendering module (actually rendering the results of the Compiler module to the page)
  3. Reactivity module: reactive module (responds to changes when data changes)

This article implements a compact version of the Mini-Vue framework, which consists of three modules:

  • Render system module (Runtime -> vNode -> Real DOM)
  • Reactive Modules
  • Application entry Module (createApp)

2. Implementation of rendering system

This module mainly contains three functions:

  • Function h, which returns a VNode object
  • The mount function is used to mount vNodes to the DOM
  • Function 3. Patch function is used to compare two VNodes and determine how to deal with the new VNode

2.1 Implementation of h function

  • H: the h function returns aVirtual node, usually abbreviated toVNodeThree parameters are received:type.props 和 children, virtual nodes form the virtual DOM.

The virtual DOM is a lightweight JavaScript object created by a rendering function. It takes three arguments: an element, an object with data, prop, attR, and so on, and an array. Arrays are where we pass children, which also have all these parameters, and then they can have children, and so on, until we build the entire tree of elements.

function h(type, props, children) {
  return {
    type,
    props,
    children
  }
}
Copy the code

2.2 Implementation of mount function

  • The mount function is used to mount the virtual node to the page. It takes two parameters, the first being the vNode to mount and the second being the actual DOM node to mount
function mount(vnode, container) {
  // 1. Create real elements and keep el on vNode
  const el = vnode.el = document.createElement(vnode.type)

  // 2. Handle props
  if(vnode.props) {
    for(const key in vnode.props) {
      const value = vnode.props[key]
      
      // Determine if the attribute is an event attribute
      if(key.startsWith('on')) {
        el.addEventListener(key.slice(2).toLocaleLowerCase(), value)
      } else {
        el.setAttribute(key, value)
      }
    }
  }

  // 3
  if(vnode.children) {
    if(typeof vnode.children === 'string') {
      el.textContent = vnode.children
    } else if(vnode.children instanceof Array) {
      vnode.children.forEach(item= > {
        mount(item, el)
      })
    } else if (typeof vnode.children === 'object') {
      mount(vnode.children, el)
    }
  }

  // 4. Mount it to the Container
  container.appendChild(el)
}
Copy the code

2.3 Implementation of patch function

  • The patch function compares two VNodes and determines how to treat the new vNode. It takes two arguments, the first for the old vNode and the second for the new vNode
function patch(n1, n2) {
  if(n1.type ! == n2.type) {// 1. If the label name is different, remove the original element and mount a new vNode
    const n1ParentEl = n1.el.parentElement
    n1ParentEl.removeChild(n1.el)
    mount(n2, n1ParentEl)
  } else {
    // 2 The label name is the same
    const el = n2.el = n1.el

    // 2.1 processing props
    const oldProps = n1.props || {}
    const newProps = n2.props || {}
    
    // Add new attributes
    for(const key in newProps) {
      const oldValue = oldProps[key]
      const newValue = newProps[key]
      if(oldValue ! == newValue) {if(key.startsWith('on')) {
          el.addEventListener(key.slice(2).toLocaleLowerCase(), oldValue)
        } else {
          el.setAttribute(key, newValue)
        }
      }
    }

    // Delete the old attributes
    for(const key in oldProps) {
      const oldValue = oldProps[key]
      const newValue = newProps[key]
      if(oldValue ! == newValue) {if(key.startsWith('on')) {
          el.removeEventListener(key.slice(2).toLocaleLowerCase(), value)
        } else {
          el.removeAttribute(key)
        }
      }
    }

    // Handle children
    const oldChildren = n1.children
    const newChildren = n2.children
    // The new children is a string
    if(typeof newChildren === 'string') {
      if(newChildren ! == oldChildren) { el.textContent = newChildren } }// The new children is an array
    else if (newChildren instanceof Array) {
      if(typeof oldChildren === 'string') {
      newChildren.forEach(item= > {
          mount(item, el)
        })
      } else {
      // Both new and old are arrays.
      const commonLength = Math.min(oldChildren.length, newChildren.length)
      for(let i = 0; i < commonLength; i++) {
          patch(oldChildren[i], newChildren[i])
        }

        if(newChildren.length > oldChildren) {
          newChildren.slice(oldChildren.length).forEach(item= > {
            mount(item, el)
          })
        }
        
        if(newChildren.length < oldChildren) {
          oldChildren.slice(oldChildren.length).forEach(item= > {
            el.removeChild(item.el)
          })
        }
      }
    }
  }
}
Copy the code

2.4 presentation

The test code

    <div id="app"></div>
    <button id="btn">CHANGE</button>
    <! Renderer.js contains the above h, mount, patch functions -->
    <script src="./renderer.js"></script>
    <script>
      // 1. Create a vnode using the h function
      const vnode1 = h("div", { class: "coder" }, [
        h("h2".null."Current count: 100"),
        h("button".null."+ 1"),]);// 2. Mount vnode to div#app using the mount function
      mount(vnode1, document.getElementById("app"));

      // 3. Create vnode2
      const vnode2 = h(
        "div",
        { class: "coder".style: "font-weight: 700; font-size: 30px;" },
        [h("h3".null."Ha ha ha."), h("b".null."Hey hey hey.")]);const btn = document.getElementById("btn");
      btn.addEventListener("click".(e) = > {
        patch(vnode1, vnode2);
      });
    </script>
Copy the code

The results of

3. Responsive systems

In my previous article, I explained waking up at 3am to implement the vUE responsive principle

  • Here’s the code
// Save the current collection of reactive functions
let activeReactiveFn = null

class Depend {
  constructor() {
  // Use Set to store dependent functions instead of arrays []
    this.reactiveFns = new Set()}notify() {
    this.reactiveFns.forEach(fn= > {
      fn()
    })
  }
  depend() {
    if(activeReactiveFn) {
      this.reactiveFns.add(activeReactiveFn)
    }
  }
}

WeakMap({key(object): value}); WeakMap({key(object): value}); WeakMap({key(object): value}); WeakMap({key(object): value}); WeakMap({key(object): value});
const targetMap = new WeakMap(a)// Encapsulate a function that gets depend
function getDepend(target, key) {
// The process of getting a map based on the target object
  let map = targetMap.get(target)
  if(! map) { map =new Map()
    targetMap.set(target, map)
  }
 // Get the Depend object by key
  let depend = map.get(key)
  if(! depend) { depend =new Depend()
    map.set(key, depend)
  }
  return depend
}

// Encapsulate a reactive function
function watchEffect(fn) {
  activeReactiveFn = fn
  fn()
  activeReactiveFn = null
}

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      // Get the corresponding depend based on target.key
      // Do dependency collection
      const depend = getDepend(target, key)
      depend.depend()
      return Reflect.get(target, key, receiver)
    },
    set(target, key, newValue , receiver) {
      Reflect.set(target, key, newValue, receiver)
      // Listen for changes in the object
      const depend = getDepend(target, key)
      depend.notify()
    }
  })
}

Copy the code

4. CreateApp implementation

  • CreateApp: Requires a root component instance to be passed in and a mount function to be provided
function createApp(rootComponent) {
  return {
    mount(selector) {
      const container = document.querySelector(selector);
      let isMounted = false;
      let oldVNode = null;

      // Monitor counter changes for page updates
      watchEffect(function() {
        if(! isMounted) {// Mount for the first time
          oldVNode = rootComponent.render();
          mount(oldVNode, container);
          isMounted = true;
        } else {
          // Perform patch operation when data is updated
          constnewVNode = rootComponent.render(); patch(oldVNode, newVNode); oldVNode = newVNode; }})}}}Copy the code

5. Final demonstration of Mini-Vue framework

  • The test code
<div id="app"></div>
<script src="./renderer.js"></script>
<script src="./reactive.js"></script>
<script src="./createApp.js"></script>
<script>
  const vnode1 = h("div", { class: "coder" }, [
    h("h2".null."Current count: 100"),
    h("button".null."+ 1"),]);const App = {
    data: reactive({
      counter: 0,}).render() {
      return h("div", { class: "coder" }, [
        h("h2".null.'Current count:The ${this.data.counter}`),
        h("button", {onClick: () = > {
              this.data.counter--; }},"1"
        ),
        h("button", {onClick: () = > {
              this.data.counter++; }},"+ 1"),]); }};const app = createApp(App);

  app.mount("#app");
Copy the code