vue2源碼學習開胃菜——snabbdom源碼學習(一)

前言

最近在學習vue2.0的源碼,剛開始看其vdom源碼,着實找不到方向,由於其在vdom的實現上還加
入了不少vue2.0自己的鉤子,加大了閱讀難度。因而看到第一行尤大說vue2.0的vdom是在snabbdom
的基礎上改過來的,而snabbdom只有不到300sloc,那不妨先從snabbdom入手,熟悉其中的原理,
再配合vue2.0的vdom看,效果可能更好。javascript

什麼是virtual-dom

virtual-dom能夠看作一棵模擬了DOM樹的JavaScript樹,其主要是經過vnode,實現一個無
狀態的組件,當組件狀態發生更新時,而後觸發virtual-dom數據的變化,而後經過virtual-dom
和真實DOM的比對,再對真實dom更新。html

爲何是virtual-dom

咱們知道,當咱們但願實現一個具備複雜狀態的界面時,若是咱們在每一個可能發生變化的組件上都綁定
事件,綁定字段數據,那麼很快因爲狀態太多,咱們須要維護的事件和字段將會愈來愈多,代碼也會
愈來愈複雜,因而,咱們想咱們可不能夠將視圖和狀態分開來,只要視圖發生變化,對應狀態也發生
變化,而後狀態變化,咱們再重繪整個視圖就行了。這樣的想法雖好,可是代價過高了,因而咱們又
想,能不能只更新狀態發生變化的視圖?因而virtual-dom應運而生,狀態變化先反饋到vdom上,
vdom在找到最小更新視圖,最後批量更新到真實DOM上,從而達到性能的提高。vue

除此以外,從移植性上看,virtual-dom還對真實dom作了一次抽象,這意味着virtual-dom對應
的能夠不是瀏覽器的dom,而是不一樣設備的組件,極大的方便了多平臺的使用。java

snabbdom目錄結構

好了,說了這麼多,咱們先來看看snabbdom吧,我看的是這個版本的snabbdom
(心塞,typescript學的不深,看最新版的有點吃力,因此選了ts版本前的一個版本)。好了咱們先
看看snabbdom的主要目錄結構。node

名稱 類型 解釋
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
對象的形式git

//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大屬性:github

  • sel 對應的是選擇器,如'div','div#a','div#a.b.c'的形式typescript

  • data 對應的是vnode綁定的數據,能夠有如下類型:attribute、props、eventlistner、
    class、dataset、hookapi

  • 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自己!

相關文章
相關標籤/搜索