vue2源码进修开胃菜——snabbdom源码进修(一)

媒介

近来在进修vue2.0的源码,刚最先看其vdom源码,实在找不到方向,因为其在vdom的完成上还加
入了许多vue2.0自身的钩子,加大了浏览难度。因而看到第一行尤大说vue2.0的vdom是在snabbdom
的基础上改过来的,而snabbdom只需不到300sloc,那无妨先从snabbdom入手,熟习个中的道理,
再合营vue2.0的vdom看,结果能够更好。

什么是virtual-dom

virtual-dom能够看作一棵模拟了DOM树的JavaScript树,其重如果经由历程vnode,完成一个无
状况的组件,当组件状况发作更新时,然后触发virtual-dom数据的变化,然后经由历程virtual-dom
和实在DOM的比对,再对实在dom更新。

为何是virtual-dom

我们晓得,当我们愿望完成一个具有庞杂状况的界面时,假如我们在每一个能够发作变化的组件上都绑定
事宜,绑定字段数据,那末很快因为状况太多,我们须要保护的事宜和字段将会愈来愈多,代码也会
愈来愈庞杂,因而,我们想我们可不能够将视图和状况分开来,只需视图发作变化,对应状况也发作
变化,然后状况变化,我们再重绘全部视图就好了。如许的主意虽好,然则价值太高了,因而我们又
想,能不能只更新状况发作变化的视图?因而virtual-dom应运而生,状况变化先反应到vdom上,
vdom在找到最小更新视图,末了批量更新到实在DOM上,从而到达机能的提拔。

除此之外,从移植性上看,virtual-dom还对实在dom做了一次笼统,这意味着virtual-dom对应
的能够不是浏览器的dom,而是差别装备的组件,极大的方便了多平台的运用。

snabbdom目次组织

好了,说了这么多,我们先来看看snabbdom吧,我看的是这个版本的snabbdom
(心塞,typescript学的不深,看最新版的有点费劲,所以选了ts版本前的一个版本)。好了我们先
看看snabbdom的重要目次组织。

称号范例诠释
dist文件夹内里包含了snabddom打包后的文件
examples文件夹内里包含了运用snabbdom的例子
helpers文件夹包含svg操纵须要的东西
modules文件夹包含了对attribute,props,class,dataset,eventlistner,style,hero的操纵
perf文件夹机能测试
test文件夹测试
h文件把状况转化为vnode
htmldomapi文件原生dom操纵的笼统
is文件推断范例
snabbdom.bundle文件snabbdom自身依靠打包
snabbdom文件snabbdom 中心,包含diff,patch等操纵
thunk文件snabbdom下的thunk功用完成
vnode文件组织vnode

snabbdom源码之旅

第一站 vnode

起首,我们从最简朴的vnode最先入手,vnode完成的功用异常简朴,就是讲输入的数据转化为vnode
对象的情势

    //VNode函数,用于将输入转化成VNode
    /**
     *
     * @param sel    选择器
     * @param data    绑定的数据
     * @param children    子节点数组
     * @param text    当前text节点内容
     * @param elm    对实在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重要有5大属性:

  • sel 对应的是选择器,如’div’,’div#a’,’div#a.b.c’的情势

  • data 对应的是vnode绑定的数据,能够有以下范例:attribute、props、eventlistner、
    class、dataset、hook

  • children 子元素数组

  • text 文本,代表该节点中的文本内容

  • elm 内里存储着对对应的实在dom element的援用

  • key 用于差别vnode之间的比对

第二站 h

说完vnode,就到h了,h也是一个包装函数,重如果在vnode上再做一层包装,完成功用以下

  • 假如是svg,则为其增加定名空间

  • 将children中的text包装成vnode情势

    var VNode = require ( './vnode' );
    var is = require ( './is' );
    //增加定名空间(svg才须要)
    function addNS ( data, children, sel ) {
        data.ns = 'http://www.w3.org/2000/svg';
    //假如选择器
        if ( sel !== 'foreignObject' && children !== undefined ) {
            //递归为子节点增加定名空间
            for (var i = 0; i < children.length; ++i) {
                addNS ( children[ i ].data, children[ i ].children, children[ i ].sel );
            }
        }
    }
    //将VNode衬着为VDOM
    /**
     *
     * @param sel 选择器
     * @param b    数据
     * @param c    子节点
     * @returns {{sel, data, children, text, elm, key}}
     */
    module.exports = function h ( sel, b, c ) {
        var data = {}, children, text, i;
        //假如存在子节点
        if ( c !== undefined ) {
            //那末h的第二项就是data
            data = b;
            //假如c是数组,那末存在子element节点
            if ( is.array ( c ) ) {
                children = c;
            }
            //不然为子text节点
            else if ( is.primitive ( c ) ) {
                text = c;
            }
        }
        //假如c不存在,只存在b,那末申明须要衬着的vdom不存在data部份,只存在子节点部份
        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) {
                //假如子节点数组中,存在节点是原始范例,申明该节点是text节点,因而我们将它衬着为一个只包含text的VNode
                if ( is.primitive ( children[ i ] ) ) children[ i ] = VNode ( undefined, undefined, undefined, children[ i ] );
            }
        }
        //假如是svg,须要为节点增加定名空间
        if ( sel[ 0 ] === 's' && sel[ 1 ] === 'v' && sel[ 2 ] === 'g' ) {
            addNS ( data, children, sel );
        }
        return VNode ( sel, data, children, text, undefined );
    };

第三站 htmldomapi

htmldomapi中供应了对原生dom操纵的一层笼统,这里就不再论述了

第四站 modules

modules中重要包含attributes,class,props,dataset,eventlistener,hero,style
这些模块,个中attributes,class,props,dataset,eventlistener,style这些模块是我们
一样平常所须要的,也是snabbdom.bundle默许注入的也是这几个,这里就细致引见这几个模块

attributes

重要功用以下:

  • 从elm的属性中删除vnode中不存在的属性(包含那些boolean类属性,假如新vnode设置为false,一样删除)

  • 假如oldvnode与vnode用同名属性,则在elm上更新对应属性值

  • 假如vnode有新属性,则增加到elm中

  • 假如存在定名空间,则用setAttributeNS设置

    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);
    
    //建立属性字典,默许为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 (!oldAttrs && !attrs) return;
      oldAttrs = oldAttrs || {};
      attrs = attrs || {};
    
      // update modified attributes, add new attributes
      //更新改变了的属性,增加新的属性
      for (key in attrs) {
        cur = attrs[key];
        old = oldAttrs[key];
        //假如旧的属性和新的属性差别
        if (old !== cur) {
        //假如是boolean类属性,当vnode设置为falsy value时,直接删除,而不是更新值
          if(!cur && booleanAttrsDict[key])
            elm.removeAttribute(key);
          else {
            //不然更新属性值或许增加属性
            //假如存在定名空间
            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
      //删除不在新节点属性中的旧节点的属性
      for (key in oldAttrs) {
        if (!(key in attrs)) {
          elm.removeAttribute(key);
        }
      }
    }
    
    module.exports = {create: updateAttrs, update: updateAttrs};

class

重要功用以下:

  • 从elm中删除vnode中不存在的或许值为false的类

  • 将vnode中新的class增加到elm上去

    function updateClass(oldVnode, vnode) {
      var cur, name, elm = vnode.elm,
          oldClass = oldVnode.data.class,
          klass = vnode.data.class;
      //假如旧节点和新节点都没有class,直接返回
      if (!oldClass && !klass) return;
      oldClass = oldClass || {};
      klass = klass || {};
      //从旧节点中删除新节点不存在的类
      for (name in oldClass) {
        if (!klass[name]) {
          elm.classList.remove(name);
        }
      }
      //假如新节点中对应旧节点的类设置为false,则删除该类,假如新设置为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

重要功用以下:

  • 从elm中删除vnode不存在的属性集合的属性

  • 更新属性集合的属性值

    function updateDataset(oldVnode, vnode) {
      var elm = vnode.elm,
        oldDataset = oldVnode.data.dataset,
        dataset = vnode.data.dataset,
        key
    
      //假如新旧节点都没数据集,则直接返回
      if (!oldDataset && !dataset) return;
      oldDataset = oldDataset || {};
      dataset = dataset || {};
     //删除旧节点中在新节点不存在的数据集
      for (key in oldDataset) {
        if (!dataset[key]) {
          delete elm.dataset[key];
        }
      }
      //更新数据集
      for (key in dataset) {
        if (oldDataset[key] !== dataset[key]) {
          elm.dataset[key] = dataset[key];
        }
      }
    }
    
    module.exports = {create: updateDataset, update: updateDataset}

eventlistener

snabbdom中对事宜处置惩罚做了一层包装,实在DOM的事宜触发的是对vnode的操纵,重要门路是:

createListner => 返回handler作事宜监听生成器 =>handler上绑定vnode =>将handler作实在DOM的事宜处置惩罚器
实在DOM事宜触发后 => handler取得实在DOM的事宜对象 => 将实在DOM事宜对象传入handleEvent => handleEvent找到
对应的vnode事宜处置惩罚器,然后挪用这个处置惩罚器从而修正vnode

//snabbdom中对事宜处置惩罚做了一层包装,实在DOM的事宜触发的是对vnode的操纵
//重要门路是
// createListner => 返回handler作事宜监听生成器 =>handler上绑定vnode =>将handler作实在DOM的事宜处置惩罚器
//实在DOM事宜触发后 => handler取得实在DOM的事宜对象 => 将实在DOM事宜对象传入handleEvent => handleEvent找到
//对应的vnode事宜处置惩罚器,然后挪用这个处置惩罚器从而修正vnode

//对vnode举行事宜处置惩罚
function invokeHandler ( handler, vnode, event ) {
    if ( typeof handler === "function" ) {
        // call function handler
        //将事宜处置惩罚器在vnode上挪用
        handler.call ( vnode, event, vnode );
    }
    //存在事宜绑定数据或许存在多事宜处置惩罚器
    else if ( typeof handler === "object" ) {

        //申明只需一个事宜处置惩罚器
        if ( typeof handler[ 0 ] === "function" ) {
            //假如绑定数据只需一个,则直接将数据用call的体式格局挪用,进步机能
            //形如on:{click:[handler,1]}
            if ( handler.length === 2 ) {
                handler[ 0 ].call ( vnode, handler[ 1 ], event, vnode );
            }
            //假如存在多个绑定数据,则要转化为数组,用apply的体式格局挪用,而apply机能比call差
            //形如:on:{click:[handler,1,2,3]}
            else {
                var args = handler.slice ( 1 );
                args.push ( event );
                args.push ( vnode );
                handler[ 0 ].apply ( vnode, args );
            }
        } else {
            //假如存在多个雷同事宜的差别处置惩罚器,则递归挪用
            //如on:{click:[[handeler1,1],[handler,2]]}
            for (var i = 0; i < handler.length; i++) {
                invokeHandler ( handler[ i ] );
            }
        }
    }
}

/**
 *
 * @param event 实在dom的事宜对象
 * @param vnode
 */
function handleEvent ( event, vnode ) {
    var name = event.type,
        on = vnode.data.on;

    // 假如找到对应的vnode事宜处置惩罚器,则挪用
    if ( on && on[ name ] ) {
        invokeHandler ( on[ name ], vnode, event );
    }
}
//事宜监听器生成器,用于处置惩罚实在DOM事宜
function createListener () {
    return function handler ( event ) {
        handleEvent ( event, handler.vnode );
    }
}
//更新事宜监听
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 ( oldOn === on ) {
        return;
    }

    // remove existing listeners which no longer used
    //假如新节点上没有事宜监听,则将旧节点上的事宜监听都删除
    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 {
            //删除旧节点中新节点不存在的事宜监听
            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
        //假如oldvnode上已经有listener,则vnode直接复用,不然则新建事宜处置惩罚器
        var listener = vnode.listener = oldVnode.listener || createListener ();
        // update vnode for listener
        //在事宜处置惩罚器上绑定vnode
        listener.vnode = vnode;

        // if element changed or added we add all needed listeners unconditionally‘
        //假如oldvnode上没有事宜处置惩罚器
        if ( !oldOn ) {
            for (name in on) {
                // add listener if element was changed or new listeners added
                //直接将vnode上的事宜处置惩罚器增加到elm上
                elm.addEventListener ( name, listener, false );
            }
        } else {
            for (name in on) {
                // add listener if new listener added
                //不然增加oldvnode上没有的事宜处置惩罚器
                if ( !oldOn[ name ] ) {
                    elm.addEventListener ( name, listener, false );
                }
            }
        }
    }
}

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

props

重要功用:

  • 从elm上删除vnode中不存在的属性

  • 更新elm上的属性

    function updateProps(oldVnode, vnode) {
      var key, cur, old, elm = vnode.elm,
          oldProps = oldVnode.data.props, props = vnode.data.props;
     //假如新旧节点都不存在属性,则直接返回
      if (!oldProps && !props) return;
      oldProps = oldProps || {};
      props = props || {};
      //删除旧节点中新节点没有的属性
      for (key in oldProps) {
        if (!props[key]) {
          delete elm[key];
        }
      }
      //更新属性
      for (key in props) {
        cur = props[key];
        old = oldProps[key];
        //假如新旧节点属性差别,且对照的属性不是value或许elm上对应属性和新属性也差别,那末就须要更新
        if (old !== cur && (key !== 'value' || elm[key] !== cur)) {
          elm[key] = cur;
        }
      }
    }
    
    module.exports = {create: updateProps, update: updateProps};
    
    

style

重要功用以下:

  • 将elm上存在于oldvnode中但不存在于vnode中不存在的style置空

  • 假如vnode.style中的delayed与oldvnode的差别,则更新delayed的属性值,并在下一帧将elm的style设置为该值,从而完成动画过渡结果

  • 非delayed和remove的style直接更新

  • vnode被destroy时,直接将对应style更新为vnode.data.style.destory的值

  • vnode被reomve时,假如style.remove不存在,直接挪用全局remove钩子进入下一个remove历程
    假如style.remove存在,那末我们就须要设置remove动画过渡结果,比及过渡结果完毕以后,才挪用
    下一个remove历程

    //假如存在requestAnimationFrame,则直接运用,以优化机能,不然用setTimeout
    var raf = (typeof window !== 'undefined' && window.requestAnimationFrame) || setTimeout;
    var nextFrame = function(fn) { raf(function() { raf(fn); }); };
    
    //经由历程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;
      //假如oldvnode和vnode都没有style,直接返回
      if (!oldStyle && !style) return;
      oldStyle = oldStyle || {};
      style = style || {};
      var oldHasDel = 'delayed' in oldStyle;
      //遍历oldvnode的style
      for (name in oldStyle) {
        //假如vnode中无该style,则置空
        if (!style[name]) {
          elm.style[name] = '';
        }
      }
      //假如vnode的style中有delayed且与oldvnode中的差别,则在下一帧设置delayed的参数
      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);
            }
          }
        }
        //假如不是delayed和remove的style,且差别于oldvnode的值,则直接设置新值
        else if (name !== 'remove' && cur !== oldStyle[name]) {
          elm.style[name] = cur;
        }
      }
    }
    
    //设置节点被destory时的style
    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];
      }
    }
    //删除结果,当我们删除一个元素时,先回挪用删除过分结果,过渡完才会将节点remove
    function applyRemoveStyle(vnode, rm) {
      var s = vnode.data.style;
      //假如没有style或没有style.remove
      if (!s || !s.remove) {
        //直接挪用rm,即实际上是挪用全局的remove钩子
        rm();
        return;
      }
      var name, elm = vnode.elm, idx, i = 0, maxDur = 0,
          compStyle, style = s.remove, amount = 0, applied = [];
      //设置并纪录remove行动后删除节点前的款式
      for (name in style) {
        applied.push(name);
        elm.style[name] = style[name];
      }
      compStyle = getComputedStyle(elm);
      //拿到一切须要过渡的属性
      var props = compStyle['transition-property'].split(', ');
      //对过渡属性计数,这里applied.length >=amount,因为有些属性是不须要过渡的
      for (; i < props.length; ++i) {
        if(applied.indexOf(props[i]) !== -1) amount++;
      }
      //当过渡结果的完成后,才remove节点,挪用下一个remove历程
      elm.addEventListener('transitionend', function(ev) {
        if (ev.target === elm) --amount;
        if (amount === 0) rm();
      });
    }
    
    module.exports = {create: updateStyle, update: updateStyle, destroy: applyDestroyStyle, remove: applyRemoveStyle};
    

第五站 is

啃完modules这些大部头,总算有个比较好吃的甜品了,他重要功用就是推断是不是为array范例或许原始范例

//is东西库,用于推断是不是为array或许原始范例
module.exports = {
  array: Array.isArray,
  primitive: function(s) { return typeof s === 'string' || typeof s === 'number'; },
};

半途歇息

看了这么多源码,预计也累了吧,毕竟一下完整明白能够有点难,无妨先歇息一下,消化一下,下一章将会见到最大的boss——snabbdom自身!

    原文作者:steinns
    原文地址: https://segmentfault.com/a/1190000009017324
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞