Author: Steins from thunderbolt Front

Original address: github.com/linrui1994/…

With the popularity of React Vue and other frameworks, Virtual DOM is becoming more and more popular. Snabbdom is one of the implementations, and the Virtual DOM of Vue 2.x version is also modified based on SNabbDOM. With just over 200 lines of core code, snabbDOM is a great library for readers who want to learn more about Virtual DOM implementation. If you haven’t heard of snabbDOM, take a look at the official documentation.

Why snabbDOM

  • Only 200 lines of core code, rich with test cases
  • Powerful plug-in system, hook system
  • Vue uses SNABbDOM. It is helpful to understand the implementation of VUE to understand snABbDOM

What is a Virtual DOM

Snabbdom is an implementation of the Virtual DOM, so before you do that, you need to know what a Virtual DOM is. Generally speaking, the Virtual DOM is a JS object. It is an abstraction of the real DOM. It only keeps some useful information and describes the structure of the DOM tree more lightly. For example, in snabbDOM, a VNode is defined as follows:

export interface VNode { sel: string | undefined; data: VNodeData | undefined; children: Array<VNode | string> | undefined; elm: Node | undefined; text: string | undefined; key: Key | undefined; } export interface VNodeData { props? : Props; attrs? : Attrs; class? : Classes; style? : VNodeStyle; dataset? : Dataset; on? : On; hero? : Hero; attachData? : AttachData; hook? : Hooks; key? : Key; ns? : string; // for SVGs fn? : () => VNode; // for thunks args? : Array<any>; // for thunks [key: string]: any; // for any other 3rd party module }Copy the code

From the above definition, we can see that we can use JS objects to describe the DOM structure, so we can compare the js objects in two states, record their differences, and then apply it to the real DOM tree. The answer is yes. This is diff algorithm. The basic steps of the algorithm are as follows:

  • Use the JS object to describe the DOM tree structure, and then use this JS object to create a real DOM tree, inserted into the document
  • When the status is updated, the new JS object is compared to the old JS object to get the difference between the two objects
  • Apply the differences to the real DOM

Let’s analyze the implementation of this whole process.

Source code analysis

Start with a simple example, step-by-step analysis of the entire code execution process, the following is an official simple example:

var snabbdom = require('snabbdom');
var patch = snabbdom.init([
  // Init patch function with chosen modules
  require('snabbdom/modules/class').default, // makes it easy to toggle classes
  require('snabbdom/modules/props').default, // for setting properties on DOM elements
  require('snabbdom/modules/style').default, // handles styling on elements with support for animations
  require('snabbdom/modules/eventlisteners').default // attaches event listeners
]);
var h = require('snabbdom/h').default; // helper function for creating vnodes

var container = document.getElementById('container');

var vnode = h('div#container.two.classes', { on: { click: someFn } }, [
  h('span', { style: { fontWeight: 'bold'}},'This is bold'),
  ' and this is just normal text',
  h('a', { props: { href: '/foo'}},"I'll take you places!")]);// Patch into empty DOM element -- this modifies the DOM as a side effect
patch(container, vnode);

var newVnode = h('div#container.two.classes', { on: { click: anotherEventHandler } }, [
  h('span', { style: { fontWeight: 'normal'.fontStyle: 'italic'}},'This is now italic type'),
  ' and this is still just normal text',
  h('a', { props: { href: '/bar'}},"I'll take you places!")]);// Second `patch` invocation
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
Copy the code

First, the snabbDOM module provides an init method that receives an array of modules. This design makes the library more extensible. We can also implement our own modules and introduce corresponding modules according to our own needs. For example, if you don’t need to write a class, you can simply remove the class module. A call to init returns a patch function that takes two arguments. The first argument is the old VNode or DOM node, and the second argument is the new VNode node. The call to patch updates the DOM. Vnodes can be generated by using the h function. It’s fairly simple to use, which is what we’ll analyze in the next part of this article.

The init function

export interface Module {
  pre: PreHook;
  create: CreateHook;
  update: UpdateHook;
  destroy: DestroyHook;
  remove: RemoveHook;
  post: PostHook;
}

export function init(modules: Array<Partial<Module>>, domApi? : DOMAPI) {
  // CBS is used to collect hooks in the Module
  let i: number,
    j: number,
    cbs = {} as ModuleHooks;

  constapi: DOMAPI = domApi ! = =undefined ? domApi : htmlDomApi;

  // Collect hooks in module
  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = [];
    for (j = 0; j < modules.length; ++j) {
      const hook = modules[j][hooks[i]];
      if(hook ! = =undefined) {
        (cbs[hooks[i]] as Array<any>).push(hook); }}}function emptyNodeAt(elm: Element) {
    // ...
  }

  function createRmCb(childElm: Node, listeners: number) {
    // ...
  }

  // Create a real DOM node
  function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue) :Node {
    // ...
  }

  function addVnodes(
    parentElm: Node,
    before: Node | null,
    vnodes: Array<VNode>,
    startIdx: number,
    endIdx: number,
    insertedVnodeQueue: VNodeQueue
  ) {
    // ...
  }

  // Call the deStory hook
  // Call recursively if children exist
  function invokeDestroyHook(vnode: VNode) {
    // ...
  }

  function removeVnodes(parentElm: Node, vnodes: Array<VNode>, startIdx: number, endIdx: number) :void {
    // ...
  }

  function updateChildren(parentElm: Node, oldCh: Array<VNode>, newCh: Array<VNode>, insertedVnodeQueue: VNodeQueue) {
    // ...
  }

  function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    // ...
  }

  return function patch(oldVnode: VNode | Element, vnode: VNode) :VNode {
    // ...
  };
}
Copy the code

The above is init method of some source code, in order to read convenient, the temporary implementation of some methods to comment out, such as useful to the time to specific analysis. So if you take a modules array, there’s an optional parameter called domApi, and if you don’t pass it, you’ll use the browser’s DOM-related API, which you can see here, but it’s also a nice thing to do, because it lets you customize the platform’s API, Take a look at the implementation of WEEX, for example. First, hook in module will be collected and saved to CBS. Then we define various functions, but we can ignore them here, and then we return a patch function, and we won’t analyze its logic here. So that’s the end of init.

H function

Following the flow of the example, let’s look at the implementation of method H

export function h(sel: string) :VNode;
export function h(sel: string, data: VNodeData) :VNode;
export function h(sel: string, children: VNodeChildren) :VNode;
export function h(sel: string, data: VNodeData, children: VNodeChildren) :VNode;
export function h(sel: any, b? :any, c? :any) :VNode {
  var data: VNodeData = {},
    children: any,
    text: any,
    i: number;
  // Format parameters
  if(c ! = =undefined) {
    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) {
    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 exist, convert non-vnode items to vnode
  if(children ! = =undefined) {
    for (i = 0; i < children.length; ++i) {
      if (is.primitive(children[i])) children[i] = vnode(undefined.undefined.undefined, children[i], undefined); }}// Add a namespace to the SVG element
  if (sel[0= = ='s' && sel[1= = ='v' && sel[2= = ='g' && (sel.length === 3 || sel[3= = ='. ' || sel[3= = =The '#')) {
    addNS(data, children, sel);
  }
  / / return vnode
  return vnode(sel, data, children, text, undefined);
}

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) {
      let childData = children[i].data;
      if(childData ! = =undefined) {
        addNS(childData, (children[i] as VNode).children asVNodes, children[i].sel); }}}}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: sel,
    data: data,
    children: children,
    text: text,
    elm: elm,
    key: key
  };
}
Copy the code

Since the last two arguments of the h function are optional and can be passed in various ways, the arguments are first formatted and then the children attribute is processed to convert the item that may not be vNode to vnode. If it is an SVG element, a special processing will be done. Finally, a VNode object is returned.

The patch function

The patch function is the core of snabbDOM. Calling init will return this function for DOM-related updates. Let’s see how to implement it.

function patch(oldVnode: VNode | Element, vnode: VNode) :VNode {
  let i: number, elm: Node, parent: Node;
  const insertedVnodeQueue: VNodeQueue = [];
  // Call the pre hook in module
  for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();

  // If an Element is passed, turn to an empty vNode
  if(! isVnode(oldVnode)) { oldVnode = emptyNodeAt(oldVnode); }// Call patchVnode when sameVnode (sel and key are the same)
  if (sameVnode(oldVnode, vnode)) {
    patchVnode(oldVnode, vnode, insertedVnodeQueue);
  } else {
    elm = oldVnode.elm as Node;
    parent = api.parentNode(elm);

    // Create a new DOM node vnode.elm
    createElm(vnode, insertedVnodeQueue);

    if(parent ! = =null) {
      / / insert the dom
      api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));
      // Remove the old DOM
      removeVnodes(parent, [oldVnode], 0.0); }}// Call insert hooks on the element. Note that insert hooks are not supported on modules
  for (i = 0; i < insertedVnodeQueue.length; ++i) {
    (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]);
  }

  // Call module post hook
  for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
  return vnode;
}

function emptyNodeAt(elm: Element) {
  const id = elm.id ? The '#' + elm.id : ' ';
  const c = elm.className ? '. ' + elm.className.split(' ').join('. ') : ' ';
  return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
}

// Key is the same as selector
function sameVnode(vnode1: VNode, vnode2: VNode) :boolean {
  return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}
Copy the code

First, the module’s pre hook is called. You may wonder why the pre hook is not called from each element. This is because the pre hook is not supported on the element. If it is not, emptyNodeAt is called and converted to a Vnode. The implementation of emptyNodeAt is simple. Note that only class and style are left. This is a bit different from the toVnode implementation because there is not a lot of information to store, such as prop attribute, etc. SameVnode is then called to determine whether it is the sameVnode node. The implementation is also simple, in this case the key and sel are the same. If so, call patchVnode. If not, call createElm to create a new DOM node. If the parent node exists, insert it into the DOM and remove the old dom node to complete the update. Finally, call the insert hook on the element and the Post hook on the Module. The focus here is on the patchVnode and createElm functions. Let’s first look at the createElm function to see how the DOM node is created.

CreateElm function

// Create a real DOM node
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue) :Node {
  let i: any, data = vnode.data;

  // Call the init hook of the element
  if(data ! = =undefined) {
    if(isDef(i = data.hook) && isDef(i = i.init)) { i(vnode); data = vnode.data; }}let children = vnode.children, sel = vnode.sel;
  // Comment node
  if (sel === '! ') {
    if (isUndef(vnode.text)) {
      vnode.text = ' ';
    }
    // Create a comment node
    vnode.elm = api.createComment(vnode.text as string);
  } else if(sel ! = =undefined) {
    // 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 as VNodeData).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.' '));

    // Call the Create hook in module
    for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);

    // Mount the child nodes
    if (is.array(children)) {
      for (i = 0; i < children.length; ++i) {
        const ch = children[i];
        if(ch ! =null) {
          api.appendChild(elm, createElm(ch asVNode, insertedVnodeQueue)); }}}else if (is.primitive(vnode.text)) {
      api.appendChild(elm, api.createTextNode(vnode.text));
    }
    i = (vnode.data as VNodeData).hook; // Reuse variable
    // Call hook on vnode
    if (isDef(i)) {
      // Call create hook
      if (i.create) i.create(emptyNode, vnode);
      // Insert hook is stored and will not be called until the DOM is inserted. In this case, it is stored in an array to avoid having to traverse the vNode tree again
      if(i.insert) insertedVnodeQueue.push(vnode); }}else {
    // Text node
    vnode.elm = api.createTextNode(vnode.text as string);
  }
  return vnode.elm;
}
Copy the code

The logic here is also clear. First, init hook of the element will be called, and then there will be three cases:

  • If the current element is a comment nodecreateCommentTo create a comment node and mount it tovnode.elm
  • If there is no selector, just plain text, callcreateTextNodeTo create the text and then mount it tovnode.elm
  • If there is a selector, it parses that selector and getstag,idclass, and then callcreateElementcreateElementNSTo generate a node and mount it tovnode.elm. Then callmoduleOn thecreate hookIf there ischildren, traversing all the child nodes and recursively callingcreateElmcreatedomThrough theappendChildMount to the currentelmIt doesn’t existchildrenBut there aretext, we will know how to usecreateTextNodeTo create the text. Finally, call the one on the invocation elementcreate hookAnd save existenceinsert hookvnodeBecause theinsert hookNeed etc.domActually mount todocumentI’m going to call it on, so I’m going to keep it in an array so that I don’t have to call it when I actually need tovnodeThe tree is traversed.

Next, let’s look at how SNabbDOM does vNode diff, which is the core of the Virtual DOM.

PatchVnode function

What this function does is diff the two vNodes passed in and, if any updates exist, feed them back to the DOM.

function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
  let i: any, hook: any;
  // Call the prepatch hook
  if (isDef((i = vnode.data)) && isDef((hook = i.hook)) && isDef((i = hook.prepatch))) {
    i(oldVnode, vnode);
  }
  const elm = (vnode.elm = oldVnode.elm as Node);
  let oldCh = oldVnode.children;
  let ch = vnode.children;
  if (oldVnode === vnode) return;
  if(vnode.data ! = =undefined) {
    // Call the Update hook on module
    for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
    i = vnode.data.hook;
    // Call the update hook on vnode
    if (isDef(i) && isDef((i = i.update))) i(oldVnode, vnode);
  }
  if (isUndef(vnode.text)) {
    if (isDef(oldCh) && isDef(ch)) {
      // Diff is performed on children when both old and new nodes exist and they are different
      // There will be optimizations in Thunk related to this
      if(oldCh ! == ch) updateChildren(elm, oldChas Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
    } else if (isDef(ch)) {
      // The old node has no children and the new node has children
      // The old node has text set to null
      if (isDef(oldVnode.text)) api.setTextContent(elm, ' ');
      // Add a new VNode
      addVnodes(elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue);
    } else if (isDef(oldCh)) {
      // The new node has no children. The old node has children. Remove the children of the old node
      removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
    } else if (isDef(oldVnode.text)) {
      // The old node has text set to null
      api.setTextContent(elm, ' '); }}else if(oldVnode.text ! == vnode.text) {/ / update the text
    api.setTextContent(elm, vnode.text as string);
  }
  // Call the postpatch hook
  if(isDef(hook) && isDef((i = hook.postpatch))) { i(oldVnode, vnode); }}Copy the code

First call the prepatch hook on the Vnode, if the current two Vnodes are identical, directly return. Then call the Update hook on the Module and vNode. Then it will be divided into the following situations to do processing:

  • There werechildrenAnd different, callupdateChildren
  • newvnodeThere arechildrenAnd the oldvnodeThere is nochildrenIf the oldvnodeThere aretextLet’s empty it and then calladdVnodes
  • newvnodeThere is nochildrenAnd the oldvnodeThere arechildren, the callremoveVnodesremovechildren
  • Are there is nochildrenThe newvnodeThere is notext, remove the oldvnodetext
  • There weretextTo updatetext

Finally, call the Postpatch hook. The whole process is clear. What we need to focus on is updateChildren addVnodes removeVnodes.

updateChildren

function updateChildren(parentElm: Node, oldCh: Array<VNode>, newCh: Array<VNode>, insertedVnodeQueue: VNodeQueue) {
  let oldStartIdx = 0,
    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: any;
  let idxInOld: number;
  let elmToMove: VNode;
  let before: any;

  // Iterate over oldCh newCh to compare and update nodes
  // At most one node is processed in each round of comparison. Algorithm complexity O(n)
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // If there is an empty node among the four nodes to be compared, the null node subscript is pushed to the center to continue the next cycle
    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 new and old start nodes are the same. Directly call patchVnode to update, and push the subscript to the middle
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
      oldStartVnode = oldCh[++oldStartIdx];
      newStartVnode = newCh[++newStartIdx];
      // The new and old end nodes are the same, with the same logic as above
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
      oldEndVnode = oldCh[--oldEndIdx];
      newEndVnode = newCh[--newEndIdx];
      // The old start node is equal to the new node node, indicating that the node is moved to the right. Call patchVnode to update the node
    } else if (sameVnode(oldStartVnode, newEndVnode)) {
      // Vnode moved right
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
      // The old start node equals the new end node, indicating that the node moved to the right
      Because the new node is at the end, it is added after the old end node (which will be moved left with updateChildren)
      // Note that you need to move the DOM because the node is moved to the right.
      // This can be divided into two cases:
      // 1. When the loop starts and the subscripts are not moved, it makes sense to move to the end of oldEndVnode
      // 2. The loop is already partially executed, because after each comparison, the subscripts are moved to the center and each node is processed.
      // At this point, the left and right sides of the subscript have been processed. We can treat the subscript from start to end as a whole without starting the loop.
      OldEndVnode = oldEndVnode = oldEndVnode = oldEndVnode = oldEndVnode
      api.insertBefore(parentElm, oldStartVnode.elm as Node, api.nextSibling(oldEndVnode.elm as Node));
      oldStartVnode = oldCh[++oldStartIdx];
      newEndVnode = newCh[--newEndIdx];
      // The old end node equals the new start node, indicating that the node is moved to the left
    } else if (sameVnode(oldEndVnode, newStartVnode)) {
      // Vnode moved left
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
      api.insertBefore(parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node);
      oldEndVnode = oldCh[--oldEndIdx];
      newStartVnode = newCh[++newStartIdx];
      // If none of the above four conditions match, the following two conditions may exist
      // 1. This node is newly created
      // 2. This node is in the middle in its original position (oldStartIdx and endStartIdx)
    } else {
      // If oldKeyToIdx does not exist, create a mapping between key and index
      // And there are all sorts of subtle optimizations that are created only once and don't need to be mapped to parts that are already done
      if (oldKeyToIdx === undefined) {
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
      }
      // Get the corresponding subscript under oldCh
      idxInOld = oldKeyToIdx[newStartVnode.key as string];
      // If the subscript does not exist, the node is newly created
      if (isUndef(idxInOld)) {
        // New element
        // Insert at the beginning of oldStartVnode (equivalent to the beginning for the current loop)
        api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
        newStartVnode = newCh[++newStartIdx];
      } else {
        // If it is an existing node, find the node that needs to be moved
        elmToMove = oldCh[idxInOld];
        // Create a new DOM node by calling createElm
        if(elmToMove.sel ! == newStartVnode.sel) { api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elmas Node);
        } else {
          // Otherwise, call patchVnode to update the old VNode
          patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
          // In oldCh, empty the vnode that is currently being processed and skip it the next time you loop to this subscript
          oldCh[idxInOld] = undefined as any;
          // Insert at the beginning of oldStartVnode (equivalent to the beginning for the current loop)
          api.insertBefore(parentElm, elmToMove.elm as Node, oldStartVnode.elm asNode); } newStartVnode = newCh[++newStartIdx]; }}}// After the loop ends, there are two possible situations
  // 1. OldCh is all processed, newCh has a new node, and a new DOM needs to be created for each remaining item
  if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
    if (oldStartIdx > oldEndIdx) {
      before = newCh[newEndIdx + 1] = =null ? null : newCh[newEndIdx + 1].elm;
      addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
      // 2. NewCh has been processed completely, oldCh still has old nodes, need to remove redundant nodes
    } else{ removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); }}}Copy the code

The process is simple: compare the two arrays, find the same parts, reuse them, and update them. The whole logic may seem silly, but it can be understood with the following example:

  1. Suppose old nodes are [A, B, C, D] and new nodes are [B, A, C, D, E].

  1. First round comparison: If newStartVnode exists in the old node, call patchVnode to update the new node. Set oldCh[1] to null and insert dom in front of oldStartVnode. NewStartIdx moves to the middle and the status updates are as follows

  1. Second round comparison: oldStartVnode and newStartVnode are equal, directly Move patchVnode, newStartIdx and oldStartIdx to the middle, and the status is updated as follows

  1. Third round of comparison: oldStartVnode is empty, oldStartIdx moves to the middle and enters the next round of comparison. The status is updated as follows

  1. The fourth comparison: oldStartVnode and newStartVnode are equal, directly Move patchVnode, newStartIdx and oldStartIdx to the middle, and update the status as follows

  1. OldStartVnode and newStartVnode are equal. Directly move patchVnode, newStartIdx and oldStartIdx to the middle, and the status is updated as follows

  1. OldStartIdx is greater than oldEndIdx, and the loop ends. Since the old node ended the loop first and there are still new nodes to process, call addVnodes to process the remaining new nodes

The addVnodes and removeVnodes functions

function addVnodes(parentElm: Node, before: Node | null, vnodes: Array<VNode>, startIdx: number, endIdx: number, insertedVnodeQueue: VNodeQueue) {
  for (; startIdx <= endIdx; ++startIdx) {
    const ch = vnodes[startIdx];
    if(ch ! =null) { api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before); }}}function removeVnodes(parentElm: Node, vnodes: Array<VNode>, startIdx: number, endIdx: number) :void {
  for (; startIdx <= endIdx; ++startIdx) {
    let i: any, listeners: number, rm: (a)= > void, ch = vnodes[startIdx];
    if(ch ! =null) {
      if (isDef(ch.sel)) {
        // Call the deStory hook
        invokeDestroyHook(ch);
        // Count the number of times you need to call removecallback to remove the DOM
        listeners = cbs.remove.length + 1;
        rm = createRmCb(ch.elm as Node, listeners);
        // Call module with remove hook
        for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
        // Call vnode's remove hook
        if (isDef(i = ch.data) && isDef(i = i.hook) && isDef(i = i.remove)) {
          i(ch, rm);
        } else{ rm(); }}else { // Text node
        api.removeChild(parentElm, ch.elm asNode); }}}}// Call the deStory hook
// Call recursively if children exist
function invokeDestroyHook(vnode: VNode) {
  let i: any, j: number, data = vnode.data;
  if(data ! = =undefined) {
    if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode);
    for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
    if(vnode.children ! = =undefined) {
      for (j = 0; j < vnode.children.length; ++j) {
        i = vnode.children[j];
        if(i ! =null && typeofi ! = ="string") {
          invokeDestroyHook(i);
        }
      }
    }
  }
}

// The DOM will be removed only if all the remove hooks have called the Remove callback
function createRmCb(childElm: Node, listeners: number) {
  return function rmCb() {
    if (--listeners === 0) {
      constparent = api.parentNode(childElm); api.removeChild(parent, childElm); }}; }Copy the code

These two functions are used to add vNodes and remove vnodes, and the code logic is basically readable.

Thunk function

Generally, our application is updated according to the JS state, such as in the following example

function renderNumber(num) {
  return h('span', num);
}
Copy the code

This means that if num does not change, it is meaningless to patch vnode. In this case, snabbDOM provides an optimization method, namely thunk. This function also returns a VNode. This is similar to the React pureComponent. The pureComponent implementation does a light shadowEqual comparison, especially when used in conjunction with immutable data. The example above could look like this.

function renderNumber(num) {
  return h('span', num);
}

function render(num) {
  return thunk('div', renderNumber, [num]);
}

var vnode = patch(container, render(1))
RenderNumber will not be executed because num is the same
patch(vnode, render(1))
Copy the code

Its specific implementation is as follows:

export interface ThunkFn {
  (sel: string, fn: Function.args: Array<any>): Thunk;
  (sel: string, key: any, fn: Function.args: Array<any>): Thunk;
}

// Use h to return vNode and add init and prepatch hooks to it
export const thunk = function thunk(sel: string, key? : any, fn? : any, args? : any) :VNode {
  if (args === undefined) {
    args = fn;
    fn = key;
    key = undefined;
  }
  return h(sel, {
    key: key,
    hook: {init: init, prepatch: prepatch},
    fn: fn,
    args: args
  });
} as ThunkFn;

// Copy the data from the VNode to Thunk. If the data is the same as the data in patchVnode, the patchVnode is terminated
// The fn and args attributes of Thunk are saved to the VNode, which needs to be compared during prepatch
function copyToThunk(vnode: VNode, thunk: VNode) :void {
  thunk.elm = vnode.elm;
  (vnode.data as VNodeData).fn = (thunk.data as VNodeData).fn;
  (vnode.data as VNodeData).args = (thunk.data as VNodeData).args;
  thunk.data = vnode.data;
  thunk.children = vnode.children;
  thunk.text = vnode.text;
  thunk.elm = vnode.elm;
}

function init(thunk: VNode) :void {
  const cur = thunk.data as VNodeData;
  const vnode = (cur.fn as any).apply(undefined, cur.args);
  copyToThunk(vnode, thunk);
}

function prepatch(oldVnode: VNode, thunk: VNode) :void {
  let i: number, old = oldVnode.data as VNodeData, cur = thunk.data as VNodeData;
  const oldArgs = old.args, args = cur.args;
  if(old.fn ! == cur.fn || (oldArgsasany).length ! == (argsas any).length) {
    // If fn is different or the length of args is different, that means something has changed, call fn to generate a new vNode and return
    copyToThunk((cur.fn as any).apply(undefined, args), thunk);
    return;
  }
  for (i = 0; i < (args as any).length; ++i) {
    if ((oldArgs asany)[i] ! == (argsas any)[i]) {
      // If each parameter changes, the logic is the same as above
      copyToThunk((cur.fn as any).apply(undefined, args), thunk);
      return;
    }
  }
  copyToThunk(oldVnode, thunk);
}
Copy the code

You can review the implementation of patchVnode. After prepatch, the data of VNode will be compared. For example, the patchVnode will end when the children and text are the same.

conclusion

Here snabbDOM core source code has been read, the rest of the built-in module, interested can read.

Scan to pay attention to thunder front public number