When researching Vue source code, I learned about Virtual DOM, and the role of Virtual DOM. Let’s share what the Virtual DOM is and what it does. And a more common Virtual DOM library use and source code analysis.

What is the Virtual DOM

  • The Virtual DOM is a JS object that describes the DOM object. It is called the Virtual DOM because it is not a real DOM.
  • The format of the Virtual DOM is as follows:
{
  sel: "div".data: {},
  children: undefined.text: "Hello Virtual DOM".elm: undefined.key: undefined
}
Copy the code

Why use the Virtual DOM

  • Manipulating the DOM manually can be tricky, and although libraries such as JQuery simplify DOM manipulation, it becomes increasingly difficult as the complexity of a project increases
  • We can use a template engine to simplify view operations, but the template engine does not solve the problem of tracking state changes
  • The advantage of Virtual DOM is that it is not necessary to update DOM immediately. When data changes, it only needs to create a Virtual tree to describe DOM, which can track and record the state of the last time and update the real DOM by comparing the state differences between the two before and after, which can improve the performance of rendering in complex projects
  • In addition to DOM rendering, you can also implement SSR rendering (nuxt.js/ Next-js), Native applications (Weex /React Native), small programs, etc

Snabbdom (Virtual DOM library)

  • Snabbdom document
  • The Virtual DOM used internally in Vue 2.x is a modified Snabbdom
  • Approximately 200 SLOC (single line of code)
  • Module extensible, source developed in TypeScript, one of the fastest Virtual DOM

Basic use of Snabbdom

  • The packaging tool I use is Parcel, not WebPack, because I can run a simple demo with zero configuration
Create project directory
md snabbdom-demo
Enter the project directory
cd snabbdom-demo
# to create package. Json
npm init -y
Install parcel locally
npm install parcel-bundler -D
Note that the latest version of the SNabbDOM has some problems. You are advised to install the latest version of the SNabbDOMNPM install [email protected] - SCopy the code
  • Create the corresponding directory structure
└─ SRC index.js -index.html -package. json ├ ─ SRC index.jsCopy the code
  • Configure scripts for package.json
"scripts": {
 "dev": "parcel index.html --open"."build": "parcel build index.html"
}
Copy the code
  • Snabbdom import
// SnabbDOM is a commonJs syntax. If you want to use ES6 syntax in your project, you need to pay attention to how you import it

// commonJs
let snabbdom = require('snabbdom')

// ES6
import * as snabbdom from 'snabbdom'orimport { h, init, thunk} from 'snabbdom'
Copy the code

The use of Snabbdom

First, a brief introduction to the common functions of several methods

  • Use the h() function to create JavaScript objects (vNodes) that describe the real DOM
  • Init () sets the module, creates the patch()
  • Patch () compares the old and new vNodes and takes two arguments. The first argument can be the real DOM or VNode and the second argument is the new VNode
  • Update the changed content to the real DOM tree

Let’s go to code

  • Let’s write a simple example of Hello World.

There is no need to use the Virtual DOM in very simple projects, but this is just for demonstration purposes

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>snabbdom-demo</title>
</head>
<body>
  <div id="app"></div>
  <script src="./src/index.js"></script>
</body>
</html>
Copy the code
import * as snabbdom from 'snabbdom'

let patch = snabbdom.init([])

let vnode = snabbdom.h('div#app.app'.'Hello World')
let app = document.querySelector('#app')

let oldVnode = patch(app, vnode) // Keep the last vNode

setTimeout(() = > { // Update the DOM after two seconds
  vnode = snabbdom.h('div#app.app'.'Hello World1')
  oldVnode = patch(oldVnode, vnode)
}, 2000);

setTimeout(() = > {
  vnode = snabbdom.h('div#app.app'.'Hello World2')
  oldVnode = patch(oldVnode, vnode)
}, 4000);
Copy the code
  • Let’s take an example that contains child nodes
import * as snabbdom from 'snabbdom'
// Here are some common modules, and a few more are on the official website
import { classModule } from 'snabbdom/modules/class' // A class that can be dynamically switched based on variables
import { propsModule } from 'snabbdom/modules/props' // Sets a non-boolean property
import { attributesModule } from 'snabbdom/modules/attributes' // Sets a non-boolean property
import { datasetModule } from 'snabbdom/modules/dataset' // Set the data-* property
import { styleModule } from 'snabbdom/modules/style' // Set the inline style
import { eventListenersModule } from 'snabbdom/modules/eventlisteners' // Register and remove events

let patch = snabbdom.init([
  classModule,
  propsModule,
  attributesModule,
  datasetModule,
  styleModule,
  eventListenersModule
])

function handleClick(num) {
  console.log('Clicked' + num)
}

let app = document.querySelector('#app')

let vnode = snabbdom.h('div#app', {
  style: {
    color: 'pink'
  },
  dataset: {
    id: 1
  },
  props: {
    title: 'hello world'
  },
  on: {
    click: [handleClick, 1] // If you don't want to pass parameters, you can write handleClick
  }
},[
  snabbdom.h('p'.'Hello.')])let oldVnode = patch(app, vnode)
Copy the code
  • The last list that can be sorted, added, and removed
<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>snabbdom-demo</title>
  <style>* {margin: 0;
      padding: 0;
    }
    html.body{
      height: 100%;
      background-color: # 000000;
      color: #ffffff;
      font-size: 14px;
    }
    #app{
      width: 600px;
      padding-top: 50px;
      margin-left: 100px;
    }
    .h1{
      margin-bottom: 15px;
    }
    .options_box{
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin-bottom: 15px;
    }
    .options_box .left_btn {
      margin-right: 10px;
    }
    .options_box .left_btn..options_box .right_btn{
      cursor: pointer;
      padding: 5px 10px;
      background-color: #1d1717;
    }
    .options_box .left_btn.active..options_box .right_btn.active{
      background-color: #524e4e;
    }

    .list_box .list{
      background-color: #524e4e;
      margin-bottom: 10px;
    }
    .list_box .list .btn_box{
      overflow: hidden;
    }
    .list_box .list .btn_box .btn{
      cursor: pointer;
      float: right;
      padding: 5px 10px;
    }
    .list_box .list .row{
      display: flex;
      align-items: center;
      padding: 0 15px 15px;
    }
    .list_box .list .row .rank{
      width: 30px;
    }
    .list_box .list .row .title{
      flex: 1;
    }
    .list_box .list .row .desc{
      margin-left: 20px;
      flex: 3;
    }
  </style>
</head>
<body>
  <div id="app"></div>
  <script src="./src/index.js"></script>
</body>
</html>
Copy the code
import {h, init} from 'snabbdom'
// Here are some common modules, and a few more are on the official website
import { classModule } from 'snabbdom/modules/class' // A class that can be dynamically switched based on variables
import { propsModule } from 'snabbdom/modules/props' // Sets a non-boolean property
import { styleModule } from 'snabbdom/modules/style' // Set the inline style
import { eventListenersModule } from 'snabbdom/modules/eventlisteners' // Register and remove events

let patch = init([
  classModule,
  propsModule,
  styleModule,
  eventListenersModule
])

let oldVnode = null / / the old node
let types = ' ' // The current sort condition

let tableData = [
  { rank: 3.title: 'The Godfather: Part II'.desc: 'The early life and career of Vito Lake Tahoe, Nevada to pre-revolution 1958 Cuba.'.elmHeight: 0 },
  { rank: 1.title: 'The Shawshank Redemption'.desc: 'Two imprisoned men bond over a '.elmHeight: 0 },
  { rank: 7.title: '12 Angry Men'.desc: 'A dissenting juror in a murder trial slowly ma as it seemed in court.'.elmHeight: 0 },
  { rank: 4.title: 'The Dark Knight'.desc: 'When the menace known as the Joker wreaks h ability to fight injustice.'.elmHeight: 0 },
  { rank: 9.title: 'The Lord of the Rings: The Return of the King'.desc: 'Gandalf and A from Frodo and Sam aDoom with the One Ring.'.elmHeight: 0 },
  { rank: 6.title: 'Schindler\'s List'.desc: 'In Poland during World War II, Oskar Schinessing their persecution by the Nazis.'.elmHeight: 0 },
  { rank: 2.title: 'The Godfather'.desc: 'The aging patriarch of an organized crime son.'.elmHeight: 0 },
  { rank: 8.title: 'The Good, the Bad and the Ugly'.desc: 'A bounty hunting scam joins rtune in gold buried in a remote cemetery.'.elmHeight: 0 },
  { rank: 5.title: 'Pulp Fiction'.desc: 'The lives of two mob hit men, a boxer, a gangs violence and redemption.'.elmHeight: 0 },
  { rank: 10.title: 'Fight Club'.desc: 'An insomniac office worker looking for a way t they form an into something much, much more... '.elmHeight: 0},]let rankId = tableData.length // The last rank value

let app = document.querySelector('#app')

// Delete a line
function handleRemove(row, index) {
  tableData.splice(index, 1)
  oldVnode = patch(oldVnode, returnVnode(tableData))
}
// Add a line
function addRow() {
  let data = {
    rank: ++rankId,
    title: 'I am the first to add${rankId - 10}Data `.desc: 'I am the first to add${rankId - 10}Data `.elmHeight: 0
  }
  tableData.push(data)
  oldVnode = patch(oldVnode, returnVnode(tableData))
}
// Create each row of the table
function tableRow(row, index) {
  return h('div.list', [
    h('p.btn_box', [
      h('span.btn', {
        on: {
          click: [handleRemove, row, index]
        }
      }, 'x')
    ]),
    h('div.row', [
      h('div.rank', row.rank),
      h('div.title', row.title),
      h('div.desc', row.desc),
    ])
  ])
}

/ / sorting
function changeSort(type) {
  types = type
  tableData.sort((a, b) = > {
    if(typeof a[type] === 'number') {
      return a[type] - b[type]
    }else {
      return a[type].localeCompare(b[type])
    }
  })
  oldVnode = patch(oldVnode, returnVnode(tableData))
}

/ / return vnode
function returnVnode(data) {
  return h('div#app', [
    h('h1.h1'.'Top 10 movies'),
    h('div.options_box', [
      h('div.left', [
        h('span.left_btn', {
          class: {active: types === 'rank'},
          on: {
            click: [changeSort, 'rank']}},'rank'),
        h('span.left_btn', {
          class: {active: types === 'title'},
          on: {
            click: [changeSort, 'title']}},'title'),
      ]),
      h('span.right_btn', {on: {
          click: addRow
        }
      }, 'add')
    ]),
    h('div.list_box', data.map(tableRow))
  ])
}

oldVnode = patch(app, returnVnode(tableData))
Copy the code

Snabbdom source code analysis

The core of Snabbdom

  • Use the h() function to create JavaScript objects (vNodes) that describe the real DOM
  • Init () sets the module, creates the patch()
  • Patch () compares the old and new vNodes and takes two arguments. The first argument can be the real DOM or VNode and the second argument is the new VNode
  • Update the changed content to the real DOM tree

The source code of Snabbdom

  • Source address snabbdom
  • SRC directory structure
│ ├ ─ h. tesh (), │ htmldomAPi.ts type declaration file │ is.ts type declaration file │ jsx-global.d.ts type declaration file │ jsx.ts type declaration file │ htmldomAPI JSX │ snabbdom.bundle.ts init/h/thunk │ snabbdom.ts │ ├─helpers │ attachto. Ts defines the AttachData structure in VNode ├ ─modules Attributes. Ts class. Ts dataset.ts eventlisteners. Ts hero props.ts style.tsCopy the code

H function

  • The h function is often seen when using VUE
new Vue({
 router,
 store,
 render: h= > h(App)
}).$mount('#app')
Copy the code
  • Snabbdom’s h function is used to create a VNode, which takes advantage of the idea of function overloading, executing different functions depending on the number or type of arguments passed in.
// overloading the h function
export function h(sel: string) :VNode;
export function h(sel: string, data: VNodeData | null) :VNode;
export function h(sel: string, children: VNodeChildren) :VNode;
export function h(sel: string, data: VNodeData | null, children:
VNodeChildren) :VNode;
export function h(sel: any, b? : any, c? : any) :VNode {
 var data: VNodeData = {}, children: any, text: any, i: number;
 // Handle arguments to implement overloading mechanisms
 if(c ! = =undefined) {
  // Handle the case with three parameters
  // sel, data, children/text
  if(b ! = =null) { data = b; }
  if (is.array(c)) { children = c; }
  // If c is a string or number
  else if (is.primitive(c)) { text = c; }
  // If c is VNode
  else if(c && c.sel) { children = [c]; }}else if(b ! = =undefined&& b ! = =null) {
  // Handle the case with two arguments
  // if b is array
  if (is.array(b)) { children = b; }
  // If b is a string or number
  else if (is.primitive(b)) { text = b; }
  // If b is VNode
  else if (b && b.sel) { children = [b]; }
  else{ data = b; }}if(children ! = =undefined) {
  // Handle primitive values in children (string/number)
  for (i = 0; i < children.length; ++i) {
   // If child is string/number, create a text node
   if (is.primitive(children[i])) children[i] = vnode(undefined.undefined.undefined, children[i], undefined); }}if (
  sel[0= = ='s' && sel[1= = ='v' && sel[2= = ='g' &&
 (sel.length === 3 || sel[3= = ='. ' || sel[3= = =The '#')) {// If SVG is used, add namespace
  addNS(data, children, sel);
}
 / / return VNode
 return vnode(sel, data, children, text, undefined);
};
// Export the module
export default h;
Copy the code

VNode

A VNode is a virtual node that describes a DOM element. Let’s examine the vnode.ts of snabbDOM

export interface VNode {
 / / selector
 sel: string | undefined;
 // Node data: attributes/styles/events etc
 data: VNodeData | undefined;
 // Child nodes, and text are mutually exclusive
 children: Array<VNode | string> | undefined;
 // Record the actual DOM corresponding to the vNode
 elm: Node | undefined;
 // The contents of this node are mutually exclusive with children
 text: string | undefined;
 / / optimization
 key: Key | undefined;
}
export function vnode(sel: string | undefined,
           data: any | undefined,
           children: Array<VNode | string> | undefined,
           text: string | undefined,
           elm: Element | Text | undefined) :VNode {
 let key = data === undefined ? undefined : data.key;
 return {sel, data, children, text, elm, key};
}
export default vnode;
Copy the code

Snabbdom.ts

Analyze the snabbdom.ts file

  • patch(oldVnode, newVnode)
  • Patch, render the changed content of the new node into the real DOM, and finally return the new node as the old node for the next processing
  • Check whether the old and new VNodes have the same key and SEL.
  • If it is not the same node, delete the previous content and re-render
  • If the node is the same, check whether the new VNode has a text. If the text is different from that of the oldVnode, update the text directly
  • If the new VNode has children, the diff algorithm is used to determine whether the child node has changed
  • The diff procedure only makes same-level comparisons

The init function

  • Init (modules, domApi), return patch() function (higher order function). Modules is an array of extensions, and domApi is an object that contains DOM operations

The patch function

  • function
    • Pass in the old and new VNodes, compare the differences, and render the differences into the DOM
    • Return the new VNode as the oldVnode for the next patch()
  • Implementation process
    • Start by executing the hook function pre in the module
    • If oldVnode and vnode are the same (the key and SEL are the same), call patchVnode() to find the difference and update the DOM
    • If oldVnode is a DOM element. Call createElm() to convert vNode to a real DOM. Elm ③ Insert the newly created DOM element into the parent node ④ Remove the old node. Triggers the user-set CREATE hook function
return function patch(oldVnode: VNode | Element, vnode: VNode) :VNode {
 let i: number, elm: Node, parent: Node;
 // Save the queue of newly inserted nodes in order to trigger the hook function
 const insertedVnodeQueue: VNodeQueue = [];
 // Execute the module's Pre hook function
 for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
 // If oldVnode is not VNode, create VNode and set elm
 if(! isVnode(oldVnode)) {// Convert the DOM element to an empty VNode
  oldVnode = emptyNodeAt(oldVnode);
}
 // If the new and old nodes are the same node (key and SEL are the same)
 if (sameVnode(oldVnode, vnode)) {
  // Find the node differences and update the DOM
  patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else {
  // If the old and new nodes are different, vNode creates the corresponding DOM
  // Get the current DOM elementelm = oldVnode.elm! ; parent = api.parentNode(elm);// Trigger init/create hook function to create DOM
  createElm(vnode, insertedVnodeQueue);
  if(parent ! = =null) {
   // If the parent node is not empty, insert the DOM corresponding to the vnode into the documentapi.insertBefore(parent, vnode.elm! , api.nextSibling(elm));// Remove the old node
   removeVnodes(parent, [oldVnode], 0.0); }}// Execute the user-set insert hook function
 for (i = 0; i < insertedVnodeQueue.length; ++i) { insertedVnodeQueue[i].data! .hook! .insert! (insertedVnodeQueue[i]); }// Execute the module's POST hook function
 for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
 / / return vnode
 return vnode;
};
Copy the code

CreateElm function

  • CreateElm (vnode, insertedVnodeQueue) createElm(vnode, insertedVnodeQueue
  • Implementation process
    • The user-set init hook function is triggered first
    • If the selector is! , create a comment node; If the selector is empty, a text node is created
    • If the selector is not empty. Create a DOM for vNodes that have children. Create a DOM for vNodes that have children. (4) If the vNode text value is string/number, create a text node and chase to the DOM tree. (5) Execute the user-set create hook function. (6) If the user-set insert hook function is available, Add vNodes to the queue

PatchVnode function

  • Function: patchVnode(oldVnode, vNode, insertedVnodeQueue), compare the difference between oldVnode and Vnode, render the difference to DOM
  • Implementation process
    • The user-set prePatch hook function is first executed
    • Execute the module’s CREATE hook function, followed by the user-set CREATE hook function
    • If vnode.text is set and does not equal oldvNode. text. If the old node has children, remove them all and set the textContent of the DOM element to vNode.text
    • If vnode.text is not defined
      • If oldvNode. children and vnode.children both have values. Call updateChildren() to compare and update the child nodes using the diff algorithm
      • If vNode.children has a value. OldVnode. Children have no value. Empty the DOM element and call addVnodes() to add child nodes in batches
      • If oldvNode. children has a value, vnode.children has no value. Call removeVnodes() to remove the children in batches
      • If oldvNode. text has a value. Empty the content of the DOM element
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue:VNodeQueue) {
 consthook = vnode.data? .hook;// Execute the user-set prepatch hook function firsthook? .prepatch? .(oldVnode, vnode);constelm = vnode.elm = oldVnode.elm! ;let oldCh = oldVnode.children as VNode[];
 let ch = vnode.children as VNode[];
 // Return if the old and new vNodes are the same
 if (oldVnode === vnode) return;
 if(vnode.data ! = =undefined) {
  // Execute the module's update hook function
  for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode,
vnode);
  // Execute the user-set update hook functionvnode.data.hook? .update? .(oldVnode, vnode); }// If vnode.text is not defined
 if (isUndef(vnode.text)) {
  // If both new and old nodes have children
  if (isDef(oldCh) && isDef(ch)) {
   // Use the diff algorithm to compare child nodes and update child nodes
   if(oldCh ! == ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue); }else if (isDef(ch)) {
   // If the new node has children, the old node has no children
   // Empty the dom element if the old node has text
   if (isDef(oldVnode.text)) api.setTextContent(elm, ' ');
   // Add child nodes in batches
   addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
 } else if (isDef(oldCh)) {
   // If the old node has children, the new node has no children
   // Remove child nodes in batches
   removeVnodes(elm, oldCh, 0, oldCh.length - 1);
 } else if (isDef(oldVnode.text)) {
   // If the old node has text, clear the DOM element
   api.setTextContent(elm, ' '); }}else if(oldVnode.text ! == vnode.text) {// If vnode.text is not set
  if (isDef(oldCh)) {
   // If the old node has children, remove it
   removeVnodes(elm, oldCh, 0, oldCh.length - 1);
 }
  // Set the DOM element's textContent to vnode.textapi.setTextContent(elm, vnode.text!) ; }// Finally execute the user-set postpatch hook functionhook? .postpatch? .(oldVnode, vnode); }Copy the code

UpdateChildren function

  • The core of the diff algorithm, comparing the children of the old and new nodes, updates the DOM
  • By debugging updateChildren, we find that two DOM operations are required for the case without a key, and only one DOM operation (moving DOM items) is required for the case with a key. Therefore, DOM operations can be reduced for the case with a key. It can better reflect the advantages of taking key.

Describe the execution process of Diff algorithm

Diff algorithm is an efficient algorithm to compare nodes in the same layer of the tree, avoiding layer by layer search traversal of the tree, so the time complexity is only O(n).

Diff algorithm has two notable features:

1. Comparisons will only be made at the same level, not across levels.

2. During the diff comparison, the loop is closed from both sides towards the middle.

The diff process:

OldStartIdx, newStartIdx, oldEndIdx, and newEndIdx are the indexes on both sides of the old and new vNodes.

2. This is followed by a while loop, in which oldStartIdx, newStartIdx, oldEndIdx, and newEndIdx gradually converge to the center. The exit condition of the while loop is until the start of the old node or the new node is greater than the end position.

There are four situations encountered in the while loop:

Case 1: When the start of the new and old VNodes is the same node, patchVnode can be used directly, and the start index of both the new and old VNodes is added by 1.

Case 2: When the end of the new and old VNodes is the same node, patchVnode can be used directly, and the end index of both the new and old VNodes is reduced by 1.

Case 3: If the start of the old VNode is the same as the end of the new VNode, it indicates that oldStartVnode has been replaced by oldEndVnode after the data update. At this time, after patchVnode, the current real DOM node needs to be moved to the back of oldEndVnode. Meanwhile, the start index of the old VNode increases by 1, and the end index of the new VNode decreases by 1.

Case 4: If the end of the old VNode is the same as the start of the new VNode, it indicates that oldEndVnode is ahead of oldStartVnode after the data update. At this time, after patchVnode, the current real DOM node needs to be moved to the front of oldStartVnode. Meanwhile, the end index of the old VNode is reduced by 1, and the start index of the new VNode is increased by 1.

3. The exit condition of the while loop is until the start of the old node or the new node is greater than the end position.

Case 1: If oldStartIdx is greater than oldEndIdx in the loop, then oldChildren completes the loop before newChildren, and the remaining nodes in newChildren are nodes that need to be added. Insert all nodes between [newStartIdx, newEndIdx] into the DOM

Situation 2: If newStartIdx is greater than newEndIdx in the loop, newChildren completes the loop before oldChildren, and the remaining nodes in oldChildren are nodes that need to be deleted. Delete all nodes between [oldStartIdx, oldEndIdx]