.12-Vue源码之patch(2)

快完事咯!  

 

  简单看了下patch函数,虽然不长,但是实际上很长很长,慢慢来吧,

  首先来个总览:

    // line-5250
    // oldVnode => 原生DOM节点
    // vnode => 虚拟DOM
    // hydrating => undefined
    // removeOnly => false
    // 后面两个undefined
    function patch(oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
        // 判断是否存在虚拟DOM
        if (isUndef(vnode)) {
            if (isDef(oldVnode)) {
                invokeDestroyHook(oldVnode);
            }
            return
        }

        var isInitialPatch = false;
        var insertedVnodeQueue = [];

        if (isUndef(oldVnode)) {
            // empty mount (likely as component), create new root element
            // 空挂载或作为组件
            isInitialPatch = true;
            createElm(vnode, insertedVnodeQueue, parentElm, refElm);
        } else {
            var isRealElement = isDef(oldVnode.nodeType);
            if (!isRealElement && sameVnode(oldVnode, vnode)) {
                // patch existing root node
                patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly);
            } else {
                if (isRealElement) {
                    // 判断是否SSR
                    if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
                        oldVnode.removeAttribute(SSR_ATTR);
                        hydrating = true;
                    }
                    // hydrating这个到底是啥????
                    if (isTrue(hydrating)) {
                        if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
                            invokeInsertHook(vnode, insertedVnodeQueue, true);
                            return oldVnode
                        } else {
                            warn(
                                'The client-side rendered virtual DOM tree is not matching ' +
                                'server-rendered content. This is likely caused by incorrect ' +
                                'HTML markup, for example nesting block-level elements inside ' +
                                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                                'full client-side render.'
                            );
                        }
                    }
                    // 两个都不是 新建空虚拟DOM
                    oldVnode = emptyNodeAt(oldVnode);
                }
                // 此处oldElm => div | parentElm$1 => body
                var oldElm = oldVnode.elm;
                var parentElm$1 = nodeOps.parentNode(oldElm);
                createElm(
                    vnode,
                    insertedVnodeQueue,
                    // BUG:当节点处于leaving transition状态中不插入
                    oldElm._leaveCb ? null : parentElm$1,
                    // 获取相邻节点
                    nodeOps.nextSibling(oldElm)
                );

                if (isDef(vnode.parent)) {
                    // component root element replaced.
                    // update parent placeholder node element, recursively
                    var ancestor = vnode.parent;
                    while (ancestor) {
                        ancestor.elm = vnode.elm;
                        ancestor = ancestor.parent;
                    }
                    if (isPatchable(vnode)) {
                        for (var i = 0; i < cbs.create.length; ++i) {
                            cbs.create[i](emptyNode, vnode.parent);
                        }
                    }
                }

                if (isDef(parentElm$1)) {
                    removeVnodes(parentElm$1, [oldVnode], 0, 0);
                } else if (isDef(oldVnode.tag)) {
                    invokeDestroyHook(oldVnode);
                }
            }
        }

        invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
        return vnode.elm
    }

  函数总共有6个形参,其中有3个为undefined,内部总共主要是条件判断,按照顺序跑下来。

 

一、

  由于不是SSR,也没有hydrating这个参数,所以会直接创建一个空的虚拟DOM作为oldVnode,如下:

    // line-4778
    function emptyNodeAt(elm) {
        return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
    }

  

二、

  接下来到了createElm方法,代码如下:

    // line-4802
    // vnode => 之前的虚拟DOM
    // insertedVnodeQueue => [] => 空数组
    // parentElm => body => 父元素
    // refElm => 空白文本 => 节点的相邻元素
    // nested => undefined
    function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested) {
        vnode.isRootInsert = !nested; // for transition enter check
        if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
            return
        }

        var data = vnode.data;
        var children = vnode.children;
        var tag = vnode.tag;
        if (isDef(tag)) {
            // error检测
            {
                if (data && data.pre) {
                    inPre++;
                }
                // 这个错误经常见!
                if (!inPre &&
                    !vnode.ns &&
                    !(config.ignoredElements.length && config.ignoredElements.indexOf(tag) > -1) &&
                    config.isUnknownElement(tag)
                ) {
                    warn(
                        'Unknown custom element: <' + tag + '> - did you ' +
                        'register the component correctly? For recursive components, ' +
                        'make sure to provide the "name" option.',
                        vnode.context
                    );
                }
            }
            // ns???
            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);
        }
    }

  函数进入就会调用createComponent进行判断,这个函数暂时看不太懂,一个分支都没有进,直接返回了undefined:

    // line-4855
    function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
        // 获取属性
        var i = vnode.data;
        if (isDef(i)) {
            var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
            if (isDef(i = i.hook) && isDef(i = i.init)) {
                i(vnode, false /* hydrating */ , parentElm, refElm);
            }
            // after calling the init hook, if the vnode is a child component
            // it should've created a child instance and mounted it. the child
            // component also has set the placeholder vnode's elm.
            // in that case we can just return the element and be done.
            if (isDef(vnode.componentInstance)) {
                initComponent(vnode, insertedVnodeQueue);
                if (isTrue(isReactivated)) {
                    reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
                }
                return true
            }
        }
    }

  下一步是跳到createElement函数,nodeOps中的方法都是自封装的DOM操作方法,超级简单。

    // line-4599
    function createElement$1(tagName, vnode) {
        var elm = document.createElement(tagName);
        if (tagName !== 'select') {
            return elm
        }
        // 对select做特殊处理
        // false or null will remove the attribute but undefined will not
        if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
            elm.setAttribute('multiple', 'multiple');
        }
        return elm
    }

  可以看,这个方法也就是创建一个DOM节点并返回,对select会做特殊处理,于是vnode.elm也就是个普通的DOM节点。

 

  接下来是setScope函数(),该函数目的是给根节点添加一个特殊的ID,避免css样式影响其他模块。

  这个东西我就熟悉啦,触发形式是这样:

  渲染结果是这样:

  这样可以保证所有的样式都是独立的,就算名字一样,后面添加的随机字符串可以保证唯一。

    // line-4958
    function setScope(vnode) {
        var i;
        var ancestor = vnode;
        while (ancestor) {
            if (isDef(i = ancestor.context) && isDef(i = i.$options._scopeId)) {
                nodeOps.setAttribute(vnode.elm, i, '');
            }
            ancestor = ancestor.parent;
        }
        // for slot content they should also get the scopeId from the host instance.
        if (isDef(i = activeInstance) &&
            i !== vnode.context &&
            isDef(i = i.$options._scopeId)) {
            nodeOps.setAttribute(vnode.elm, i, '');
        }
    }

  函数判断虚拟DOM是否存在_scopeId,然后对每一个节点添加对应的Id,这里没有,所以就会跳过去。

  

  接下来调用createChildren方法,从名字也能看出来是对子节点的处理。实际上,也是对子节点或者纯文本进行递归处理:

    // line-4927
    function createChildren(vnode, children, insertedVnodeQueue) {
        // 存在复杂子节点
        if (Array.isArray(children)) {
            for (var i = 0; i < children.length; ++i) {
                createElm(children[i], insertedVnodeQueue, vnode.elm, null, true);
            }
        }
        // 子节点为纯文本
        else if (isPrimitive(vnode.text)) {
            nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(vnode.text));
        }
    }

  第一条分支处理DOM子节点,直接递归。本例子节点只是一个纯文本:

  因此,重新跳入createElm方法后,会直接进入第三个分支,即:

    // line-4802
    function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested) {
        // 属性获取
        // ...
        if (isDef(tag)) {
            // 处理节点类型
        } else if (isTrue(vnode.isComment)) {
            // 处理注释
        } else {
            vnode.elm = nodeOps.createTextNode(vnode.text);
            insert(parentElm, vnode.elm, refElm);
        }
    }

  这个分支只有两条语句,第一条会创建一个文本节点给elm属性,第二条负责插入。

  第一个DOM操作只是简单调用document.createTextNode方法,直接看第二个函数吧:

    // line-4915
    // parent => div => 父节点
    // elm => #text => 本节点
    // ref => null => 相邻节点
    function insert(parent, elm, ref) {
        if (isDef(parent)) {
            if (isDef(ref)) {
                if (ref.parentNode === parent) {
                    // 
                    nodeOps.insertBefore(parent, elm, ref);
                }
            } 
            // parent.appendChild(elm)
            else {
                nodeOps.appendChild(parent, elm);
            }
        }
    }

  第二个也是DOM操作!好吧,比较简单,看看就懂了。

  

  子节点处理完后,接下来处理本节点的属性,相关函数为invokeCreateHooks:

    // line-4944
    // vnode => 虚拟DOM
    // insertedVnodeQueue => []
    function invokeCreateHooks(vnode, insertedVnodeQueue) {
        // cbs为工具对象
        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对象,在外部函数运行最开始有进行初始化,如下:

    // line-4762
    function createPatchFunction(backend) {
        var i, j;
        var cbs = {};
        // line-6968
        // var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });
        var modules = backend.modules;
        var nodeOps = backend.nodeOps;

        for (i = 0; i < hooks.length; ++i) {
            cbs[hooks[i]] = [];
            for (j = 0; j < modules.length; ++j) {
                if (isDef(modules[j][hooks[i]])) {
                    cbs[hooks[i]].push(modules[j][hooks[i]]);
                }
            }
        }

        // tools fn

        // return fn
    }

  这个对象来源于多个对象的拼接,包含大量虚拟DOM的操作方法,内容如图:

  从名字可见,该对象整合了创建、摧毁、移除、更新等功能库,此处仅用create来创建,内部函数如图:

  invokeCreateHooks函数一开始遍会遍历此数组,依次传参执行,参数为空vnode与当前的vnode。

 

  这部分比较长,下次再讲。

  

posted @ 2017-07-20 11:22  书生小龙  阅读(1277)  评论(0编辑  收藏  举报