Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Vue更新 #59

@andyChenAn

Description

@andyChenAn

Vue更新

当我们修改Vue中的一个数据时,就会触发Vue组件的更新,而这个更新是异步的(通过Promise来实现)。更新过程主要就是对比新节点和旧节点过程,找出需要更新的地方进行更新。对比的方式是同层级的节点进行对比,得到变化,然后更新变化的视图。

image

在对比的过程中,如果两个节点被认为是同一个节点,那么会进行深度的比较,得到最小差异,否则直接删除旧的DOM节点,创建新的DOM节点。

sameVNode方法

该方式是用来比较两个节点是否为同一个节点

/*
    判断两个节点是否为同一个节点需要满足以下条件:
    1、节点的key相同
    2、节点的tag(当前节点的标签名)相同
    3、节点的isComment(是否为注释节点)相同
    4、节点的data是否同时有定义或者同时未定义
    5、如果两个节点是input,那么要判断input的type是否相同
*/
function sameVnode (a, b) {
    return (
        a.key === b.key && (
            (
                a.tag === b.tag &&
                a.isComment === b.isComment &&
                isDef(a.data) === isDef(b.data) &&
                sameInputType(a, b)
            ) || (
                isTrue(a.isAsyncPlaceholder) &&
                a.asyncFactory === b.asyncFactory &&
                isUndef(b.asyncFactory.error)
            )
        )
    )
}

/*
    判断当标签是<input>的时候,type的类型是否相同
*/
function sameInputType (a, b) {
    if (a.tag !== 'input') { return true }
    var i;
    // 获取两个input标签的type值
    var typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type;
    var typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type;
    return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}

我们可以看到,只有当两个节点的key,tag,isComment相同,并且两个节点的data都同时有定义或者同时未定义,并且如果节点的标签名是input,那么还要判断input的type是否相同,当满足这些条件时,才能证明两个节点相同。

当两个节点相同时,就可以调用patchVnode方法,去对比同层级中的节点。

patchVnode方法主要做了以下几件事情:

  • 1、如果新节点和旧节点都是静态节点,并且新旧节点的key相同,并且新节点是旧节点克隆出来的或者是新节点标记了v-once属性,那么就只要替换ele和componentInstance即可。
  • 2、如果新节点和旧节点都存在子节点,那么就调用updateChildren方法,对子节点进行diff操作。
  • 3、如果旧节点没有子节点而新节点有子节点,那么先清空旧节点的文本内容,然后为新节点插入子节点。
  • 4、如果新节点没有子节点而旧节点有子节点,那么就移除旧节点的所有子节点。
  • 5、如果新节点和旧节点都没有子节点,但是旧节点有文本内容,那么会清空旧节点的文本内容。
  • 6、如果新节点和旧节点都没有子节点,但是新节点和旧节点都有文本内容,那么会替换文本内容。

updateChildren方法

该方法是diff算法的核心,首先在新旧两个Vnode树的左右两侧都有一个变量标记,即:
oldStartVnode,oldEndVnode,newStartVnode,newEndVnode。这四个变量分别代表新旧节点树的第一个节点和最后一个节点。

索引与VNode节点的对应关系: oldStartIdx => oldStartVnode oldEndIdx => oldEndVnode newStartIdx => newStartVnode newEndIdx => newEndVnode

从代码中,我们可以看到oldStartVnode,oldEndVnode和newStartVnode,newEndVnode有4种比较方式。oldStartVnode和newStartVnode,oldStartVnode和newEndVnode,oldEndVnode和newStartVnode,oldEndVnode和newEndVnode。

如果oldStartVnode和newStartVnode或者oldEndVnode和newStartVnode满足sameVnode,那么就直接调用patchVnode方法,对比这两个节点。

如果oldStartVnode和newEndVnode满足sameVnode,那么表示oldStartVnode跑到了oldEndVnode后面去了,在调用patchVnode方法,对比这两个节点之后,还需要调用DOM的insertBefore方法将真实的DOM节点移动到oldEndVnode的后面。

如果oldEndVnode和newStartVnode满足sameVnode,那么表示oldEndVnode跑到了oldStartVnode前面去了,在调用patchVnode方法对比这两个节点之后,还需要调用DOM的insertBefore方法将真实的DOM节点移动到oldStartVnode的前面。

如果都不是上面的情况,那么会调用createKeyToOldIdx方法,得到一张关于旧节点的key的映射表,映射表的结构为:{key1 : index1 , key2 : index2 , ...},然后我们在映射表中可以找到与newStartVnode节点的key一致的旧的Vnode节点(我们这里称它为:vnodeToMove节点),如果找到了,并且新节点和旧节点满足sameVnode,那么就调用patchVnode方法,对比这两个节点,并且将vnodeToMove移动到oldStartVnode对应的真实DOM的前面。

当然也有可能在旧节点的映射表中没有找到与newStartVnode一致的key,那么就会调用createElm创建一个新的DOM节点。

以上就是while循环里面需要做的事情,剩下我们还需要处理多余或者不够的真实DOM节点。

当while循环结束时,oldStartIdx > oldEndIdx,这就表示旧的节点已经遍历完了,但是新节点还没有,说明新节点实际上是要比旧节点多,那么就将剩下的节点插入到真实的DOM节点中,调用addVnodes方法,批量调用createElm方法将这些剩余的节点插入到DOM中。

当while循环结束时,newStartIdx > newEndIdx,表示新的节点已经遍历完了,但是旧节点还有剩余,说明存在多余的DOM节点,那么我们就需要就多余的DOM节点删除,这时候调用removeVnodes方法,将多余的DOM节点删除。

updateAttrs方法

该方法主要是更新节点的属性

function updateAttrs (oldVnode, vnode) {
    var opts = vnode.componentOptions;
    if (isDef(opts) && opts.Ctor.options.inheritAttrs === false) {
        return
    }
    // 如果旧的节点和新的节点都不存在任何属性,那么就直接返回,不用执行后面的更新操作
    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 || {};
    // clone observed objects, as the user probably wants to mutate it
    if (isDef(attrs.__ob__)) {
        attrs = vnode.data.attrs = extend({}, attrs);
    }
    // 遍历新节点的所有属性,如果新旧节点的属性值不同(有可能是重新修改属性,有可能是删除属性),那么就进行更新
    // 节点属性的更新主要是调用setAttribute和removeAttribute方法
    for (key in attrs) {
        cur = attrs[key];
        old = oldAttrs[key];
        if (old !== cur) {
            setAttr(elm, key, cur);
        }
    }
    // #4391: in IE9, setting type can reset value for input[type=radio]
    // #6666: IE/Edge forces progress value down to 1 before setting a max
    /* istanbul ignore if */
    // 兼容IE浏览器
    if ((isIE || isEdge) && attrs.value !== oldAttrs.value) {
        setAttr(elm, 'value', attrs.value);
    }
    // 遍历旧节点的所有属性
    for (key in oldAttrs) {
        if (isUndef(attrs[key])) {
            if (isXlink(key)) {
                elm.removeAttributeNS(xlinkNS, getXlinkProp(key));
            } else if (!isEnumeratedAttr(key)) {
                elm.removeAttribute(key);
            }
        }
    }
}

updateClass方法

该方法用来更新节点的class属性

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
    }
    
    // 为新节点生成新的class
    var cls = genClassForVnode(vnode);

    // 将transition的class合并到新节点的class上
    var transitionClass = el._transitionClasses;
    if (isDef(transitionClass)) {
        cls = concat(cls, stringifyClass(transitionClass));
    }
    
    // 设置class
    if (cls !== el._prevClass) {
        el.setAttribute('class', cls);
        // 当设置完之后,将当前的class重新赋值为prevClass
        el._prevClass = cls;
    }
}

updateDOMListeners方法

该方法是用来更新节点的事件监听器

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, createOnceHandler$1, vnode.context);
    target$1 = undefined;
}

updateDOMProps方法

该方法主要是更新DOM元素属性

function updateDOMProps (oldVnode, vnode) {
    // 如果新节点和旧节点不存在DOM属性,那么就不需要更新,直接返回
    if (isUndef(oldVnode.data.domProps) && isUndef(vnode.data.domProps)) {
        return
    }
    var key, cur;
    var elm = vnode.elm;
    // 旧节点的DOM属性
    var oldProps = oldVnode.data.domProps || {};
    // 新节点的DOM属性
    var props = vnode.data.domProps || {};
    if (isDef(props.__ob__)) {
        props = vnode.data.domProps = extend({}, props);
    }
    // 遍历旧节点的DOM属性,如果新节点中不存在旧节点的DOM属性,那么就直接删除
    for (key in oldProps) {
        if (!(key in props)) {
            elm[key] = '';
        }
    }
    // 遍历新节点的DOM属性
    for (key in props) {
        // 新节点的DOM属性值
        cur = props[key];
        // 如果新节点存在textContext或者innerHTML属性,那么忽略新节点的子节点
        if (key === 'textContent' || key === 'innerHTML') {
            if (vnode.children) { vnode.children.length = 0; }
            if (cur === oldProps[key]) { continue }
            if (elm.childNodes.length === 1) {
                elm.removeChild(elm.childNodes[0]);
            }
        }
        // 如果是value属性
        if (key === 'value' && elm.tagName !== 'PROGRESS') {
            // store value as _value as well since
            // non-string values will be stringified
            elm._value = cur;
            // avoid resetting cursor position when value is the same
            var strCur = isUndef(cur) ? '' : String(cur);
            if (shouldUpdateValue(elm, strCur)) {
                elm.value = strCur;
            }
        } else if (key === 'innerHTML' && isSVG(elm.tagName) && isUndef(elm.innerHTML)) {
            // IE doesn't support innerHTML for SVG elements
            svgContainer = svgContainer || document.createElement('div');
            svgContainer.innerHTML = "<svg>" + cur + "</svg>";
            var svg = svgContainer.firstChild;
            while (elm.firstChild) {
                elm.removeChild(elm.firstChild);
            }
            while (svg.firstChild) {
                elm.appendChild(svg.firstChild);
            }
        } else if (
            cur !== oldProps[key]
        ) {
            // some property updates can throw
            // e.g. `value` on <progress> w/ non-finite value
            try {
                elm[key] = cur;
            } catch (e) {}
        }
    }
}

updateStyle方法

该方法主要用来更新节点的style

function updateStyle (oldVnode, vnode) {
    var data = vnode.data;
    var oldData = oldVnode.data;
    // 如果新旧节点不存在style,那么就直接返回,不用更新
    if (isUndef(data.staticStyle) && isUndef(data.style) &&
        isUndef(oldData.staticStyle) && isUndef(oldData.style)
    ) {
        return
    }
    
    var cur, name;
    var el = vnode.elm;
    // 旧节点的静态的style,比如: style="color : #fff";
    var oldStaticStyle = oldData.staticStyle;
    // 旧节点绑定的style,比如::style
    var oldStyleBinding = oldData.normalizedStyle || oldData.style || {};
    
    // if static style exists, stylebinding already merged into it when doing normalizeStyleData
    var oldStyle = oldStaticStyle || oldStyleBinding;
    
    var style = normalizeStyleBinding(vnode.data.style) || {};
    
    // store normalized style under a different key for next diff
    // make sure to clone it if it's reactive, since the user likely wants
    // to mutate it.
    vnode.data.normalizedStyle = isDef(style.__ob__)
    ? extend({}, style)
    : style;
    
    var newStyle = getStyle(vnode, true);
    // 设置新节点的style
    for (name in oldStyle) {
        if (isUndef(newStyle[name])) {
            setProp(el, name, '');
        }
    }
    for (name in newStyle) {
        cur = newStyle[name];
        if (cur !== oldStyle[name]) {
            // ie9 setting to null has no effect, must use empty string
            setProp(el, name, cur == null ? '' : cur);
        }
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions