Originally intended to see the realization of virtualDOM principle, but see many articles are just talking about the principle, there are few sources of vDOM library analysis, today is going to try to start from their own point of view, write a source code analysis of the article

First of all, please show today’s protagonist — Vue2 vDOM based on the library, snabbdom, github address as follows

Making: github.com/snabbdom/sn…


Type a,

First, let’s look at his type definition

VNode type

VNodeData { props? : Props; attrs? : Attrs; class? : Classes; style? : VNodeStyle; dataset? : Dataset; on? : On; hero? : Hero; attachData? : AttachData; hook? : Hooks; key? : Key; ns? : string; //forSVGs fn? : () => VNode; //forthunks args? : Array<any>; //for thunks
  [key: string]: any; // forAny other 3rd party module} // Recode const user: Record<'name'|'email', string> = {
//   name: ' ', 
//   email: ' '
// }

type Props = Record<string, any>;

type Classes = Record<string, boolean>

typeAttrs = Record<string, string | number | boolean> interface Hooks { pre? : PreHook; init? : InitHook; create? : CreateHook; insert? : InsertHook; prepatch? : PrePatchHook; update? : UpdateHook; postpatch? : PostPatchHook; destroy? : DestroyHook; remove? : RemoveHook; post? : PostHook; }Copy the code

It can be seen that the virtual DOM node defined by SnabbDOM is not like that defined in many Vue. It has a series of attributes such as class and attrs that conform to our cognition, but at the same time it provides us with hooks that allow us to operate on it when updating the node

Second, the method

Let’s take a look at the official 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 toVNode = require('snabbdom/tovnode').default;

var newVNode = h('div', {style: {color: '# 000'}}, [
  h('h1'.'Headline'),
  h('p'.'A paragraph'),]); patch(toVNode(document.querySelector('.container')), newVNode)
Copy the code

It is very convenient to define a node and an update when the function is normal to use, let’s look at the specific methods to do what

The realization of the h

functionh(sel: any, b? : any, c? : any): VNode { var data: VNodeData = {}, children: any, text: any, i: number;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 ! == undefined) {for (i = 0; i < children.length; ++i) {
      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 '#')
  ) {
    addNS(data, children, sel);
  }
  return vnode(sel, data, children, text, undefined);
};

// addNs
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 as VNodes, children[i].sel); } } } } // vnodefunction 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};
}
Copy the code

As you can see, all you have to do is make some judgments about your input (mutable arguments) and work with elements that have their own special namespace (SVG)

Init function implementation

Init accepts the plug-in and optional domAPI properties and returns a function to update the DOM

init(modules: Array<Partial<Module>>, domApi? : DOMAPI)Copy the code

The first parameter accepts a series of plug-ins to update the DOM

// Partial marks all types as optional attributes interface Module {pre: PreHook; create: CreateHook; update: UpdateHook; destroy: DestroyHook; remove: RemoveHook; post: PostHook; }Copy the code

Look at the source code of a plug-in

import {VNode, VNodeData} from '.. /vnode';
import {Module} from './module';

export type Classes = Record<string, boolean>

function updateClass(oldVnode: VNode, vnode: VNode): void {
  var cur: any, name: string, elm: Element = vnode.elm as Element,
      oldClass = (oldVnode.data as VNodeData).class,
      klass = (vnode.data as VNodeData).class;

  if(! oldClass && ! klass)return;
  if (oldClass === klass) return;
  oldClass = oldClass || {};
  klass = klass || {};

  for (name in oldClass) {
    if (!klass[name]) {
      elm.classList.remove(name);
    }
  }
  for (name in klass) {
    cur = klass[name];
    if(cur ! == oldClass[name]) { (elm.classList as any)[cur ?'add' : 'remove'](name); }}}export const classModule = {create: updateClass, update: updateClass} as Module;
export default classModule;

Copy the code

The plug-in is the action of dom actual operation provided by each hook when the patch function is running. Then how does the plug-in load into the patch? So what does init do

const hooks: (keyof Module)[] = ['create'.'update'.'remove'.'destroy'.'pre'.'post'];
functioninit(modules: Array<Partial<Module>>, domApi? : DOMAPI) {leti: number, j: number, cbs = ({} as ModuleHooks); const api: DOMAPI = domApi ! == undefined ? domApi : htmlDomApi;for(i = 0; i < hooks.length; ++ I) {// Put the hook functions into an array, save them with closures 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); }}} //...return function patch() {/ /... }}Copy the code

The idea is to store these methods in closures and call them at run time

Look at the patch function (the function returned by the init method to update the DOM)

  • If the VNode is different, create a DOM based on the new VNode to replace the old VNode
  • If it is judged to be the same vNode, the method of patchNode will be run (to operate on the original DOM).
function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
    let i: number, elm: Node, parent: Node;
    const insertedVnodeQueue: VNodeQueue = [];
    for(i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); // Execute hook: preif(! isVnode(oldVnode)) { oldVnode = emptyNodeAt(oldVnode); }if (sameVnode(oldVnode, vnode)) {
      patchVnode(oldVnode, vnode, insertedVnodeQueue);
    } else {
      elm = oldVnode.elm as Node;
      parent = api.parentNode(elm);

      createElm(vnode, insertedVnodeQueue);

      if (parent !== null) {
        api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));
        removeVnodes(parent, [oldVnode], 0, 0);
      }
    }

    for (i = 0; i < insertedVnodeQueue.length; ++i) {
      (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]);
    }
    for(i = 0; i < cbs.post.length; ++i) cbs.post[i](); // Execute hook: postreturn vnode;
  };
Copy the code

And then what does pathVnode do

  • The update operation is provided by the hook update, and then the child nodes are updated or added or deleted
  • Update and the handlers passed in when the init function above initializes, the actual elements are processed in this step
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
let i: any, hook: any;
if(isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) { i(oldVnode, vnode); // Execute hook: prepatch (defined on 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) {for(i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); // Execute hook: update I = vnode.data.hook;if(isDef(i) && isDef(i = i.update)) i(oldVnode, vnode); } / /if (isUndef(vnode.text)) {
  if (isDef(oldCh) && isDef(ch)) {
    if(oldCh ! == ch) updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue); }else if (isDef(ch)) {
    if (isDef(oldVnode.text)) api.setTextContent(elm, ' ');
    addVnodes(elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue);
  } else if (isDef(oldCh)) {
    removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
  } else if (isDef(oldVnode.text)) {
    api.setTextContent(elm, ' '); }}else if(oldVnode.text ! == vnode.text) {if (isDef(oldCh)) {
    removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
  }
  api.setTextContent(elm, vnode.text as string);
}
if(isDef(hook) &&isdef (I = hook. Postpatch)) {// Execute hook: postpatch (defined on VNode) I (oldVnode, VNode); }}Copy the code

Let’s finally take a look at how snabbDOM handles child element updates, which can be summarized as:

  • If a vNode does not exist, change the location of the vNode to narrow the processing scope
  • If both children of the old and new VNodes exist and are the same node, the method of patchVnode is recursively called
  • If the current operation of the new VNode child node is equal to the old VNode child node, it indicates that the child node position has been moved, the operation will be inserted
  • If none of the preceding conditions is met, the system checks whether the child node of the new VNode exists in the inactive child node of the old VNode. If the node does not exist, it is judged as a new node, and a DOM will be created to perform the insertion operation. If the sels are the same, update them and insert them. If the sels are different, create child nodes
  • After exiting the preceding loop, if the new VNode or the old VNode has remaining child nodes, the node insertion or deletion operation continues
  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;

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (oldStartVnode == null) {
        oldStartVnode = oldCh[++oldStartIdx]; left
      } else if (oldEndVnode == null) {
        oldEndVnode = oldCh[--oldEndIdx];
      } else if (newStartVnode == null) {
        newStartVnode = newCh[++newStartIdx];
      } else if(newEndVnode == null) { newEndVnode = newCh[--newEndIdx]; // These are all empty elements}else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
        oldStartVnode = oldCh[++oldStartIdx];
        newStartVnode = newCh[++newStartIdx];
      } else if(sameVnode(oldStartVnode, newEndVnode)) { patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); api.insertBefore(parentElm, oldStartVnode.elm as Node, api.nextSibling(oldEndVnode.elm as Node)); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; // The above two are the elements of the movement of the case}else {
        if (oldKeyToIdx === undefined) {
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
        }
        idxInOld = oldKeyToIdx[newStartVnode.key as string];
        if(isUndef(idxInOld)) {// Check whether the new vNode exists in the old vNode. InsertBefore (parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVNode.elm as Node); newStartVnode = newCh[++newStartIdx]; }else {
          elmToMove = oldCh[idxInOld];
          if(elmToMove.sel ! == newStartVnode.sel) { api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node); }else{ patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); oldCh[idxInOld] = undefined as any; api.insertBefore(parentElm, (elmToMove.elm as Node), oldStartVnode.elm as Node); } newStartVnode = newCh[++newStartIdx]; }} / /...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);
      } else{ removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); }}}Copy the code

The above are the main operations of SNabbDOM when updating nodes, which can be summarized as follows

  • The update behavior of the element itself is handled by the function passed in during init, which only cares about updating the attributes of the element itself
  • The snabbDOM handles the position of an element and whether to add or update it. This process only deals with the position of an element and does not care about the update of the element itself

Three, copy writing

Once we understand the behavior of snabbDOM, we can do simple (no special case, just simple implementation of the function) imitation to practice our hands and deepen our understanding

1. Define the vNode type

const NODE_KEY = Symbol('vNode')

type Style = {
  [key: string]: string
}

export typevNodeModal = { tag: string class? : string id? : string style? : Style [NODE_KEY]: string elem? : Element children? : Array<vNodeModal | string> }Copy the code

Here, I use symbol as a unique symbol to facilitate the accurate judgment of whether vNodes and vnodes are the same

export const isVNode = (elem: vNodeModal | Element) => Boolean(elem && elem[NODE_KEY])

export const isSameNode = (node: vNodeModal, otcNode: vNodeModal) =>  node[NODE_KEY] === otcNode[NODE_KEY]

Copy the code

2. Define the constructor

I’m going to define tag as a mandatory property, and KEY as a private property, and I’m going to create it for it

const constructVNode = function(data: Partial<vNodeModal> & { tag: string }) {
  return {
    ...data,
    [NODE_KEY]: uuid()
  }
}
Copy the code

3. Define the update function

I call the update handler plugin to make it easier to understand, so plugin has nothing to do with this simple vNode library and is purely external


const init = function (plugins = []) {
  if(! plugins || ! plugins.length)return// save hooks to returns.foreach (function(hook) {
    plugins.forEach(function(plugin) {
      if (plugin[hook]) {
        handler[hook] ? handler[hook].push(plugin[hook]) : handler[hook] = [plugin[hook]]
      }
    })
  })

  return function(ctrlNode: Element | vNodeModal, newVNode: vNodeModal) {
    let oldVNode = ctrlNode
    if(! isVNode(ctrlNode)) oldVNode = transformToVNode(ctrlNode as Element)if (handler.pre) {
      handler.pre.map((preHandle) => { preHandle(oldVNode, newVNode) })
    }

    updateNode(oldVNode as vNodeModal, newVNode)

    if (handler.finish) {
      handler.finish.map((finishHandle) => { finishHandle(oldVNode, newVNode) })
    }

    return newVNode
  }
}
Copy the code

Next, update the function that handles the judgment

Const updateNode = const updateNode = const updateNode = const updateNode =function(oldVNode: vNodeModal, newVNode: vNodeModal) {
  if(! isSameVNode(oldVNode as vNodeModal, newVNode) || isTagChange(oldVNode, newVNode)) { const newElement = createDOMByVNode(newVNode) oldVNode.elem.replaceWith(newElement) }else{updateVNodeByModal(oldVNode, newVNode)}} // Update dom by VNode const updateVNodeByModal =function(oldVNode: vNodeModal, newVNode: vNodeModal) {
  if(handler.update.length) { handler.update.forEach((updateHandle) => { updateHandle(oldVNode, NewVNode)})} / / pair after updating the element itself for processing const oCh = oldVNode. Children | | [] const nCh = newVNode. Children | | []if(oCh.length && ! nCh.length) { removeAllChild(oldVNode.elem) }else if(! oCh.length && nCh.length) { inertNode(newVNode.elem, nCh) }else if (oCh.length && nCh.length) {
    diff(oldVNode, newVNode)

    for(let i = 0; i < nCh.length; i++) {
      if (isVNode(nCh[i])) {
        const idx = oCh.findIndex((oChild) => isSameVNode(nCh[i], oChild))
        if(idx > -1) updateNode(oCh[idx] as vNodeModal, nCh[I] as vNodeModal)}}}} // Const diff =function(oldVNode: vNodeModal, newVNode: Const oCh = oldvNode. children const nCh = newvNode. children const nLen = nch.lengthlet lastIdx = 0

  const getIndex = function(checkArray: Array<vNodeModal | string>, item: vNodeModal | string) {
    if (isVNode(item)) {
      return checkArray.findIndex(o => isSameVNode(o as vNodeModal, item as vNodeModal))
    } else {
      returnCheckarray.findindex (o => o === item)}} // Refer to the react diff policy, but not stringsfor (let i = 0; i < nLen; i++) {
    const oldIdx = getIndex(oCh, nCh[i])
    if (oldIdx > -1) {
      if (oldIdx < lastIdx) {
        if (typeof oCh[oldIdx] === 'string') {
          oldVNode.elem.childNodes[oldIdx].remove()
        }
        getElement(oCh[i]).after(getElement(oCh[oldIdx]))
      }
      lastIdx = Math.max(oldIdx, lastIdx)
    } else {
      const newElem = createDOMByVNode(nCh[i])
      if (i === 0) (oldVNode as vNodeModal).elem.parentElement.prepend(newElem)
      else {
        if (typeof nCh[i] === 'string') (oldVNode as vNodeModal).elem.childNodes[i].after(newElem)
        else getElement(nCh[i]).after(newElem)
      }
    }
  }

  for (let i = 0; i < oldVNode.children.length; i++) {
    const idx = getIndex(nCh, oCh[i])
    if (idx < 0) {
      if (typeof oCh[i] === 'string') {
        oldVNode.elem.childNodes[i].remove()
      } else {
        (oCh[i] as vNodeModal).elem.remove()
      }
    }
  }
}
Copy the code

4. Write plug-ins

Write a plug-in to update the class

const getClassList = (className: string) => className ? className.split('. ') : []

const updateClassName = function (oldVNode: vNodeModal, newVNode: vNodeModal) {
  const elem = newVNode.elem
  if(! elem)return
  const oldClassList = getClassList(oldVNode.class)
  const newClassList = getClassList(newVNode.class)
  if(! newClassList.length)return
  oldClassList.forEach((className) => {
    if(! newClassList.includes(className)) { elem.classList.remove(className) }else {
      newClassList.splice(newClassList.indexOf(className), 1)
    }
  })
  newClassList.forEach((className) => elem.classList.add(className))
}

const updateClassPlugin = {
  update: updateClassName
}
Copy the code

5. Use

Write it this way when you use it

import init from './tools/init'
import transFromClass from './tools/plugins/class'

import './style.css'

const inp1 = document.querySelector('#first')

const newV = constructVNode({
  tag: 'div',
  class: 'haha.mama',
  id: 'no',
  children: [
    'lalala',
    constructVNode({
      tag: 'input',
      class: 'asdad',
      id: '123'
    })
  ]
})

// 插入子元素
const patch = init([transFromClass])

let newModal = patch(inp1, newV)

// 交换子元素位置
setTimeout(() => {
  const changPosModal = {
    ...newModal,
    children: [newModal.children[1], newV.children[0]]
  }
  
  newModal = patch(newModal, changPosModal)
}, 500)

// 修改子元素属性
setTimeout(() => {
  const newChildren0 = {
    ...newModal.children[0] as vNodeModal,
    class: 'newChildren0'
  }
  
  const changClassModal = {
    ...newModal,
    children: [newChildren0, newModal.children[1] + 'juejin']
  }


  newModal = patch(newModal, changClassModal)
}, 1000)

// 删除子元素
setTimeout(() => {
  const deleteChildrenModal = {
    ...newModal,
    children: []
  }

  newModal = patch(newModal, deleteChildrenModal)
}, 1500)

Copy the code

Finally, the results:

  • The original HTML structure
  • Define our colors for easy viewing
  • Run it and see what happens

This allows for a very simple vDOM processing (missing boundary handling, special element handling, etc.)

The main thing snabbDOM does is make the STRUCTURE of the DOM much clearer and easier to manage, helping us optimize a number of operations and encapsulating the actual operation logic as we update DOM elements. And provides a series of plug-ins for us to use.


This is oneself of write such article for the first time, write to have bad place welcome everybody to criticize to point out! 😄