vue2 source learning appetizer-snabbdom source learning (1)

Posted by wilburforce on Thu, 11 Jul 2019 03:10:55 +0200

Preface

Recently, in learning the source code of vue2.0, I just started to look at the source code of vdom, but I really can't find the direction, because it is added to the implementation of vdom.
Many hooks of vue2.0 itself make reading more difficult. So I saw the first line saying that vdom of vue2.0 is snabbdom.
On the basis of that, snabbdom has less than 300 slocs, so it's better to start with snabbdom and get familiar with its principles.
With the vdom of vue2.0, the effect may be better.

What is virtual-dom

virtual-dom can be seen as a JavaScript tree that simulates the DOM tree. It mainly realizes a nonexistence through vnode.
A component in state, when its state is updated, triggers a change in virtual-dom data, and then passes through virtual-dom.
Compared with the real DOM, the real DOM is updated.

Why virtual-dom

We know that when we want to implement an interface with complex states, if we bind to every component that may change.
Events, bind field data, and soon because there are too many states, we will need to maintain more and more events and fields, as well as code.
More and more complex, so we wonder if we can separate the view from the state, as long as the view changes, the corresponding state also occurs.
Change, then change the state, and we can redraw the whole view. That's a good idea, but it's too expensive, so we did it again.
Think, can you just update the view whose state has changed? So virtual-dom came into being, and the state changes were fed back to vdom first.
vdom finds the minimum update view, and finally updates it to the real DOM in batches, so as to achieve performance improvement.

In addition, from the perspective of portability, virtual-dom also abstracts the real dom, which means virtual-dom corresponds to each other.
It is not the dom of the browser, but the components of different devices, which greatly facilitates the use of multiple platforms.

snabbdom directory structure

Well, that's all. Let's take a look at snabbdom first. I'm looking at this version. snabbdom
(Heart plug, typescript learning is not deep, see the latest version of a bit of effort, so choose a version before ts version). Okay, let's go ahead.
Look at the main directory structure of snabbdom.

Name type explain
dist Folder It contains snabddom packaged files
examples Folder It contains examples of using snabbdom
helpers Folder Contains tools needed for svg operations
modules Folder Includes operations on attribute, props, class, dataset, eventlistner, style, hero
perf Folder performance testing
test Folder test
h file Transforming state into vnode
htmldomapi file Abstraction of native dom operation
is file Judgment type
snabbdom.bundle file snabbdom itself relies on packaging
snabbdom file snabbdom core, including diff, patch and other operations
thunk file thunk function realization under snabbdom
vnode file Constructing vnode

Snbbdom Source Tour

First stop vnode

First of all, we start with the simplest vnode. The function of vnode is very simple, that is to say, the input data is converted into vnode.
Object form

    //VNode function for converting input to VNode
    /**
     *
     * @param sel    selector
     * @param data    Binding data
     * @param children    Array of child nodes
     * @param text    Current text node content
     * @param elm    A Reference to Real dom element
     * @returns {{sel: *, data: *, children: *, text: *, elm: *, key: undefined}}
     */
    module.exports = function ( sel, data, children, text, elm ) {
        var key = data === undefined ? undefined : data.key;
        return {
            sel: sel, data: data, children: children,
            text: text, elm: elm, key: key
        };
    };

vnode has five main attributes:

  • Selsels correspond to selectors in the form of'div','div a','div a.b.c'.

  • Data corresponds to data bound by vnode, and can be of the following types: attribute, props, eventlistner, and
    class,dataset,hook

  • children subelement array

  • Text text, representing the text content in the node

  • There are references to the corresponding true dom element in elm

  • key is used to compare different vnode s

Second stop h

After vnode, it's h. h is also a wrapping function. It's mainly to make another layer of wrapping on vnode. The functions are as follows.

  • If it is svg, add a namespace to it

  • Wrap text in children into vnode form

    var VNode = require ( './vnode' );
    var is = require ( './is' );
    //Add namespaces (svg is required)
    function addNS ( data, children, sel ) {
        data.ns = 'http://www.w3.org/2000/svg';
    //If the selector
        if ( sel !== 'foreignObject' && children !== undefined ) {
            //Add namespaces recursively for child nodes
            for (var i = 0; i < children.length; ++i) {
                addNS ( children[ i ].data, children[ i ].children, children[ i ].sel );
            }
        }
    }
    //Rendering VNode to VDOM
    /**
     *
     * @param sel selector
     * @param b    data
     * @param c    Subnode
     * @returns {{sel, data, children, text, elm, key}}
     */
    module.exports = function h ( sel, b, c ) {
        var data = {}, children, text, i;
        //If there are child nodes
        if ( c !== undefined ) {
            //So the second item of h is data.
            data = b;
            //If c is an array, then there are child element nodes
            if ( is.array ( c ) ) {
                children = c;
            }
            //Otherwise, it's a sub text node
            else if ( is.primitive ( c ) ) {
                text = c;
            }
        }
        //If c does not exist and b only exists, then the vdom that needs to be rendered does not exist in the data part and only in the sub-node part.
        else if ( b !== undefined ) {
            if ( is.array ( b ) ) {
                children = b;
            }
            else if ( is.primitive ( b ) ) {
                text = b;
            }
            else {
                data = b;
            }
        }
        if ( is.array ( children ) ) {
            for (i = 0; i < children.length; ++i) {
                //If the existing node in the subnode array is of the original type, it means that the node is a text node, so we render it as a VNode containing text only.
                if ( is.primitive ( children[ i ] ) ) children[ i ] = VNode ( undefined, undefined, undefined, children[ i ] );
            }
        }
        //If it's svg, you need to add a namespace for the node
        if ( sel[ 0 ] === 's' && sel[ 1 ] === 'v' && sel[ 2 ] === 'g' ) {
            addNS ( data, children, sel );
        }
        return VNode ( sel, data, children, text, undefined );
    };

Third stop htmldomapi

The htmlDOM API provides a layer of abstraction for native dom operations, which is not explained here.

Fourth station modules

modules mainly include attributes, class, props, dataset, eventlistener, hero, style.
These modules, attributes,class,props,dataset,eventlistener,style are ours.
These are also the default injections of snabbdom.bundle, which are needed everyday. Here we will introduce these modules in detail.

attributes

The main functions are as follows:

  • Delete attributes that do not exist in vnode from the properties of elm (including those boolean class attributes, if the new vnode is set to false, the same is deleted)

  • If oldvnode and vnode use the same name attribute, the corresponding attribute value is updated on elm

  • If vnode has new properties, add them to elm

  • If there is a namespace, set AttributeNS

    var NamespaceURIs = {
      "xlink": "http://www.w3.org/1999/xlink"
    };
    
    var booleanAttrs = ["allowfullscreen", "async", "autofocus", "autoplay", "checked", "compact", "controls", "declare",
                    "default", "defaultchecked", "defaultmuted", "defaultselected", "defer", "disabled", "draggable",
                    "enabled", "formnovalidate", "hidden", "indeterminate", "inert", "ismap", "itemscope", "loop", "multiple",
                    "muted", "nohref", "noresize", "noshade", "novalidate", "nowrap", "open", "pauseonexit", "readonly",
                    "required", "reversed", "scoped", "seamless", "selected", "sortable", "spellcheck", "translate",
                    "truespeed", "typemustmatch", "visible"];
    
    var booleanAttrsDict = Object.create(null);
    
    //Create a dictionary of attributes, default to true
    for(var i=0, len = booleanAttrs.length; i < len; i++) {
      booleanAttrsDict[booleanAttrs[i]] = true;
    }
    
    function updateAttrs(oldVnode, vnode) {
      var key, cur, old, elm = vnode.elm,
          oldAttrs = oldVnode.data.attrs, attrs = vnode.data.attrs, namespaceSplit;
    
    
      //If neither the old node nor the new node contains attributes, return immediately
      if (!oldAttrs && !attrs) return;
      oldAttrs = oldAttrs || {};
      attrs = attrs || {};
    
      // update modified attributes, add new attributes
      //Update changed attributes and add new attributes
      for (key in attrs) {
        cur = attrs[key];
        old = oldAttrs[key];
        //If the old attribute is different from the new one
        if (old !== cur) {
        //If it's a boolean class attribute, when vnode is set to false value, delete it directly instead of updating it
          if(!cur && booleanAttrsDict[key])
            elm.removeAttribute(key);
          else {
            //Otherwise, update attribute values or add attributes
            //If a namespace exists
            namespaceSplit = key.split(":");
            if(namespaceSplit.length > 1 && NamespaceURIs.hasOwnProperty(namespaceSplit[0]))
              elm.setAttributeNS(NamespaceURIs[namespaceSplit[0]], key, cur);
            else
              elm.setAttribute(key, cur);
          }
        }
      }
      //remove removed attributes
      // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
      // the other option is to remove all attributes with value == undefined
      //Delete properties of old nodes that are not in new node properties
      for (key in oldAttrs) {
        if (!(key in attrs)) {
          elm.removeAttribute(key);
        }
      }
    }
    
    module.exports = {create: updateAttrs, update: updateAttrs};

class

The main functions are as follows:

  • Delete classes that do not exist in vnode or have a value of false from elm

  • Add the new class in vnode to elm

    function updateClass(oldVnode, vnode) {
      var cur, name, elm = vnode.elm,
          oldClass = oldVnode.data.class,
          klass = vnode.data.class;
      //If neither the old node nor the new node has a class, return directly
      if (!oldClass && !klass) return;
      oldClass = oldClass || {};
      klass = klass || {};
      //Delete classes that do not exist for new nodes from old nodes
      for (name in oldClass) {
        if (!klass[name]) {
          elm.classList.remove(name);
        }
      }
      //If the class corresponding to the old node is set to false in the new node, delete the class and add the class if the new one is set to true
      for (name in klass) {
        cur = klass[name];
        if (cur !== oldClass[name]) {
          elm.classList[cur ? 'add' : 'remove'](name);
        }
      }
    }
    
    module.exports = {create: updateClass, update: updateClass};

dataset

The main functions are as follows:

  • Delete attributes from attribute sets where vnode does not exist from elm

  • Update the attribute values in the attribute set

    function updateDataset(oldVnode, vnode) {
      var elm = vnode.elm,
        oldDataset = oldVnode.data.dataset,
        dataset = vnode.data.dataset,
        key
    
      //If the old and new nodes do not have data sets, they are returned directly
      if (!oldDataset && !dataset) return;
      oldDataset = oldDataset || {};
      dataset = dataset || {};
     //Delete data sets that do not exist in the new node in the old node
      for (key in oldDataset) {
        if (!dataset[key]) {
          delete elm.dataset[key];
        }
      }
      //Update Data Set
      for (key in dataset) {
        if (oldDataset[key] !== dataset[key]) {
          elm.dataset[key] = dataset[key];
        }
      }
    }
    
    module.exports = {create: updateDataset, update: updateDataset}

eventlistener

In snabbdom, event processing is wrapped. The real DOM event triggers the operation of vnode. The main way is:

CreateListner => Return handler as event listener => Bind vnode => Handler as event handler for real DOM
After a real DOM event is triggered, => handler obtains the real DOM event object, => passes the real DOM event object into handleEvent => handleEvent to find it.
The corresponding vnode event handler is then called to modify the vnode

//In snabbdom, event processing is wrapped in a layer. The real DOM event triggers the operation of vnode.
//The main approach is
// CreateListner => Return handler as event listener => Bind vnode => Handler as event handler for real DOM
//After a real DOM event is triggered, => handler obtains the real DOM event object, => passes the real DOM event object into handleEvent => handleEvent to find it.
//The corresponding vnode event handler is then called to modify the vnode

//Event handling for vnode
function invokeHandler ( handler, vnode, event ) {
    if ( typeof handler === "function" ) {
        // call function handler
        //Call event handlers on vnode
        handler.call ( vnode, event, vnode );
    }
    //Existence of event-bound data or presence of multi-event handlers
    else if ( typeof handler === "object" ) {

        //Explain that there is only one event handler
        if ( typeof handler[ 0 ] === "function" ) {
            //If there is only one bound data, call the data directly to improve performance.
            //Like on:{click:[handler,1]}
            if ( handler.length === 2 ) {
                handler[ 0 ].call ( vnode, handler[ 1 ], event, vnode );
            }
            //If there are multiple bound data, they are converted into arrays and called in an apply manner, and apply performs worse than call.
            //For example: on:{click:[handler,1,2,3]}
            else {
                var args = handler.slice ( 1 );
                args.push ( event );
                args.push ( vnode );
                handler[ 0 ].apply ( vnode, args );
            }
        } else {
            //If there are different processors with multiple identical events, the call is recursive
            //For example, on:{click:[[handeler1,1],[handler,2]}
            for (var i = 0; i < handler.length; i++) {
                invokeHandler ( handler[ i ] );
            }
        }
    }
}

/**
 *
 * @param event Real dom event object
 * @param vnode
 */
function handleEvent ( event, vnode ) {
    var name = event.type,
        on = vnode.data.on;

    // If the corresponding vnode event handler is found, the call is made
    if ( on && on[ name ] ) {
        invokeHandler ( on[ name ], vnode, event );
    }
}
//Event listener generator for handling real DOM events
function createListener () {
    return function handler ( event ) {
        handleEvent ( event, handler.vnode );
    }
}
//Update Event Listening
function updateEventListeners ( oldVnode, vnode ) {
    var oldOn = oldVnode.data.on,
        oldListener = oldVnode.listener,
        oldElm = oldVnode.elm,
        on = vnode && vnode.data.on,
        elm = vnode && vnode.elm,
        name;

    // optimization for reused immutable handlers
    //If the old and new event listeners are the same, return directly
    if ( oldOn === on ) {
        return;
    }

    // remove existing listeners which no longer used
    //If there is no event listener on the new node, delete all event listeners on the old node
    if ( oldOn && oldListener ) {
        // if element changed or deleted we remove all existing listeners unconditionally
        if ( !on ) {
            for (name in oldOn) {
                // remove listener if element was changed or existing listeners removed
                oldElm.removeEventListener ( name, oldListener, false );
            }
        } else {
            //Delete event listeners where new nodes do not exist in old nodes
            for (name in oldOn) {
                // remove listener if existing listener removed
                if ( !on[ name ] ) {
                    oldElm.removeEventListener ( name, oldListener, false );
                }
            }
        }
    }

    // add new listeners which has not already attached
    if ( on ) {
        // reuse existing listener or create new
        //If a listener already exists on the oldvnode, the vnode is directly multiplexed, otherwise a new event handler is created.
        var listener = vnode.listener = oldVnode.listener || createListener ();
        // update vnode for listener
        //Binding vnode on event handler
        listener.vnode = vnode;

        // if element changed or added we add all needed listeners unconditionally'
        //If there is no event handler on oldvnode
        if ( !oldOn ) {
            for (name in on) {
                // add listener if element was changed or new listeners added
                //Add event handlers on vnode directly to elm
                elm.addEventListener ( name, listener, false );
            }
        } else {
            for (name in on) {
                // add listener if new listener added
                //Otherwise, add event handlers that are not available on oldvnode
                if ( !oldOn[ name ] ) {
                    elm.addEventListener ( name, listener, false );
                }
            }
        }
    }
}

module.exports = {
    create: updateEventListeners,
    update: updateEventListeners,
    destroy: updateEventListeners
};

props

Main functions:

  • Delete attributes that do not exist in vnode from elm

  • Update attributes on elm

    function updateProps(oldVnode, vnode) {
      var key, cur, old, elm = vnode.elm,
          oldProps = oldVnode.data.props, props = vnode.data.props;
     //If neither new nor old nodes have attributes, they are returned directly
      if (!oldProps && !props) return;
      oldProps = oldProps || {};
      props = props || {};
      //Delete attributes that the new node does not have in the old node
      for (key in oldProps) {
        if (!props[key]) {
          delete elm[key];
        }
      }
      //Update attributes
      for (key in props) {
        cur = props[key];
        old = oldProps[key];
        //If the attributes of old and new nodes are different, and the attributes of comparison are different from those of value or elm, then they need to be updated.
        if (old !== cur && (key !== 'value' || elm[key] !== cur)) {
          elm[key] = cur;
        }
      }
    }
    
    module.exports = {create: updateProps, update: updateProps};
    
    

style

The main functions are as follows:

  • Empty the style that exists in old vnode but does not exist in vnode

  • If delayed in vnode.style is different from oldvnode, the attribute value of delayed is updated, and the style of elm is set to that value in the next frame to achieve animation transition effect.

  • Direct update of style for non-delayed and remote

  • When vnode is destroy ed, the corresponding style is updated directly to the value of vnode.data.style.destory

  • When a vnode is reomve, if style.remove does not exist, call the global remove hook directly to the next remove process
    If style. remote exists, then we need to set the remove animation transition effect and wait until the transition effect is over before calling it.
    Next remove process

    //If there is a request Animation Frame, use it directly to optimize performance, otherwise use setTimeout
    var raf = (typeof window !== 'undefined' && window.requestAnimationFrame) || setTimeout;
    var nextFrame = function(fn) { raf(function() { raf(fn); }); };
    
    //Realize animation effect through nextFrame
    function setNextFrame(obj, prop, val) {
      nextFrame(function() { obj[prop] = val; });
    }
    
    function updateStyle(oldVnode, vnode) {
      var cur, name, elm = vnode.elm,
          oldStyle = oldVnode.data.style,
          style = vnode.data.style;
      //If neither old vnode nor vnode has a style, return directly
      if (!oldStyle && !style) return;
      oldStyle = oldStyle || {};
      style = style || {};
      var oldHasDel = 'delayed' in oldStyle;
      //Traversing the style of oldvnode
      for (name in oldStyle) {
        //If there is no such style in the vnode, empty it
        if (!style[name]) {
          elm.style[name] = '';
        }
      }
      //If there is a delayed in the style of the vnode and it is different from the old vnode, set the delayed parameter in the next frame
      for (name in style) {
        cur = style[name];
        if (name === 'delayed') {
          for (name in style.delayed) {
            cur = style.delayed[name];
            if (!oldHasDel || cur !== oldStyle.delayed[name]) {
              setNextFrame(elm.style, name, cur);
            }
          }
        }
        //If it's not the style of delayed and remote, and it's different from the value of oldvnode, set the new value directly
        else if (name !== 'remove' && cur !== oldStyle[name]) {
          elm.style[name] = cur;
        }
      }
    }
    
    //Set the style when the node is destory
    function applyDestroyStyle(vnode) {
      var style, name, elm = vnode.elm, s = vnode.data.style;
      if (!s || !(style = s.destroy)) return;
      for (name in style) {
        elm.style[name] = style[name];
      }
    }
    //Delete effect. When we delete an element, we call back the Delete Excessive Effect before we remove the node after the transition.
    function applyRemoveStyle(vnode, rm) {
      var s = vnode.data.style;
      //If there is no style or style.
      if (!s || !s.remove) {
        //Calling rm directly means actually calling the global remote hook
        rm();
        return;
      }
      var name, elm = vnode.elm, idx, i = 0, maxDur = 0,
          compStyle, style = s.remove, amount = 0, applied = [];
      //Set and record the style before deleting the node after remove action
      for (name in style) {
        applied.push(name);
        elm.style[name] = style[name];
      }
      compStyle = getComputedStyle(elm);
      //Get all the attributes that need to be transitioned
      var props = compStyle['transition-property'].split(', ');
      //For counting transition attributes, here applied. length >= amount, because some attributes do not need transition
      for (; i < props.length; ++i) {
        if(applied.indexOf(props[i]) !== -1) amount++;
      }
      //When the transition effect is complete, the remove node calls the next remove process
      elm.addEventListener('transitionend', function(ev) {
        if (ev.target === elm) --amount;
        if (amount === 0) rm();
      });
    }
    
    module.exports = {create: updateStyle, update: updateStyle, destroy: applyDestroyStyle, remove: applyRemoveStyle};
    

Station 5 is

Having eaten all these big pieces of modules, there's a tasty dessert. Its main function is to determine whether it's an array type or a primitive type.

//Is toolkit for judging whether it is array or primitive type
module.exports = {
  array: Array.isArray,
  primitive: function(s) { return typeof s === 'string' || typeof s === 'number'; },
};

Half-way rest

Looking at so many source codes, I guess I'm tired. After all, it may be a little difficult to fully understand. Take a rest and digest it. The next chapter will see the biggest boss, snabbdom itself.

Topics: Javascript Attribute REST less