Recently I wanted to see how the React and Vue frameworks are implemented in the Virtual DOM and how they differ. So I first opened the source code of Vue to find the realization of virtual DOM. At the beginning, it mentioned that the virtual DOM update algorithm of Vue was realized based on Snabbdom. So, to clone the source code of Snabbdom, found that its source code is not very complex and the star 🌟 is also a lot, so take a close look at it again, here will learn in detail how it is to achieve virtual DOM.

Snabbdom is a library that implements virtual DOM, simple, modular, and powerful features and performance.

A virtual DOM library with focus on simplicity, modularity, powerful features and performance.

This is the repository address of Snabbdom.

init

Snabbdom simple is based on its modularity, it is very clever, the design of virtual dom in the core logic would only focus on vNode updating algorithm computation, and each node to update specific parts, such as props, class, styles, datalist put in separate modules, such as This is done by firing hook functions on different modules at different times. Decoupling in this way not only makes the code more clearly organized, but also allows each part to focus on implementing a specific function, which in design patterns is also known as the single responsibility principle. When used in a real scenario, you can introduce only the specific modules you need. For example, we only update the class name and style of the node, not the attributes and events, so we only need to reference the class and style modules. For example,

// We only need the class and style modules here, so we can reference only these two modules
var patch = snabbdom.init([
 require('snabbdom/modules/class').default,
 require('snabbdom/modules/style').default,
]);
Copy the code

The core method is this init, so let’s just take a quick look at the implementation of this function,

// This is the hook function in module
const hooks = ['create'.'update'.'remove'.'destroy'.'pre'.'post'];
export function init(modules:Array<Partial<Module>>, domApi? :DOMAPI){
    let i:number, j:number, cbs = ({} as ModuleHooks);
    constapi: DOMAPI = domApi ! = =undefined ? domApi : htmlDomApi;
    // CBS stores the hook functions defined in modules introduced
    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]].push(hook); }}}// There are some other internal methods that are defined for patch
    function emptyNodeAt(){/... /};function createRmCb(){/... /};function createElm(){/... /};function addVnodes(){/... /};function invokeDestroyHook(){/... /};function removeVnodes(){/... /};function updateChildren(){/... /};function patchVnode(){/... /};// Init returns a patch function that takes two parameters, the first is the vNode or real DOM node to be updated, and the second is the new vNode to be updated
    return function patch(oldVnode: VNode | Element,vnode:VNode) :VNode{
        / /...}}Copy the code

The init function as a whole takes a modules array and returns a new function, patch. Isn’t that the familiar closure function? In init, it stores the hook functions introduced into the module in the CBS variable by traversing it, and then fires them accordingly when the update algorithm is executed. It only needs to be initialized once, and all subsequent updates of the Virtual DOM are completed through patch.

The flow chart is as follows,

patch

The most complex and time-consuming part is how to update the Virtual DOM. The performance of the entire framework is directly affected by the update algorithm. For example, the React-Reconciler module in React and the VDOM module in VUE can be optimized to the greatest extent. The updating logic of virtual DOM in Snabbdom is roughly as follows:

// This patch is returned by init
function patch(oldVnode,vnode){
    // Step 1: If oldVnode is Element, create an empty vnode based on Element, which is also the root of the vNode tree
    if(! isVnode(oldVnode)){ oldVnode = emptyAtNode(oldVnode); }// Step 2: Check whether oldVnode has the same element as vNode. If so, update the element. They are the same because they have the same key, the same tagName, the same ID attribute, and the same class
    if(sameVnode(oldVnode,vnode)){
        patchVnode(oldVnode,vnode);
    }else{
        // Step 3: If not, replace oldVnode with a new element with vNode and delete oldVnode.
        elm = oldVnode.elm;
        parent = api.parentNode(elm);
        createElm(vnode);
        if(parent ! = =null){
            api.insertBefore(parent,vnode.elm,api.nextSlibing(elm));
            removeVnodes(parent,[oldVnode], 0.0); }}}Copy the code

Patch logic can be simplified as follows:

  1. If oldVnode is of Element type, an empty Vnode is created based on oldVnode, which is also the root node of the vNode tree
  2. Compare oldVnode with vnode, if it is the same vnode (same key) or element of the same type (same tagName, same ID, same class), then call oldVnodepatchVnode
  3. Otherwise, create a new element based on vNode and replace the oldVnode element with the new element and delete the oldVnode

The flow chart is as follows,

In step 3, when oldVnode is different from vnode, it directly abandons the old node and creates a new node to replace it. When creating a node with a new vnode, it checks whether the current vnode has children, and if so, it also traverses children to create a new element. This means that the oldVnode and all its children will be replaced as a whole by the new VNode. The schematic diagram is as follows,

If B is not the same as B’, the child D of B is replaced by the child D and E of B’ in the process of replacing B’.

patchVnode

Step 2: If oldVnode is the same as vNode, it will reuse the dom already created, but update the dom’s differences, such as text, class, datalist, style, etc. This is implemented in the function patchVnode. The following is its general logic.

function patchVnode(oldVnode,vnode){
    const elm = oldVnode.elm; // Get the DOM object of oldVnode
    vnode.elm = elm; // Point vNode's ELM directly to ELM and reuse oldVnode's DOM object because they are of the same type
    // If oldVnode is equal to vNode, it is returned directly without updating at all
    if(oldVnode === vnode){
        return;
    }
    // If vnode contains text and does not equal oldvNode. text, update elm's textContent to vnode.text
    if(isDef(vnode.text) && vnode.text ! == oldVnode.text){return api.setTextContext(elm,vnode.text);
    }
    let oldCh = oldVnode.children; // Get the children of the oldVnode
    let ch = vnode.children; // Get the child node of the vnode
    
    // If oldVnode has no children and vnode has children, add children of vnode
    if(isUndef(oldCh) && isDef(ch)){
        // If oldVnode has a text value, empty elm's textContent first
        if(idDef(oldVnode.text)){
            api.setTextContext(elm,' ');
        }
        addVnodes(elm,null,ch,0,ch.length- 1);
    }
    // If oldVnode has children and vnode has no children, oldVnode's children are deleted
    else if(isUndef(ch) && isDef(oldCh)){
        reoveVnodes(elm,oldCh,0,oldCh.length- 1)}// If they both have children and the children are different, their children are updated
    else if(ch ! == oldCh){ updateChildren(elm,oldCh,ch); }If oldVnode has a text value, empty the elm textContent
    else if(ifDef(oldVnode.text)){
        api.setTextContext(elm,' '); }}Copy the code

The patchVnode logic can be simplified as follows:

  1. Directly set the ELM of VNode to the ELM of oldVnode to reuse existing DOM objects and avoid the overhead of creating new DOM objects
  2. OldVnode === vnode; if oldVnode == vnode; if oldVnode == vnode
  3. If vNode has a text value, then ELM contains only plain text and no other types of child nodes. If its value is different from oldVnode’s text, then elm’s textContent is updated and returns.
  4. This is where we start to really compare their children,
    • If vNode has children and oldVnode does not, delete elm’s textContext and add vNode’s children
    • If vnode has no children and oldVnode has children, delete the children of oldVnode directly
    • If they all have children and are not identical, their children are updated
    • If they all have children and are the same, clear elm’s textContext

The flow chart is as follows,

When patchVnode is updated, VNode will first trigger the hook function defined on data data to update the information of its node, such as class or styles, and then update the information of children node.

updateChildren

Updating the vNode. children information is done using the updateChildren function. Only if children exist on oldVnode and children exist on vNode, and oldvNode.children! When == vnode.children, updateChildren is called. To sort out the general logic of Update Dren,

function updateChildren(parentElm,oldCh,newCh){
    / / the old children
    let oldStartIdx = 0;
    let oldEndIdx = oldCh.length- 1;
    let oldStartVnode = oldCh[oldStartIdx];
    let oldEndVnode = oldCh(oldEndIdx);
    
    / / new children
    let newStartIdx = 0;
    let newEndIdx = newCh.length- 1;
    let newStartVnode = newCh(newStartIdx);
    let newEndVnode = newCh(newEndIdx);
    
    let before = null;
    
    // loop comparison
    while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){
        if(oldStartVnode == null) {// The current node may have been moved
            oldStartVnode = oldCh[++oldStartIdx];
        }else if(oldEndVnode == null){
            oldEndVnode = oldCh[--oldEndIdx];
        }else if(newStartVnode == null){
            newStartVnode = newCh[++newStartIdx];
        }else if(newEndVnode == null){
            newEndVnode = newCh[--newEndIdx];
        }else if(sameVnode(oldStartVnode,newStartVnode)){
            patchVnode(oldStartVnode,newStartVnode); / / update the newStartVnode
            oldStartVnode = oldCh[++oldStartIdx]; // oldStartIdx moves to the right
            newStartVnode = newCh[++newStartIdx]; // newStartIdx moves to the right
        }else if(sameVnode(oldEndVnode,newEndVnode)){
            patchVnode(oldEndVnode,newEndVnode); / / update the newEndVnode
            oldEndVnode = oldCh[--oldEndIdx]; // oldEndIdx moves to the left
            newEndVnode = newCh[--newEndIdx]; // newEndIdx moves left
        }else if(sameVnode(oldStartVnode,newEndVnode)){
            patchVnode(oldStartVnode,newEndVnode); / / update the newEndVnode
            let oldAfterVnode = api.nextSibling(oldEndVnode);
            // Move oldStartVnode after the current oldEndVnode
            api.insertBefore(parentElm, oldStartVnode.elm,oldAfterVnode);
            oldStartVnode = oldCh[++oldStartIdx]; // oldStartIdx moves to the right
            newEndVnode = newCh[--newOldVnode]; // newEndIdx moves left
        }else if(sameVnode(oldEndVnode,newStartVnode)){
            patchVnode(oldEndVnode,newStartVnode); / / update the newStartVnode
            // Move oldEndVnode in front of oldStartVnode
            api.insertBefore(parentElm,oldEndVnode.elm,oldStartVnode.elm);
            oldEndVnode = oldCh[--oldEndIdx]; // oldEndVnode moves to the right
        	newStartVnode = newCh[++newStartIdx]; // newStartVnode moves to the left
        }else{
            // Get the key of the current children node and its index.
            if(oldKeyIdx == undefined){
                oldKeyIdx = createKeyToOldIdx(oldCh,oldStartIdx,oldEndIdx);
            }
            // Check whether the current newStartVnode key exists in the old children array
            idxInOld = oldKeyIdx[newStartVnode.key];
            if(isUndef(idxInOld)){
                // If the current newStartVnode key does not exist in the old children array, then the newStartVnode is a new dom
                let newDom = createElm(newStartVnode);
                api.insertBefore(parentElm,newDom,oldStartVnode.elm);
                newStartVnode = newCh[++newStartIdx];
            }else{
                // Otherwise, the current newStartVnode key exists in the old children, indicating that they were previously the same Vnode.
                elmToMove = oldCh[idxInOld];
                if(elmToMove.sel ! == newStartVnode.sel){// The node type has changed. It is not a DOM element of the same type and needs to be created
                    let newDom = createElm(newStartVnode);
                    api.insertBefore(parentElm,newDom,oldStartVnode.elm);
                }else{
                    // If they are the same Vnode and the DOM element is the same, they do not need to be created
                    patchVnode(elmToMove,newStartVnode);
                    oldCh[idxInOld] = undefined; // The element marking the current location of the old children has been removed,api.insertBefore(parentElm,elmToMove,oldStartVnode.elm); } newStartVnode = newCh[++newStartIdx]; }}}// If there are unprocessed children after the loop,
    if(oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx){
        // If the new children are not processed, add the extra parts
        if(oldStartIdx > oldEndIdx){
            before = newCh[newEndIdx+1] = =null ? null : newCh[newEndIdx+1];
            addVnodes(parentElm,before,newCh,newStartIdx,newEndIdx);
        }else{
            // If the old children are not processed, delete the many partsremoveVnodes(parentElm,oldCh,oldStartIdx,oldEndIdx); }}}Copy the code

The updateChildren function logic can be simplified as,

  1. Initialize the loop variable
  2. Loop through old children and new children according to variables, compare and update one by one, when the type is the same, then callpatchVnodeUpdate, when the type is different, directly create a new VNode DOM element and insert it into the appropriate position
  3. After the loop is complete, add new vNodes and remove old redundant vnodes

The flow chart is as follows,

In the updateChildren function, when the nodes in children are updated one by one, when the two nodes are of the same type, patchVnode is called in turn to update the nodes, thus, in fact, there is an indirect recursive call.

life cycle hooks

When you use React or Vue, you’ll see that they each define the component’s lifecycle methods. Although the names or timing are not exactly the same, the basic order and purpose are the same. Snabbdom also provides the corresponding lifecycle hook functions. The difference is that it provides two sets of hook functions. One set is for the virtual DOM, such as a Vnode create,update,remove, etc. One is for modules, which triggers hooks on different modules at different times to update the current Vnode.

Modules’ hook functions are as follows,

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

Its timing is shown below,

When the hooks function of Modules are triggered, different functions accept different arguments.

Name Triggered when Arguments to callback
pre inpatchAt the beginning of the function There is no
create increateElmFunction to create an element vnode
update inpathVnodeWhen a Vnode is updated in a function, oldVnode.newVnode
destroy inremoveVnodesFunction to remove a Vnode, vnode
remove inremoveVnodesFunction to remove a Vnode, vnode.removeCallback
post inpatchAt the end of the function, There is no

Pre and post functions are not defined in most modules. They operate on the current Vnode in create, Update, destory, and remove. For example, inside the class Module create function on Vnode,

// Class modules operate on the current Vnode in the create hook function
function updateClass(oldVnode: VNode, vnode: VNode) :void {
  var cur: any, name: string, elm: Element = vnode.elm as Element,
      oldClass = (oldVnode.data as VNodeData).class,/ / the old class
      klass = (vnode.data as VNodeData).class; / / the new class

  if(! oldClass && ! klass)return; // There is no class
  if (oldClass === klass) return; // Return the same value
  oldClass = oldClass || {};
  klass = klass || {};

    // Delete those that exist on oldVnode but not vNode
  for (name in oldClass) {
    if (!klass[name]) {
      elm.classList.remove(name);
    }
  }
    // Traverses the class on the current vnode,
  for (name in klass) {
    cur = klass[name];
      If you don't want to wait
    if(cur ! == oldClass[name]) {// If true, add class, otherwise remove class
      (elm.classList as any)[cur ? 'add' : 'remove'](name); }}}Copy the code

Other hook functions on other modules will also update the current vNode, which is not listed here.

Let’s look at the hook function on Vnode as follows:

export interfaceHooks { init? : InitHook; create? : CreateHook; insert? : InsertHook; prepatch? : PrePatchHook; update? : UpdateHook; postpatch? : PostPatchHook; destroy? : DestroyHook; remove? : RemoveHook; }Copy the code

Its trigger time and accept parameters are as follows,

Name Triggered when Arguments to callback
init increateElmIs triggered firstinit vnode
create increateElm“, the element has been created, and the corresponding children have been createdcreate emptyVnode.vnode
insert whenvnode.elmHas been updated to the DOM document, and finally inpatchFired at the end of the function vnode
prepatch inpatchVnodeIt was triggered right from the startprepatch oldVnode.vnode
update inpatchVnode,vnode.elm=oldVnode.elmAfter that, update children before triggering oldVnode.vnode
postpatch inpatchVnodeAt the end of the, has been updated to children after triggering, oldvnode.vnode
destroy inremoveVnodesHas not been removed at this time vnode
remove inremoveVnodes,destroyAfter the trigger, not really removed at this point, need to callremoveCallbackTo actually remove the element vnode.removeCallback

The hook functions on Vnode are our own, defined in data.hooks, for example,

h('div.row', {
  key: movie.rank,
  hook: {
    insert: (vnode) = >{ movie.elmHeight = vnode.elm.offsetHeight; }}});Copy the code

summary

After looking at the source code, the most complicated part is updating child nodes in updateChildren, where a lot of judgment and comparison is made to avoid repeating the creation of elements in order to maximize the reuse of previously created elements. Like React and Vue, it also adds keys to the comparison to optimize this. When updating Vnode’s corresponding element, it breaks the different data into different modules to update, triggered by hook functions, which is very elegant.