Virtual DOM

Course objectives

  • What is the virtual DOM and what it does
  • Basic use of Snabbdom
  • Snabbdom source code parsing

What is the Virtual DOM

  • Virtual DOM(Virtual DOM) is a common JS object to describe the DOM object, because it is not a real DOM object, so it is called Virtual DOM

  • Real DOM members

    let element = document.querySelector('#app') let s = '' for (var key in element) { s += key + ',' } console.log(s) // Print the result align,title,lang,translate,dir,hidden,accessKey,draggable,spellcheck,autocapitalize,contentEditable,isContentEditable,in putMode,offsetParent,offsetTop,offsetLeft,offsetWidth,offsetHeight,style,innerText,outerText,oncopy,oncut,onpaste,onabor t,onblur,oncancel,oncanplay,oncanplaythrough,onchange,onclick,onclose,oncontextmenu,oncuechange,ondblclick,ondrag,ondrag end,ondragenter,ondragleave,ondragover,ondragstart,ondrop,ondurationchange,onemptied,onended,onerror,onfocus,oninput,oni nvalid,onkeydown,onkeypress,onkeyup,onload,onloadeddata,onloadedmetadata,onloadstart,onmousedown,onmouseenter,onmouselea ve,onmousemove,onmouseout,onmouseover,onmouseup,onmousewheel,onpause,onplay,onplaying,onprogress,onratechange,onreset,on resize,onscroll,onseeked,onseeking,onselect,onstalled,onsubmit,onsuspend,ontimeupdate,ontoggle,onvolumechange,onwaiting, onwheel,onauxclick,ongotpointercapture,onlostpointercapture,onpointerdown,onpointermove,onpointerup,onpointercancel,onpo interover,onpointerout,onpointerenter,onpointerleave,onselectstart,onselectionchange,onanimationend,onanimationiteration ,onanimationstart,ontransitionend,dataset,nonce,autofocus,tabIndex,click,focus,blur,enterKeyHint,onformdata,onpointerraw update,attachInternals,namespaceURI,prefix,localName,tagName,id,className,classList,slot,part,attributes,shadowRoot,assi gnedSlot,innerHTML,outerHTML,scrollTop,scrollLeft,scrollWidth,scrollHeight,clientTop,clientLeft,clientWidth,clientHeight ,attributeStyleMap,onbeforecopy,onbeforecut,onbeforepaste,onsearch,elementTiming,previousElementSibling,nextElementSibli ng,children,firstElementChild,lastElementChild,childElementCount,onfullscreenchange,onfullscreenerror,onwebkitfullscreen change,onwebkitfullscreenerror,setPointerCapture,releasePointerCapture,hasPointerCapture,hasAttributes,getAttributeNames ,getAttribute,getAttributeNS,setAttribute,setAttributeNS,removeAttribute,removeAttributeNS,hasAttribute,hasAttributeNS,t oggleAttribute,getAttributeNode,getAttributeNodeNS,setAttributeNode,setAttributeNodeNS,removeAttributeNode,closest,match es,webkitMatchesSelector,attachShadow,getElementsByTagName,getElementsByTagNameNS,getElementsByClassName,insertAdjacentE lement,insertAdjacentText,insertAdjacentHTML,requestPointerLock,getClientRects,getBoundingClientRect,scrollIntoView,scro ll,scrollTo,scrollBy,scrollIntoViewIfNeeded,animate,computedStyleMap,before,after,replaceWith,remove,prepend,append,quer ySelector,querySelectorAll,requestFullscreen,webkitRequestFullScreen,webkitRequestFullscreen,createShadowRoot,getDestina tionInsertionPoints,ELEMENT_NODE,ATTRIBUTE_NODE,TEXT_NODE,CDATA_SECTION_NODE,ENTITY_REFERENCE_NODE,ENTITY_NODE,PROCESSIN G_INSTRUCTION_NODE,COMMENT_NODE,DOCUMENT_NODE,DOCUMENT_TYPE_NODE,DOCUMENT_FRAGMENT_NODE,NOTATION_NODE,DOCUMENT_POSITION_ DISCONNECTED,DOCUMENT_POSITION_PRECEDING,DOCUMENT_POSITION_FOLLOWING,DOCUMENT_POSITION_CONTAINS,DOCUMENT_POSITION_CONTAI NED_BY,DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC,nodeType,nodeName,baseURI,isConnected,ownerDocument,parentNode,parentEl ement,childNodes,firstChild,lastChild,previousSibling,nextSibling,nodeValue,textContent,hasChildNodes,getRootNode,normal ize,cloneNode,isEqualNode,isSameNode,compareDocumentPosition,contains,lookupPrefix,lookupNamespaceURI,isDefaultNamespace ,insertBefore,appendChild,replaceChild,removeChild,addEventListener,removeEventListener,dispatchEventCopy the code
  • You can use the Virtual DOM to describe the real DOM, for example

    {
     sel: "div",
     data: {},
     children: undefined,
     text: "Hello Virtual DOM",
     elm: undefined,
     key: undefined
    }
    Copy the code

Why use the Virtual DOM

  • It is troublesome to operate the DOM manually, and browser compatibility issues also need to be considered. Although there are libraries such as jQuery to simplify DOM operations, the complexity of DOM operations increases with the complexity of projects

  • In order to simplify the complex manipulation of DOM, various MVVM frameworks have emerged to solve the problem of view and state synchronization

  • To simplify view manipulation we could use a template engine, but the problem of tracking state changes was not solved by the template engine, so the Virtual DOM emerged

  • The advantage of the Virtual DOM is that you don’t need to update the DOM immediately when the state changes. You just need to create a Virtual tree to describe the DOM. Inside the Virtual DOM, you will figure out how to update the DOM effectively

  • See the description of virtual-dom on Github

    • The virtual DOM can maintain the state of the program, keeping track of the last state
    • Update the real DOM by comparing the two states

The role of the virtual DOM

  • Maintain the relationship between views and states

  • Improved rendering performance in complex view situations

  • In addition to DOM rendering, it can also implement SSR(nuxt.js/Next-js), Native application (Weex/React Native), small program (MPvue /uni-app), etc

Virtual DOM library

  • Snabbdom

    • The Virtual DOM used internally in Vue 2.x is a modified Snabbdom
    • Approximately 200 SLOC (single line of code)
    • Extensible through modules
    • Source code is developed using TypeScript
    • One of the fastest Virtual DOM
  • virtual-dom

Case presentation

  • jQuery-demo
  • snabbdom-demo

Snabbdom is basically used

Create a project

  • Package tools to facilitate the use of parcels

  • Create the project and install the Parcel

    CD snabbdom-demo # package. Json NPM init -y # parcel NPM install parcel-bundler -DCopy the code
  • Configure scripts for package.json

    "scripts": {
     "dev": "parcel index.html --open",
     "build": "parcel build index.html"
    }
    Copy the code
  • Creating a directory structure

    │ ├ ─ garbage └─ SRC 01-basicusage.js │ index.html │ package.json ├ ─ basicusage.jsCopy the code

Import Snabbdom

Snabbdom document

  • Look at the meaning of the document

    • Learn any library by first looking at the documentation
    • Understand the library’s role through documentation
    • See the examples provided in the documentation for a quick demo of your own
    • View API usage through documentation
  • The document address

    • Github.com/snabbdom/sn…
    • The current version is V2.1.0
    # --depth indicates the clone depth. 1 indicates that only the latest version is cloned. A lot of version because if project iteration, cloning will slow the git clone - b v2.1.0 - the depth = 1 https://github.com/snabbdom/snabbdom.gitCopy the code

Install Snabbdom

Import Snabbdom

  • The two core Snabbdom functions init and h()

    • Init () is a higher-order function that returns patch()
    • H () returns the virtual node VNode, which we saw when working with vue.js
import { init } from 'snabbdom/init'
import { h } from 'snabbdom/h'
const patch = init([])
Copy the code

Note: The path is not snabbdom/int. The path is set in the exports field of package.json. The package. exports field is not supported by the packaging tool we used. Webpack 4 also does not support this field, webPack 5 supports this field. When the field in the imported snabbdom/init will fill the full path into snabbdom/build/package/init. Js

"exports": {
  "./init": "./build/package/init.js",
  "./h": "./build/package/h.js",
  "./helpers/attachto": "./build/package/helpers/attachto.js",
  "./hooks": "./build/package/hooks.js",
  "./htmldomapi": "./build/package/htmldomapi.js",
  "./is": "./build/package/is.js",
  "./jsx": "./build/package/jsx.js",
  "./modules/attributes": "./build/package/modules/attributes.js",
  "./modules/class": "./build/package/modules/class.js",
  "./modules/dataset": "./build/package/modules/dataset.js",
  "./modules/eventlisteners": "./build/package/modules/eventlisteners.js",
  "./modules/hero": "./build/package/modules/hero.js",
  "./modules/module": "./build/package/modules/module.js",
  "./modules/props": "./build/package/modules/props.js",
  "./modules/style": "./build/package/modules/style.js",
  "./thunk": "./build/package/thunk.js",
  "./tovnode": "./build/package/tovnode.js",
  "./vnode": "./build/package/vnode.js"
}
Copy the code
  • If using a package.json exports field that does not support it, we should write out the module’s path completely

    • View the directory structure of the installed SNabbDOM
import { h } from 'snabbdom/build/package/h'
import { init } from 'snabbdom/build/package/init'
import { classModule } from 'snabbdom/build/package/modules/class'
Copy the code
  • Review the render function in Vue
new Vue({
 router,
 store,
 render: h => h(App)
}).$mount('#app')
Copy the code
  • Thunk () is an optimization strategy that can be used when dealing with immutable data

Code demo

The basic use

Import {h} from 'snabbdom/build/package/h' import {init} from 'snabbdom/build/package/init / / using the init () function creates Patch () // init() is an array of arguments that can be passed to modules in the future, Let vnode = h('div. CLS ', [h('h1', 'Hello Snabbdom'), h('p', 'This is a paragraph ')]) const app = document.querySelector('#app') // Renders vNode to an empty DOM element (replace) // will return a new vNode let oldVnode = patch(app, vnode) setTimeout(() => { vnode = h('div.cls', [ h('h1', 'Hello World'), h('p', OldVnode = patch(oldVnode, vnode) // h('! ') is to create a comment patch(oldVnode, h('! '))}, 2000)Copy the code

The module

Snabbdom’s core library does not handle DOM element attributes/styles/events, etc. Modules can be used if needed

Commonly used modules

  • Six modules are officially available

    • attributes

      • Set the attributes of the DOM element usingsetAttribute(a)
      • Handles properties of Boolean type
    • props

      • andattributesThe module is similar to setting the attributes of the DOM elementelement[attr] = value
      • Boolean type attributes are not handled
    • class

      • Switching class styles
      • Note: Setting class styles for elements is done byselThe selector
    • dataset

      • Set up thedata-*Custom property of
    • eventlisteners

      • Register and remove events
    • style

      • Set inline styles to support animation
      • delayed/remove/destroy

Module USES

  • Module usage steps:

    • Import the required modules
    • Register modules in init()
    • When creating a VNode using the h() function, you can set the second parameter to the object and move the other parameters back

Code demo

Import {h} from 'snabbdom/build/package/h' import {init} from 'snabbdom/build/package/init/modules/import need import { styleModule } from 'snabbdom/build/package/modules/style' import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners' / / using the init () function creates patch () / / init () parameter is an array, the future can be introduced into modules, Let patch = init([// register module styleModule, Let vnode = h('div. CLS ', {// Set the DOM element's inline style: {color: '#DEDEDE', backgroundColor: '#181A1B'}, // register event on: {click: clickHandler } }, [ h('h1', 'Hello Snabbdom'), h('p', 'This is a paragraph ')]) function clickHandler () {// This points to the corresponding vnode console.log(this.elm.innerhtml)}Copy the code

Snabbdom source code parsing

An overview of the

How to learn source code

  • Start with the big picture
  • Look at the source code with the target
  • Look at the source code process to understand
  • debugging
  • The resources

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
  • Update the changed content to the real DOM tree

Snabbdom source

  • Source code address:

    • Github.com/snabbdom/sn…
  • SRC directory structure

  • ├ ─ ─ package │ ├ ─ ─ helpers │ │ └ ─ ─ attachto. Ts defines the vnode. Ts AttachData data structure of │ ├ ─ ─ modules │ │ ├ ─ ─ the attributes. The ts │ │ ├ ─ ─ Class. Ts │ │ ├── Dataset. Ts │ │ ├── Eventlisteners. Ts │ │ ├─ Hero │ │ ├ ─ ─ props. Ts │ │ └ ─ ─ style.css. Ts │ ├ ─ ─ h.t sh () function, │ ├─ HtmlDomAPI. Ts │ ├─ htmlDomAPI. │ ├─ ├─ JSX │ ├─ JSX │ ├─ JSX │ ├─ JSX │ ├─ JSX │ ├─ JSX │ ├─ JSX │ ├─ JSX │ ├─ For complex view immutable worth optimizing │ ├ ─ ─ tovnode. TsDOM converts VNode │ ├ ─ ─ ts - transform - js - extension. CJS │ ├ ─ ─ tsconfig. Jsonts compiler configuration file │ └ ─ ─ Vnode. ts Specifies the virtual nodeCopy the code

H function

  • Introduction to the h() function

    • You’ve seen the h() function in Vue

      new Vue({
       router,
       store,
       render: h => h(App)
      }).$mount('#app')
      Copy the code
    • The h() function, first seen in HyperScript, uses JavaScript to create hypertext

    • Instead of creating hypertext, the h() function in Snabbdom creates vNodes

  • Function overloading

    • concept

      • A function with a different number of arguments or type
      • There is no concept of overloading in JavaScript
      • Overloading exists in TypeScript, but overloading is implemented by adjusting parameters in code
    • Hint of overload

      function add (a: number, b: number) {
       console.log(a + b)
      }
      function add (a: number, b: number, c: number) {
       console.log(a + b + c)
      }
      add(1, 2)
      add(1, 2, 3)
      Copy the code
      function add (a: number, b: number) {
       console.log(a + b)
      }
      function add (a: number, b: string) {
       console.log(a + b)
      }
      add(1, 2)
      add(1, '2')
      Copy the code
    • Source location: SRC /package/h.ts
    // 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 = {}
      var children: any
      var text: any
      var i: number// Handle arguments to implement overloading mechanismsif (c ! = =undefined) {// handle three arguments //sel,data,children/text
        if (b ! = =null) {
          data = b
        }
        if (is.array(c)) {
          children = c
        } else if (is.primitive(c)) {
          text = c
        } else if (c && c.sel) {
          children = [c]
        }
      } else if(b ! = =undefined&& b ! = =null) {
        if (is.array(b)) {
          children = b
        } else if (is.primitive(b)) {
          // If c is a string or number
          text = b
        } else if (b && b.sel) {
          // If b is VNode
          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)};Copy the code

VNode

  • A VNode is a Virtual node that describes a DOM element. If the VNode has children, it is a Virtual DOM
  • SRC /package/vnode.ts
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 {
  const key = data === undefined ? undefined : data.key
  return { sel, data, children, text, elm, key }
}
Copy the code

snabbdom

  • 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

init

  • Init (modules, domApi), return patch() function (higher order function)

  • Why use higher-order functions?

    • Because patch() is called externally multiple times, each call depends on arguments such as modules/domApi/ CBS
    • Init () can be closed internally by higher-order functions, and the returned patch() can access modules/domApi/ CBS without needing to be recreated
  • Init () first collects all of the module’s hook functions and stores them in a CBS object before returning patch()

  • SRC /package/init.ts

const hooks: Array<keyof Module> = ['create'.'update'.'remove'.'destroy'.'pre'.'post']
export function init (modules: Array<Partial<Module>>, domApi? : DOMAPI) {
  let i: number
  let j: number
  const cbs: ModuleHooks = {
    create: [].update: [].remove: [].destroy: [].pre: [].post: []}// Initialize the API
  constapi: DOMAPI = domApi ! = =undefined ? domApi : htmlDomApi
  // Store all incoming module hook methods in a CBS object
  CBS = [create: [fn1, fn2], update: [],...
	for (i = 0; i < hooks.length; ++i) {
    // cbs['create'] = []
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      // const hook = modules[0]['create']
      const hook = modules[j][hooks[i]]
      if(hook ! = =undefined) {
        (cbs[hooks[i]] asAny []).push(hook)}}}......return function patch (oldVnode: VNode | Element, vnode: VNode) :VNode {... }}Copy the code

patch

  • 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()
  • Execution process:

    • Start by executing the hook function pre in the module

    • If oldVnode and VNode are the same (same key and SEL)

      • Call patchVnode() to find the node differences and update the DOM
    • If oldVnode is a DOM element

      • Convert DOM elements into OLdvNodes
      • Call createElm() to convert vNode to a real DOM and log to vNode.elm
      • Insert the newly created DOM element into the parent
      • Removing an old node
      • The triggerThe userSet up thecreate hookfunction
  • SRC /package/init.ts

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 element
    elm = oldVnode.elm!
    parent = api.parentNode(elm) as Node
		// 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
}
Copy the code

createElm

  • Function:

    • CreateElm (vnode, insertedVnodeQueue) returns the created DOM element
    • Create the DOM element corresponding to the vNode
  • Execution process:

    • The user-set init hook function is triggered first

    • If the selector is! To create a comment node

    • If the selector is empty, a text node is created

    • If the selector is not empty

      • Parse the selector and set the id and class properties of the tag
      • Execute the module’s CREATE hook function
      • If the vnode has children, create a DOM corresponding to the child vNode and append it to the DOM tree
      • If the vNode text value is string/number, create a text node and chase it into the DOM tree
      • Execute the user-set CREATE hook function
      • If there is a user-set insert hook function, add vNode to the queue
  • SRC /package/init.ts

  function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue) :Node {
    let i: any
    let data = vnode.data
    
    if(data ! = =undefined) {
      // Execute the user-set init hook function
      constinit = data.hook? .initif (isDef(init)) {
        init(vnode)
        data = vnode.data
      }
    }
    const children = vnode.children
    const sel = vnode.sel
    if (sel === '! ') {
      // If the selector is! To create a comment node
      if (isUndef(vnode.text)) {
        vnode.text = ' '
      }
      vnode.elm = api.createComment(vnode.text!)
    } else if(sel ! = =undefined) {
      // If the selector is not empty
      // Parse the selector
      // 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.' '))
      // Execute the module's CREATE hook function
      for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode)
      // If there are child nodes in the vNode, create the DOM element corresponding to the child vnode and append it to the DOM tree
      if (is.array(children)) {
        for (i = 0; i < children.length; ++i) {
          const ch = children[i]
          if(ch ! =null) {
            api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue))
          }
        }
      } else if (is.primitive(vnode.text)) {
        // If the vNode text value is string/number, create a text node and append it to the DOM tree
        api.appendChild(elm, api.createTextNode(vnode.text))
      }
      consthook = vnode.data! .hookif (isDef(hook)) {
        // Execute the user passed hook createhook.create? .(emptyNode, vnode)if (hook.insert) {
          // Add the vNode to the queue in preparation for the insert hook
          insertedVnodeQueue.push(vnode)
        }
      }
    } else {
      // If the selector is empty, create a text node
      vnode.elm = api.createTextNode(vnode.text!)
    }
    // Returns the newly created DOM
    return vnode.elm
  }
Copy the code

patchVnode

  • Function:

    • patchVnode(oldVnode, vnode, insertedVnodeQueue)
    • Compare oldVnode and VNode differences and render the differences into the DOM
  • Execution process:

    • The user-set prePatch hook function is first executed

    • Execute the CREATE hook function

      • The module’s CREATE hook function is first executed
      • The user-set CREATE hook function is then executed
    • If vnode.text is not defined

      • If oldvNode. children and vnode.children both have values

        • callupdateChildren()
        • Diff algorithm is used to compare and update child nodes
      • If vnode.children has a value, oldvNode. children has no value

        • Clearing DOM elements
        • calladdVnodes()To add child nodes in batches
      • If oldvNode. children has a value, vnode.children has no value

        • callremoveVnodes()To remove child nodes in batches
      • If oldvNode. text has a value

        • Empty the content of the DOM element
    • If vnode.text is set and differs from oldvNode. text

      • If the old node has children, remove them all
      • To set the DOM elementtextContentvnode.text
    • Finally, execute the user-set PostPatch hook function

  • SRC /package/init.ts

function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    consthook = vnode.data? .hook// Execute the user-set prepatch hook function firsthook? .prepatch? .(oldVnode, vnode)const elm = vnode.elm = oldVnode.elm!
    const oldCh = oldVnode.children as VNode[]
    const 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)) {
        // Call updateChildren to compare 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.text
      api.setTextContent(elm, vnode.text!)
    }
    // Finally execute the user-set postpatch hook functionhook? .postpatch? .(oldVnode, vnode) }Copy the code

  • When comparing nodes of the same level, the start and end nodes of the new and old node arrays will be marked with index, and the index will be moved during traversal

  • When comparing the start and end nodes, there are four cases

    • OldStartVnode/newStartVnode (old start node/new start node)
    • OldEndVnode/newEndVnode (old end node/new end node)
    • OldStartVnode/oldEndVnode (old start node/new end node)
    • OldEndVnode/newStartVnode (old end node/new start node)

  • The start node is compared to the end node. The two cases are similar

    • OldStartVnode/newStartVnode (old start node/new start node)
    • OldEndVnode/newEndVnode (old end node/new end node)
  • If oldStartVnode and newStartVnode are samevNodes (same key and SEL)

    • Call patchVnode() to compare and update nodes
    • Move the old start and new start indexes back oldStartIdx++ / oldEndIdx++

  • OldStartVnode/newEndVnode (old start node/new end node) same

    • Call patchVnode() to compare and update nodes
    • Move the oldStartVnode DOM element to the right – update the index

  • OldEndVnode/newStartVnode (old end node/new start node) same

    • Call patchVnode() to compare and update nodes
    • Move the oldEndVnode DOM element to the left
    • Update the index

  • If it’s not four of the above

    • Iterate over the new node, using the newStartNode key to find the same node in the old node array

    • If not, newStartNode is the new node

      • Create a DOM element corresponding to the new node and insert it into the DOM tree
    • If we find it,

      • Determine whether the SEL selector of the new node is the same as that of the old node found

      • If they are different, the nodes are modified

        • Re-create the corresponding DOM element and insert it into the DOM tree
      • If so, move the DOM element corresponding to elmToMove to the left

  • End of the cycle

    • The loop ends when all children of the old node are traversed first (oldStartIdx > oldEndIdx)
    • All children of the new node are traversed first (newStartIdx > newEndIdx), and the loop ends
  • If the array of the old node is traversed first (oldStartIdx > oldEndIdx), the new node has a surplus, and the remaining nodes are batch inserted to the right

  • If the array of the new node is traversed first (newStartIdx > newEndIdx), it indicates that the old node has surplus. Delete the remaining nodes in batches

SRC /package/init.ts

function updateChildren (parentElm: Node, oldCh: VNode[], newCh: VNode[], insertedVnodeQueue: VNodeQueue) {
  let oldStartIdx = 0
  let newStartIdx = 0
  let oldEndIdx = oldCh.length - 1
  let oldStartVnode = oldCh[0]
  let oldEndVnode = oldCh[oldEndIdx]
  let newEndIdx = newCh.length - 1
  let newStartVnode = newCh[0]
  let newEndVnode = newCh[newEndIdx]
  let oldKeyToIdx: KeyToIndexMap | undefined
  let idxInOld: number
  let elmToMove: VNode
  let before: any

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // After index changes, the node may be set to null
    if (oldStartVnode == null) {
      // The node is an empty move index
      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]
    // Compare the four cases of start and end nodes
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      // 1. Compare the old start node with the new start node
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      // 2. Compare the old end node with the new end node
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
      // 3. Compare the old start node with the new end nodepatchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) api.insertBefore(parentElm, oldStartVnode.elm! , api.nextSibling(oldEndVnode.elm!) ) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] }else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
      // 4. Compare the old end node with the new start nodepatchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) api.insertBefore(parentElm, oldEndVnode.elm! , oldStartVnode.elm!) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] }else {
      // The start node and the end node are different
      // Use newStartNode's key to find the same node in the old node array
      // Set the key and index objects
      if (oldKeyToIdx === undefined) {
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      }
      // Iterate over newStartVnode to find the oldVnode index with the same key from the old node
      idxInOld = oldKeyToIdx[newStartVnode.key as string]
      // If it is a new VNode
      if (isUndef(idxInOld)) { // New element
        // If not found, newStartNode is the new node
        // Create an element to insert into the DOM tree
        api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
      } else {
        // If an old node with the same key is found, record it in elmToMove traversal
        elmToMove = oldCh[idxInOld]
        if(elmToMove.sel ! == newStartVnode.sel) {// If the old and new nodes have different selectors
          // Create a DOM element corresponding to the new start node and insert it into the DOM tree
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
        } else {
          // If same, patchVnode()
          // Move the elmToMove element to the left
          patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
          oldCh[idxInOld] = undefined as any
          api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
        }
      }
      // Re-assign newStartVnode to the next new node
      newStartVnode = newCh[++newStartIdx]
    }
  }
  // When the loop ends, the old node array completes first or the new node array completes first
  if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
    if (oldStartIdx > oldEndIdx) {
      // If the old node array is traversed first, new nodes are left
      // Insert the remaining new nodes to the right
      before = newCh[newEndIdx + 1] = =null ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else {
      // If the new node array is traversed first, the old node is left
      // Delete old nodes in batches
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }
}
Copy the code