.13-Vue源碼之patch(3)(終於完事)


怎么感覺遙遙無期了呀~這個源碼,跑不完了。

這個系列寫的不好,僅作為一個記錄,善始善終,反正也沒人看,寫着玩吧!

 

  接着上一節的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實例。

 

  到此,流程全部跑完了。

  啊~不容易。

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM