怎么感覺遙遙無期了呀~這個源碼,跑不完了。
這個系列寫的不好,僅作為一個記錄,善始善終,反正也沒人看,寫着玩吧!
接着上一節的cbs,這個對象在初始化應該只會調用create模塊數組方法,簡單回顧一下到哪了。
// line-4944 function invokeCreateHooks(vnode, insertedVnodeQueue) { // 遍歷調用數組方法 // emptyNode => 空虛擬DOM // vnode => 當前掛載的虛擬DOM for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) { cbs.create[i$1](emptyNode, vnode); } i = vnode.data.hook; // Reuse variable if (isDef(i)) { if (isDef(i.create)) { i.create(emptyNode, vnode); } if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); } } }
后面的暫時不去看,依次執行cbs.create中的方法:
一、updateAttrs
前面是對vnode的attrs進行更新,__ob__屬性代表該對象被觀測,可能會變動,后面是對舊vnode屬性的移除。
// line-5456 // 因為是初始化 // 此時oldVnode是空的vnode function updateAttrs(oldVnode, vnode) { // 老、新vnode都沒屬性就返回 if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) { return } var key, cur, old; var elm = vnode.elm; var oldAttrs = oldVnode.data.attrs || {}; var attrs = vnode.data.attrs || {}; // __ob__屬性代表可能變動 if (isDef(attrs.__ob__)) { attrs = vnode.data.attrs = extend({}, attrs); } // attrs => {id:app} for (key in attrs) { cur = attrs[key]; old = oldAttrs[key]; // 更改屬性 if (old !== cur) { setAttr(elm, key, cur); } } // IE9 if (isIE9 && attrs.value !== oldAttrs.value) { setAttr(elm, 'value', attrs.value); } // 移除舊vnode的屬性 for (key in oldAttrs) { if (isUndef(attrs[key])) { if (isXlink(key)) { elm.removeAttributeNS(xlinkNS, getXlinkProp(key)); } else if (!isEnumeratedAttr(key)) { elm.removeAttribute(key); } } } }
這里主要是最后遍歷新vnode的屬性,調用setAttr進行設置。
// line-5492 // el => div // key => id // value => app function setAttr(el, key, value) { if (isBooleanAttr(key)) { // 處理無值屬性 如:disabled if (isFalsyAttrValue(value)) { el.removeAttribute(key); } else { el.setAttribute(key, key); } } else if (isEnumeratedAttr(key)) { el.setAttribute(key, isFalsyAttrValue(value) || value === 'false' ? 'false' : 'true'); } else if (isXlink(key)) { if (isFalsyAttrValue(value)) { el.removeAttributeNS(xlinkNS, getXlinkProp(key)); } else { el.setAttributeNS(xlinkNS, key, value); } } else { if (isFalsyAttrValue(value)) { el.removeAttribute(key); } else { el.setAttribute(key, value); } } } var isBooleanAttr = makeMap( 'allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,' + 'default,defaultchecked,defaultmuted,defaultselected,defer,disabled,' + 'enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,' + 'muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,' + 'required,reversed,scoped,seamless,selected,sortable,translate,' + 'truespeed,typemustmatch,visible' ); var isEnumeratedAttr = makeMap('contenteditable,draggable,spellcheck'); var isXlink = function(name) { // 以xlink:開頭的字符串 return name.charAt(5) === ':' && name.slice(0, 5) === 'xlink' }; var isFalsyAttrValue = function(val) { return val == null || val === false };
函數看似判斷很多很復雜,其實很簡單,只是根據屬性的類別做處理。本例中,直接會跳到最后的setAttribute,直接調用原生方法設置屬性,結果就是給div設置了id:app屬性。
因為oldVnode是空的,所以沒有屬性可以移除。
二、updateClass
這個從名字看就明白了,就是類名更換而已。
// line-5525 function updateClass(oldVnode, vnode) { var el = vnode.elm; var data = vnode.data; var oldData = oldVnode.data; // 是否有定義class屬性 if ( isUndef(data.staticClass) && isUndef(data.class) && ( isUndef(oldData) || ( isUndef(oldData.staticClass) && isUndef(oldData.class) ) ) ) { return } var cls = genClassForVnode(vnode); // handle transition classes var transitionClass = el._transitionClasses; if (isDef(transitionClass)) { cls = concat(cls, stringifyClass(transitionClass)); } // set the class if (cls !== el._prevClass) { el.setAttribute('class', cls); el._prevClass = cls; } }
因為沒有class,所有會直接返回了。
三、updateDOMListeners
這個是更新事件監聽
// line-6156 function updateDOMListeners(oldVnode, vnode) { if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) { return } var on = vnode.data.on || {}; var oldOn = oldVnode.data.on || {}; target$1 = vnode.elm; normalizeEvents(on); updateListeners(on, oldOn, add$1, remove$2, vnode.context); }
很明顯,本例中也沒有事件,所以跳過
四、updateDOMProps
這個是更新組件的props值
// line-6174 function updateDOMProps(oldVnode, vnode) { if (isUndef(oldVnode.data.domProps) && isUndef(vnode.data.domProps)) { return } // code }
因為代碼太長又不執行,所以簡單跳過。
五、updateStyle
更新style屬性~
// line-6364 function updateStyle(oldVnode, vnode) { var data = vnode.data; var oldData = oldVnode.data; if (isUndef(data.staticStyle) && isUndef(data.style) && isUndef(oldData.staticStyle) && isUndef(oldData.style)) { return } // 跳過... }
六、_enter
這個enter函數有點長,但是直接return,所以具體代碼就不貼出來了。
// line-6934 function _enter(_, vnode) { // 第一參數沒有用 if (vnode.data.show !== true) { enter(vnode); } }
七、create
// line-4674 create: function create(_, vnode) { registerRef(vnode); }
// line-4688 function registerRef(vnode, isRemoval) { var key = vnode.data.ref; if (!key) { return } // return了 }
這個也return了。
八、updateDirectives
從名字來看是更新組件沒錯了!
// line-5346 function updateDirectives(oldVnode, vnode) { if (oldVnode.data.directives || vnode.data.directives) { _update(oldVnode, vnode); } }
先判斷新舊節點是否有directives屬性,沒有直接跳過。
到這里,8個初始化方法全部調用完畢,函數返回createElm:
// line-4802 function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested) { // 獲取屬性 // ... if (isDef(tag)) { // warning... vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode); setScope(vnode); /* istanbul ignore if */ { // 從這里出來 createChildren(vnode, children, insertedVnodeQueue); if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue); } // 下一個 insert(parentElm, vnode.elm, refElm); } if ("development" !== 'production' && data && data.pre) { inPre--; } } else if (isTrue(vnode.isComment)) { vnode.elm = nodeOps.createComment(vnode.text); insert(parentElm, vnode.elm, refElm); } else { vnode.elm = nodeOps.createTextNode(vnode.text); insert(parentElm, vnode.elm, refElm); } }
經過createChildren后,vnode的elm屬性,也就是原生DOM會被添加各種屬性,然后進入insert函數。
insert函數接受3個參數,父節點、當前節點、相鄰節點,本例中對應body、div、空白文本節點。
// line-4915 // parent => body // elm => vnode.elm => div // ref => #text function insert(parent, elm, ref) { if (isDef(parent)) { if (isDef(ref)) { if (ref.parentNode === parent) { nodeOps.insertBefore(parent, elm, ref); } } else { nodeOps.appendChild(parent, elm); } } }
這個函數前面也見過,簡單說一下,三個參數,父、當前、相鄰。
1、如果都存在,調用insertBefore將當前插入相鄰前。
2、如果沒有相鄰節點,直接調用appendChild把節點插入父。
這個調用完,頁面終於驚喜的出現了變化!
patch函數一階段完成,頁面已經插入的對應的DOM節點。
返回后,進入第二階段,移除那個{{message}}:
// line-5250 function patch(oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) { // isUndef(vnode)... var isInitialPatch = false; var insertedVnodeQueue = []; if (isUndef(oldVnode)) { // ... } else { var isRealElement = isDef(oldVnode.nodeType); if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly); } else { if (isRealElement) { // SSR // hydrating oldVnode = emptyNodeAt(oldVnode); } var oldElm = oldVnode.elm; var parentElm$1 = nodeOps.parentNode(oldElm); // createElm => 生成原生DOM節點並插入頁面 if (isDef(vnode.parent)) { // ... } // go! if (isDef(parentElm$1)) { removeVnodes(parentElm$1, [oldVnode], 0, 0); } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode); } } } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch); return vnode.elm }
接下里會進入removeVnodes函數對模板樣式進行移除,至於另外分支是對舊vnode移除,不屬於頁面初始化階段。
// line-4995 // parentElm => body // vnodes => 掛載虛擬DOM集合 // startIdx => endIdx =>1 function removeVnodes(parentElm, vnodes, startIdx, endIdx) { for (; startIdx <= endIdx; ++startIdx) { var ch = vnodes[startIdx]; if (isDef(ch)) { if (isDef(ch.tag)) { removeAndInvokeRemoveHook(ch); invokeDestroyHook(ch); } else { // Text node removeNode(ch.elm); } } } }
函數很簡潔,一個分支處理DOM節點,一個分支處理文本節點。本例中進入第一個分支,將移除掛載的DOM節點。
有兩個方法處理移除DOM節點:
1、removeAndInvokeRemoveHook
// line-5009 function removeAndInvokeRemoveHook(vnode, rm) { if (isDef(rm) || isDef(vnode.data)) { var i; var listeners = cbs.remove.length + 1; if (isDef(rm)) { rm.listeners += listeners; } else { // 返回一個函數 rm = createRmCb(vnode.elm, listeners); } // recursively invoke hooks on child component root node if (isDef(i = vnode.componentInstance) && isDef(i = i._vnode) && isDef(i.data)) { removeAndInvokeRemoveHook(i, rm); } // 會直接返回 for (i = 0; i < cbs.remove.length; ++i) { cbs.remove[i](vnode, rm); } if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) { i(vnode, rm); } else { // 最后跳這 rm(); } } else { removeNode(vnode.elm); } } // line-4782 function createRmCb(childElm, listeners) { // 這是rm function remove$$1() { if (--remove$$1.listeners === 0) { removeNode(childElm); } } remove$$1.listeners = listeners; return remove$$1 } // line-6934 remove: function remove$$1(vnode, rm) { /* istanbul ignore else */ if (vnode.data.show !== true) { // 由於沒有data屬性 直接返回vm() leave(vnode, rm); } else { rm(); } }
這個方法非常繞,處理的東西很多,然而本例只有一個DOM節點需要移除,所以跳過很多地方,直接執行rm()函數。
即rm => removeNode() => parentNode.removeChild()。
最后成功移除原有的div。
但是沒完,移除DOM節點后,還需要處理vnode,於是進行第二步invokeDestroyHook:
// line-4981 function invokeDestroyHook(vnode) { var i, j; var data = vnode.data; if (isDef(data)) { // destroy鈎子函數? if (isDef(i = data.hook) && isDef(i = i.destroy)) { i(vnode); } // 主函數 for (i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](vnode); } } // 遞歸處理子節點 if (isDef(i = vnode.children)) { for (j = 0; j < vnode.children.length; ++j) { invokeDestroyHook(vnode.children[j]); } } }
這里與上面的cbs.create類似,也是遍歷調用對應的數組方法,此處為destroy。
1、destroy
這里沒有ref屬性,直接返回了。
// line-4683 destroy: function destroy(vnode) { registerRef(vnode, true); } function registerRef(vnode, isRemoval) { var key = vnode.data.ref; if (!key) { return } // more }
2、unbindDirectives
直接返回了。
// line-5341 destroy: function unbindDirectives(vnode) { updateDirectives(vnode, emptyNode); } function updateDirectives(oldVnode, vnode) { if (oldVnode.data.directives || vnode.data.directives) { _update(oldVnode, vnode); } }
后面也沒有什么,直接返回到patch函數進行最后一步。
// line-5341 function patch(oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) { // 插入移除節點 invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch); return vnode.elm }
這里的invokeInsertHook處理鈎子函數的插入,由於不存在,所以也直接返回了。
接下來會一直返回到mountComponent方法:
// line-2374 function mountComponent(vm, el, hydrating) { vm.$el = el; // warning callHook(vm, 'beforeMount'); var updateComponent; if ("development" !== 'production' && config.performance && mark) { // warning } else { updateComponent = function() { // 渲染DOM vm._update(vm._render(), hydrating); }; } vm._watcher = new Watcher(vm, updateComponent, noop); hydrating = false; // 調用mounted鈎子函數 if (vm.$vnode == null) { vm._isMounted = true; callHook(vm, 'mounted'); } return vm }
最后調用鈎子函數mounted,返回vue實例。
到此,流程全部跑完了。
啊~不容易。

