Preparation:

  1. What is the virtual DOM?

    Virtual DOM is basically a tree based on JavaScript objects (vNodes), which are described by object attributes. In fact, it is just a layer of abstraction from the real DOM. Eventually, the tree can be mapped to the real world through a series of operations. www.cnblogs.com/fundebug/p/…

  2. The role of the virtual DOM

    • Maintain the relationship between views and states
    • Improve rendering performance in complex view situations
    • cross-platform
      • Browser platforms render the DOM
      • Server render SSR (nuxt.js, Next-js)
      • Native applications (WEEx, React Native)
      • Applets (MPvue, Uni-app)
  3. Related virtual DOM libraries

    • Snabbdom (here are the Snabbdom uses)
    • virtual-dom

Create a project:

md snabbdom-demo
Copy the code
cd snabbdom-demo
Copy the code
npm init -y
Copy the code
npm i parcel-bundler -D
Copy the code

Parcel is then used to package the project

The configuration scripts:

"scripts": {
    "dev": "parcel index.html --open"."build": "parcel build index.html"
}
Copy the code

Project Contents:

SRC js files need to be imported into index.html

The introduction of snabbdom

Install snabbdom:

NPM I [email protected]Copy the code

The introduction of

import { init } from 'snabbdom/src/package/init'
import { h } from 'snabbdom/src/package/h'

const patch = init([])
Copy the code

The init and H functions are the core functions in the SNabbDOM

The init function passes in an array and returns a patch function that converts the virtual DOM into a real DOM and mounts it to the DOM tree

Import in the official example is direct import: github.com/snabbdom/sn…

import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
Copy the code

This is because webpack5 already supports using the exports field in package.json to define paths for external references:

However, such imports are not currently supported in a Parcel, so you need to import them by path

The basic use

import { init } from 'snabbdom/src/package/init'
import { h } from 'snabbdom/src/package/h'

const patch = init([])

// First argument: label + selector
// Second argument: if it is a string, the second argument will be used as the text content in the tag
let vnode = h('div#container.demo'.'hello')
let app = document.querySelector('#app')

// First argument: old vNode, which can be a DOM element
// Second argument: new vnode
// Return the new vnode. Generally, the returned vnode will be used as the "old vnode" during the next patch.
let oldVnode = patch(app, vnode)
Copy the code

There needs to be a placeholder for div#app in index.html:

When done, NPM run dev starts the project and you can see that the view and structure have become the corresponding nodes:

The second argument to h can also be passed to an array:

let vnode = h('div#container', [
  h('h1'.'title'),
  h('p'.'pppppppp')])Copy the code

If you want to render an empty node, you can pass it into the h function! :

let vode = h('! ')
Copy the code

Modules in the SNabbDOM

Function:

  • Snabbdom’s core library does not handle DOM element attributes, styles, events, and so on. This can be done by registering modules that SnabbDOM provides by default
  • Modules in snabbDOM can be used to extend the functionality of snabbDOM
  • Modules in snabbDOM are implemented by registering global hook functions

These modules are officially available:

  • Attributes: Sets vNode internal attributessetAttributeImplementation of the
  • Props: To set the internal properties of a Vnode, use object. Properties are implemented in a form that internally does not handle Boolean properties
  • The dataset: processingdata-Properties like that
  • Class: Toggle class styles
  • Style: Sets inline styles
  • Events are registered and removed

Use steps:

  1. Import the required modules
  2. initMethod to register a module
  3. hThe second argument position in the function uses the module
import { init } from "snabbdom/src/package/init";
import { h } from "snabbdom/src/package/h";


// Import the module
import { styleModule } from "snabbdom/src/package/modules/style";
import { eventListenersModule } from "snabbdom/src/package/modules/eventlisteners";

// Register module
const patch = init([
  styleModule,
  eventListenersModule
])

// Use the module
let vnode = h('div#container', [
  h('h1', { style: { backgroundColor: 'cyan'}},'hello'),
  h('p', { on: { click: eventHandler } }, 'ppp')])function eventHandler() {
  console.log(111111111111)}let app = document.getElementById('app')
patch(app, vnode)
Copy the code

Results can also be displayed normally in the browser, and events can also be registered normally:

Snabbdom

H function

Creates a vNode object

Analysis:

import { vnode, VNode, VNodeData } from './vnode'
import * as is from './is'

export type VNodes = VNode[]
export type VNodeChildElement = VNode | string | number | undefined | null
export type ArrayOrElement<T> = T | T[]
export type VNodeChildren = ArrayOrElement<VNodeChildElement>

function addNS (data: any, children: VNodes | undefined, sel: string | undefined) :void {
  data.ns = 'http://www.w3.org/2000/svg'
  if(sel ! = ='foreignObject'&& children ! = =undefined) {
    for (let i = 0; i < children.length; ++i) {
      const childData = children[i].data
      if(childData ! = =undefined) {
        addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel)
      }
    }
  }
}

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 = {}
  var children: any
  var text: any
  var i: number// Process parameters to implement overloadingif (c ! = =undefined) {// Handle three arguments --self,data,children/text
    if (b ! = =null) {
      data = b
    }
    if (is.array(c)) {
      // Check if c is an array, if so, store it in children
      children = c
    } else if (is.primitive(c)) {
      // Determine if c is a string or a number, and if so, store it in text
      text = c
    } else if (c && c.sel) {
      // Determine if c is a vNode object, and if so, convert it to an array and store it in children
      children = [c]
    }
  } else if(b ! = =undefined&& b ! = =null) {
    // Handle two arguments --self, data/children
    if (is.array(b)) {
      children = b
    } else if (is.primitive(b)) {
      text = b
    } else if (b && b.sel) {
      children = [b]
    } else { data = b }
  }
  if(children ! = =undefined) {
    // Check whether there is a value in children
    for (i = 0; i < children.length; ++i) {
      // Handle the case where the children element is number or string -- when the element is a primitive value type, create it as a text node using vNode
      if (is.primitive(children[i])) children[i] = vnode(undefined.undefined.undefined, children[i], undefined)}}// Check whether the current node is SVG
  if (
    sel[0= = ='s' && sel[1= = ='v' && sel[2= = ='g' &&
    (sel.length === 3 || sel[3= = ='. ' || sel[3= = =The '#')) {// Add a namespace
    addNS(data, children, sel)
  }
  // Create a vnode and return
  return vnode(sel, data, children, text, undefined)};Copy the code

Vnode function

Function: Returns a vNode object

Source:

export function vnode (sel: string | undefined,
  data: any | undefined,
  children: Array<VNode | string> | undefined,
  text: string | undefined,
  elm: Element | Text | undefined) :VNode {
  const key = data === undefined ? undefined : data.key
  return { sel, data, children, text, elm, key }
}
Copy the code

The patch function

Function: Compare the difference between two VNodes, render the difference on the page, and return the new Vnode as the oldvNode for the next patch

Source:

  function patch (oldVnode: VNode | Element, vnode: VNode) :VNode {
    // Define some internal variables
    let i: number.elm: Node, parent: Node
    // Stores a queue of newly inserted nodes. These are used to trigger hook functions when newly inserted nodes are inserted
    const insertedVnodeQueue: VNodeQueue = []
    // Triggers the module's Pre hook function
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()

    // Determine whether oldVNode is a VNode object, if not, convert it to VNode
    // isVnode Determines whether an oldVnode is a VNode by determining whether the oldVnode has sel
    if(! isVnode(oldVnode)) {// The emptyNodeAt method concatenates the element tag name +id selector + class selector and passes it to the vNode method to convert it to vNode
      / / - > vnode (the element tag name + + class selectors, id selectors {}, [], undefined, elm)
      oldVnode = emptyNodeAt(oldVnode)
    }

    // Check whether the old and new vNodes are the same
    // sameVnode determines whether vNodes are the same by determining whether the SEL and key attributes of the old and new vNodes are the same
    if (sameVnode(oldVnode, vnode)) {
      // Determine whether the contents of the vNode have changed
      patchVnode(oldVnode, vnode, insertedVnodeQueue)
    } else {
      // Save the old VNode DOM elements on ELM
      elm = oldVnode.elm!
      // Store the parent element of the old VNode dom element on parent
      // Get the parent element to mount the new node to the parent element after it is created
      parent = api.parentNode(elm) as Node

      // Convert the new vNode to a DOM element and save the DOM on vnode.elm
      createElm(vnode, insertedVnodeQueue)

      if(parent ! = =null) {
        // If parent is not null, mount it to the DOM treeapi.insertBefore(parent, vnode.elm! , api.nextSibling(elm))// Remove the oldVnode from the DOM
        removeVnodes(parent, [oldVnode], 0.0)}}// Triggers the inserted hook function
    for (i = 0; i < insertedVnodeQueue.length; ++i) { insertedVnodeQueue[i].data! .hook! .insert! (insertedVnodeQueue[i]) }// Triggers the POST hook function
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
    return vnode
  }
Copy the code

CreateElm function

Creates a DOM node

Source:

  function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue) :Node {
    // Define internal variables
    let i: any
    let data = vnode.data
    // User-defined init hook
    if(data ! = =undefined) {
      constinit = data.hook? .init// check whether init is undefined
      if (isDef(init)) {
        // init functions handle vNodes
        init(vnode)
        data = vnode.data
      }
    }
    // Save the child node and selector
    const children = vnode.children
    const sel = vnode.sel
    if (sel === '! ') {
      // sel === '! 'to create a comment node
      if (isUndef(vnode.text)) {
        vnode.text = ' '
      }
      vnode.elm = api.createComment(vnode.text!)
    } else if(sel ! = =undefined) {
      / / create the dom
      // Parse selector
      const hashIdx = sel.indexOf(The '#')
      const dotIdx = sel.indexOf('. ', hashIdx)
      const hash = hashIdx > 0 ? hashIdx : sel.length
      const dot = dotIdx > 0 ? dotIdx : sel.length
      consttag = hashIdx ! = = -1|| dotIdx ! = = -1 ? sel.slice(0.Math.min(hash, dot)) : sel
      const elm = vnode.elm = isDef(data) && isDef(i = data.ns)
        ? api.createElementNS(i, tag)
        : api.createElement(tag)
      if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot))
      if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g.' '))
      // Triggers the CREATE hook
      for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode)
      // Determine if there are child elements
      if (is.array(children)) {
        // If the child element is an array, iterate over the array,
        for (i = 0; i < children.length; ++i) {
          const ch = children[i]
          if(ch ! =null) {
            Call createElm recursively to create a DOM and mount it to the elm property of the current element
            api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue))
          }
        }
      } else if (is.primitive(vnode.text)) {
        // If the element is text, create a text node and mount it on ELM
        api.appendChild(elm, api.createTextNode(vnode.text))
      }
    // Triggers the user-defined CREATE hook
      consthook = vnode.data! .hookif(isDef(hook)) { hook.create? .(emptyNode, vnode)if (hook.insert) {
          // Put vNode's INSERT hook function into the INSERT hook queue, which is executed after the DOM tree is inserted
          insertedVnodeQueue.push(vnode)
        }
      }
    } else {
      // Create a text node
      vnode.elm = api.createTextNode(vnode.text!)
    }
    // Return the current VNode elm
    return vnode.elm
  }
Copy the code

RemoveVnodes function

Source:

  function removeVnodes (parentElm: Node,
    vnodes: VNode[],
    startIdx: number,
    endIdx: number) :void {
    // Iterate over the vNodes array with the start and end indexes
    for (; startIdx <= endIdx; ++startIdx) {
      // Define some internal variables
      let listeners: number
      let rm: () = > void
      const ch = vnodes[startIdx]
      if(ch ! =null) {
        // The current vnode is not empty
        // ch is treated as an element node when it has sel, otherwise as a text node
        if (isDef(ch.sel)) {
          // Trigger the destroy hook function
          invokeDestroyHook(ch)
          // Get the number of CBS remove hook functions + 1
          // Listeners delete DOM elements to prevent retransmissions
          listeners = cbs.remove.length + 1
          // createRmCb returns a function that removes the elementrm = createRmCb(ch.elm! , listeners)// Iterate over the remove hook function, triggering the remove function in turn
          for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm)
          constremoveHook = ch? .data? .hook? .removeif (isDef(removeHook)) {
            removeHook(ch, rm)
          } else {
            rm()
          }
        } else { // Text node
          // When the current vNode is a text node, call removeChildren to remove itapi.removeChild(parentElm, ch.elm!) }}}}Copy the code

The RM function returned in createRmCb function will reduce listenters by 1 and then determine whether it is 0. If it is 0, the listenters will be deleted:

  function createRmCb (childElm: Node, listeners: number) {
    return function rmCb () {
      if (--listeners === 0) {
        const parent = api.parentNode(childElm) as Node
        api.removeChild(parent, childElm)
      }
    }
  }
Copy the code

PatchNode function

General process of patchNode function execution:

Source:

  function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    // Trigger prePatch and update hooks
    consthook = vnode.data? .hook// If there is a prepatch hook passed by the user, execute it immediatelyhook? .prepatch? .(oldVnode, vnode)const elm = vnode.elm = oldVnode.elm!
    const oldCh = oldVnode.children as VNode[]
    const ch = vnode.children as VNode[]
    // If the node is the same, end the patch
    if (oldVnode === vnode) return
    // Execute the CBS update hook and the user-passed update hook
    if(vnode.data ! = =undefined) {
      for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) vnode.data.hook? .update? .(oldVnode, vnode) }// diff old and new vNodes
    // Check whether vNode text is empty: if it is empty, vnode contains only element nodes; otherwise, vnode contains only text nodes
    if (isUndef(vnode.text)) {
      // Check whether the old and new nodes have children
      if (isDef(oldCh) && isDef(ch)) {
        // Both have children and the children are not the same when the updateChildren function is called
        The updateChildren function compares all child nodes and updates the DOM
        if(oldCh ! == ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue) }else if (isDef(ch)) {
        // Only new nodes have children
        // If oldVnode has text nodes, clear them
        if (isDef(oldVnode.text)) api.setTextContent(elm, ' ')
        // Call addVnodes to insert the new node into the old node
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        // Only old nodes have children
        // Remove the child node from the old node
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)}else if (isDef(oldVnode.text)) {
        // If the old node has a text attribute, clear it
        api.setTextContent(elm, ' ')}}else if(oldVnode.text ! == vnode.text) {// The text of the new and old nodes is not equal
      // Check whether the old node has child nodes. If so, delete the child nodes
      if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)}// Replace the text
      api.setTextContent(elm, vnode.text!)
    }
    // Triggers the postPatch hookhook? .postpatch? .(oldVnode, vnode) }Copy the code

UpdateChildren function

The updateChildren function, which uses the diff algorithm, is triggered when both the old and new nodes have children

The diff algorithm:

The diff algorithm in snabbDOM dif only peer nodes

In the process of diff, four situations are compared:

  • Comparison of the old start node with the new start node
  • Comparison between the old end node and the old end node
  • Comparison of the old start node to the new end node
  • Comparison between the old end node and the new start node

  1. Comparison between the old and new start nodes

    The comparison starts from the start position of the old and new nodes. If the old and new start nodes are sameVnode, call patchNode to compare the nodes and update the difference

    Then add oldStartIndex/newStartIndex from 1

  2. Comparison between old and new end nodes

    In contrast to the old and new start nodes, it compares from the end position

  3. Old start node and new end node

    The old start node is compared with the new end node. If it is the same node, the patchVnode function is called to compare the node update differences

    Then move the node corresponding to oldStartIndex to the right to update the index

  4. Old end node and new start node

    In contrast to the three

  5. If none of the above four conditions are met

    Walk through the new start node to see if there are any nodes with the same key value as the old node:

    1. If not, create a new node with the vNode and insert it in front of the old node
    2. If found, then compare the SEL attribute of the two nodes with the same key value. If the SEL attribute is not the same, then create a new DOM node and insert it into the front position. If the SEL attributes are the same, save the node toelmToMoveOn this variable, and then callpatchVnodeCompare and update the differences between the two nodes, and then change theelmToMoveMove to the front

At the end of the loop, if:

  1. The old node is traversed first:

    New nodes are available. In this case, addVnodes is called to insert the remaining new nodes to the right in batches

  2. The new node is traversed first

    Call removeVnodes to delete the remaining nodes

Source:

  function updateChildren (parentElm: Node, oldCh: VNode[], newCh: VNode[], insertedVnodeQueue: VNodeQueue) {
    // Define variables
    let oldStartIdx = 0 // Old start node index
    let newStartIdx = 0 // New start node index
    let oldEndIdx = oldCh.length - 1 // Old end node index
    let oldStartVnode = oldCh[0] // Old start node
    let oldEndVnode = oldCh[oldEndIdx] // Old end node
    let newEndIdx = newCh.length - 1 // Index of the new end node
    let newStartVnode = newCh[0] // New start node
    let newEndVnode = newCh[newEndIdx] // New end node
    let oldKeyToIdx: KeyToIndexMap | undefined
    let idxInOld: number
    let elmToMove: VNode
    let before: any

    // Node comparison
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      // The node is null
      if (oldStartVnode == null) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode might have been moved left
      } else if (oldEndVnode == null) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (newStartVnode == null) {
        newStartVnode = newCh[++newStartIdx]
      } else if (newEndVnode == null) {
        newEndVnode = newCh[--newEndIdx]
        // The processing is complete
        // If old start node === new start node
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // Call patchVnode to compare and update nodes
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        // The index of the new and old nodes is incremented by 1, and the new and old start nodes are saved
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
        // If old end node === new end node
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // Call patchVnode to compare and update nodes
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        // The index of the old and new nodes is reduced by 1, and the new and old end nodes are saved
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
        // If old start node === new end node
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        // Call patchVnode to compare and update nodes
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        // Move the old start node after the old end nodeapi.insertBefore(parentElm, oldStartVnode.elm! , api.nextSibling(oldEndVnode.elm!) )// Old start node ++, new end node --
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
        // Old end node === new start node
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        // Insert the old end node before the old start nodeapi.insertBefore(parentElm, oldEndVnode.elm! , oldStartVnode.elm!)// Old end node --, new start node ++
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        // oldKeyToIdx is an object whose key corresponds to the key of the old node and whose value is the index of the old node
        // It is convenient to find the index of the old node according to the key of the new node
        if (oldKeyToIdx === undefined) {
          // Initialize the object with createKeyToOldIdx, passing the old node, the old node to start the index, and the old node to end the index
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        }
        // oldKeyToIdx[start with new node key]
        idxInOld = oldKeyToIdx[newStartVnode.key as string]
        if (isUndef(idxInOld)) { // New element
          // If the new node does not exist in the old node, the new element is created
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
        } else {
          // Save the key of the new node to elmToMove
          elmToMove = oldCh[idxInOld]
          if(elmToMove.sel ! == newStartVnode.sel) {// If the sel attribute is different, the new node is created and inserted before the old start node
            api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
          } else {
            // If so, call patchVnode to compare and update the node
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
            // Change the node in the old node array to undefined
            oldCh[idxInOld] = undefined as any
            // Update elmToMove before the old start node
            api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
          }
        }
        // New start node ++
        newStartVnode = newCh[++newStartIdx]
      }
    }
    // Wrap things up
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
      if (oldStartIdx > oldEndIdx) {
        // If the old node is traversed, the new node is free
        // Before is the reference element, and the inserted element is inserted before
        before = newCh[newEndIdx + 1] = =null ? null : newCh[newEndIdx + 1].elm
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
      } else {
        // The old node is free after the new node is traversed
        // Delete it directly
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
      }
    }
  }
Copy the code