Implement the virtual DOM and diff

Based on the previous two articles

  • Handwritten Mini-VUE-1 responsive implementation
  • Handwriting mini-VUE-2 to implement setup, render

We have implemented reactive, effectWatch, setup, and render, respectively

But there is a performance problem:

// ... 
effectWatch(() = > {
    rootContainer.innerHTML = "";
    // const ele = rootComponent.render(context);
    // rootContainer.append(ele);
    const vnode = rootComponent.render(context);
    mountElement(vnode, rootContainer)
})
Copy the code

So every time you update the DOM, you destroy all the DOM and then you reinsert all the DOM;

Our goal: to achieve local updates, which will use the DIff algorithm;

Before defining the diff algorithm, let’s recall the vNode structure we defined:

  • tag
  • props
  • children

Therefore, diff algorithm compares the differences of VNode, which is also aimed at the above attributes.

How to update after comparison?

  • tag
el.replaceWith(newEl)
Copy the code
  • props
SetAttribute/removeAttribute
/ / - modification
/ / - new
/ / - removed
Copy the code
  • children
// children --> diff

// 1. newChidren -> string (oldChildren -> string oldChildren -> array)
// 2. newChildren -> array (oldChildren -> string oldchildren -> array)

Copy the code

Introduce diff comparisons

  • Modify the/core/index.jsFile code, introduceddiffCode:

import { effectWatch } from './reactivity/index.js'
import { mountElement, diff } from './renderer/index.js'
export function createApp(rootComponent) {
  return {
    mount(rootContainer) {
      const context = rootComponent.setup();
      let isMounted = false, preVnodeTree = null;
      effectWatch(() = > {
        if(! isMounted) {/ / initialization
          isMounted = true;
          rootContainer.innerHTML = "";
          const vnodeTree = rootComponent.render(context);
          mountElement(vnodeTree, rootContainer)
          preVnodeTree = vnodeTree;
        } else {
          console.log('update');
          // Use the diff algorithm for DOM updates
          constvnodeNewTree = rootComponent.render(context); diff(preVnodeTree, vnodeNewTree); preVnodeTree = vnodeNewTree; }})},}}Copy the code

instructions

  • isMountedFor performance reasons, consider DOMMount and updateBoth states are therefore usedisMountedVariables to distinguish
  • preVnodeTree: Because of the considerationThe UPDATED DOM is based on the old DOMTo update, that would have to be a variable to store itOld DOM, so usepreVnodeTreeTo represent the old DOM; It’s important to note that every timeAfter the DOM is mounted or updated, you need to update itpreVnodeTree
  • diff: Update when neededCompare and update the DOM; So here the diff algorithm returns no value, so let’s look at the implementation of the diff algorithm

Diff implementation details

File location: /core/renderer/index.js

  • Diff function design: receive old and new Vnodes, compare and update the old and new Vnodes;
// n1 --> old
// n2 --> new
export function diff(n1, n2) {}
Copy the code

Next, we implement the internal details:

As we mentioned in the previous section, diff updates three vNode attributes: Tag, props, and children.

  • Tag update, mainly using APIel.replaceWith(newEl)
// n1 --> old
// n2 --> new
export function diff(n1, n2) {
    console.log('n1-old: ', n1);
    console.log('n2-new: ', n2);

    // tag
    if(n1.tag ! == n2.tag) { n1.el.replaceWith(document.createElement(n2.tag))
    }
}
Copy the code

This line of code is a little bit easier to understand;

  • Props update: compares whether the keys and values of two objects are the same, and whether new objects are added or deleted
// Details: make sure n2 has a mountable EL
const el = (n2.el = n1.el);

// The props attribute has the following structure:
// new -> {id: 'foo', class: 'bar', a: 'a' }
// old -> {id: 'foo', class: 'bar1', a: 'a', b: 'b' }
const { props: oldProps } = n1;
const { props: newProps } = n2;

// update / add
if (oldProps && newProps) {
    // Check whether the key-value of the new props is consistent, if not, update;
    // If old does not exist, it is a new attribute.
    Object.keys(newProps).forEach((key) = > {
        const newVal = newProps[key]
        const oldVal = oldProps[key]
        if(newVal ! == oldVal) { el.setAttribute(key, newVal); }})}// delete
if (oldProps) {
    // Check whether the property is removed: the old dom has it, the new DOM does not, so remove the property
    Object.keys(oldProps).forEach((key) = > {
        if(! newProps[key]) { el.removeAttribute(key); }})}Copy the code

The above code and comments should be fairly understandable. There are two main steps to deal with different attributes: update/add, delete;

  • The children update:

Since we need to determine multiple types, we first define a utility function to determine the type:

function isNumberOrString(value) {
  return ['string'.'number'].includes(typeof value);
}
Copy the code

Next we look at children’s diff, the pseudocode logic is:

  • If the new object is a string or a number
    • Whether the old node is a string or a number
      • Compare whether the values are equal or not, and replace the old value with the new value
    • If the old node is an array
      • The new value replaces the old value
  • If the new node is an array
    • If the old node is a string or a number
      • Empty the value of the old node, leaving it as an empty container
      • Mount a new node (array) in the container
    • If old node is array:Compare the difference between two arrays. Here we use a simplified comparison, comparing the difference between a common VNode by the length of the array only, and then dealing with anything beyond the length
      • Gets the length of the common array, iterates over the value of each array, and diff recursively
      • If the length of the new node is greater than length, it indicates that the node is newly added. Then, the new node is mounted from the length position
      • If the length of the old node is greater than length, it indicates that the node is deleted. Then, the value of the old node is deleted from the parent node of the current node starting from length

Detail implementation:

// children -- diff
const { children: newChildren } = n2;
const { children: oldChildren } = n1;

// Both are strings/numbers
if (isNumberOrString(newChildren)) {
    // old
    if (isNumberOrString(oldChildren)) {
        if(newChildren ! == oldChildren) { el.textContent = newChildren } }else if(Array.isArray(oldChildren)) { el.textContent = newChildren; }}else if (Array.isArray(newChildren)) {
    if (isNumberOrString(oldChildren)) {
        el.innerText = ""
        mountElement(n2, el)
    } else if (Array.isArray(oldChildren)) {
        // new {a, b, c}
        // old {a, b, c, d}
        // In theory, each node should be diff compared recursively
        // As a matter of fact, to simplify the code, we'll do it by force

        // Handle public vNodes
        const length = Math.min(newChildren.length, oldChildren.length)
        for (let i=0; i<length; i++) {
            const newVnode = newChildren[i]
            const oldVnode = oldChildren[i]
            diff(oldVnode, newVnode);
        }

        // The node is added
        // old {a, b, c}
        // new {a, b, c, d}
        if (newChildren.length > length) {
            // Create a node
            for (let i=length; i < newChildren.length; i++) {
                const newVnode = newChildren[i]
                mountElement(newVnode, el)
            }
        }

        // Node deletion
        // old {a, b, c, d}
        // new {a, b, c}
        if (oldChildren.length > length) {
            for (let i = length; i<oldChildren.length; i++) {
                const oldVnode = oldChildren[i]
                el.parentNode.removeChild(oldVnode.el)
            }
        }
    }
}
Copy the code

At this point, through the above code, we achieved a simplified version of the DIff algorithm;

The mountElement method mentioned above, what does it do?

You can actually think of creating a DOM based on a vNode.

export function mountElement(vnode, container) {
  const { tag, props, children } = vnode;
  // tag
  const el = vnode.el = document.createElement(tag)
  // props
  for (let key in props) {
    const value = props[key]
    el.setAttribute(key, value);
  }
  // children--> string/number
  if (typeof children === 'string' || typeof children === 'number') {
    const textNode = document.createTextNode(children)
    el.appendChild(textNode); 
  } else if (Array.isArray(children)) {
    // Accept an array
    children.forEach((v) = > {
      mountElement(v, el);
    })
  }
  container.appendChild(el);
}
Copy the code

So far, the details of the DIff algorithm we have also implemented ~

validation

Open up the Chrome Console, click on the next-to-last tool icon from the far right (three dots), and then select Show Console Drawer.

Or press Esc in the upper left corner of the keyboard.

The purpose is to make it easy to use both the Console and Elements panels and watch the view’s DOM update locally when the console updates state

  • Test data preparation:
import { reactive } from "./core/reactivity/index.js";
import { h } from './core/h.js'

export default {
  render(context) {
    // const div = document.createElement('div');
    // div.innerHTML = context.state.count;
    // return div;
    return h('div',
            { id: 'div-wrapper'},
            [
              h('h1', 
                { id: 'div-test'.style: 'color: red; font-size: 24px; '.key: `key-${context.state.count}`  },
                context.state.count
               ),
              h('span',
                { class: 'span-str' }, 
                context.state.str
               ),
            ])
  },

  setup() {
    const state = reactive({
      count: 1.str: "Hello, World"
    })
    window.state = state;
    return{ state }; }}Copy the code
  • Test 1: Console input:
state.count ++
Copy the code

When the page is updated, not all of the DOM is refreshed, but the component containing state.count is locally updated

  • Test two: Mouse select control panelElementsIn the panel
<span class="span-str">Hello, World</span>
Copy the code

Then we type in the Console panel in the control panel:

$0.textContent = "123"
Copy the code

At this point, we can see that only the span selected above is updated;

At this point, the implementation of our simplified diff algorithm is finally complete