最近在學習vue2.0的源碼,剛開始看其vdom源碼,着實找不到方向,由於其在vdom的實現上還加
入了不少vue2.0自己的鉤子,加大了閱讀難度。因而看到第一行尤大說vue2.0的vdom是在snabbdom
的基礎上改過來的,而snabbdom只有不到300sloc,那不妨先從snabbdom入手,熟悉其中的原理,
再配合vue2.0的vdom看,效果可能更好。javascript
virtual-dom能夠看作一棵模擬了DOM樹的JavaScript樹,其主要是經過vnode,實現一個無
狀態的組件,當組件狀態發生更新時,而後觸發virtual-dom數據的變化,而後經過virtual-dom
和真實DOM的比對,再對真實dom更新。html
咱們知道,當咱們但願實現一個具備複雜狀態的界面時,若是咱們在每一個可能發生變化的組件上都綁定
事件,綁定字段數據,那麼很快因爲狀態太多,咱們須要維護的事件和字段將會愈來愈多,代碼也會
愈來愈複雜,因而,咱們想咱們可不能夠將視圖和狀態分開來,只要視圖發生變化,對應狀態也發生
變化,而後狀態變化,咱們再重繪整個視圖就行了。這樣的想法雖好,可是代價過高了,因而咱們又
想,能不能只更新狀態發生變化的視圖?因而virtual-dom應運而生,狀態變化先反饋到vdom上,
vdom在找到最小更新視圖,最後批量更新到真實DOM上,從而達到性能的提高。vue
除此以外,從移植性上看,virtual-dom還對真實dom作了一次抽象,這意味着virtual-dom對應
的能夠不是瀏覽器的dom,而是不一樣設備的組件,極大的方便了多平臺的使用。java
好了,說了這麼多,咱們先來看看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 |
首先,咱們從最簡單的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之間的比對
說完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中提供了對原生dom操做的一層抽象,這裏就再也不闡述了
modules中主要包含attributes,class,props,dataset,eventlistener,hero,style
這些模塊,其中attributes,class,props,dataset,eventlistener,style這些模塊是咱們
平常所須要的,也是snabbdom.bundle默認注入的也是這幾個,這裏就詳細介紹這幾個模塊
主要功能以下:
從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};
主要功能以下:
從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};
主要功能以下:
從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}
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 };
主要功能:
從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};
主要功能以下:
將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};
啃完modules這些大部頭,總算有個比較好吃的甜品了,他主要功能就是判斷是否爲array類型或者原始類型
//is工具庫,用於判斷是否爲array或者原始類型 module.exports = { array: Array.isArray, primitive: function(s) { return typeof s === 'string' || typeof s === 'number'; }, };
看了這麼多源碼,估計也累了吧,畢竟一下徹底理解可能有點難,不妨先休息一下,消化一下,下一章將會見到最大的boss——snabbdom自己!