Vue Diff原理分析

Diff的出现,是为了减少更新量,找到最小差异部分DOM,只更新差异部分DOM就好了,这样消耗就会小一些。Vue2.x中的diff只会对新旧节点中父节点是相同节点的那一层子节点进行比较。只有当新旧节点是相同节点的时候,才会去比较他们各自的子节点。
新旧VNode进行比较的过程中,不会对这两棵VNode树进行修改,而是以比较结果直接对真实DOM进行修改。比如说在比较VNode的过程中,发现某个节点需要移动,此时不会直接移动VNode中的节点,而是直接移动DOM。

vm._update()比较新旧VNode树,比较完成后,更新DOM

Vue.prototype._update = function(vnode) {
    var vm = this;
    var prevEl = vm.$el;
    // 这个属性保存的是当前的VNode树,当页面生成了新的VNode树之后,新的会直接替换这个旧的
    var prevVnode = vm._vnode;

    // 如果不存在旧节点,直接全部创建
    if (!prevVnode) {
        vm.$el = vm.__patch__(
            vm.$el,
            vnode,
            vm.$options._parentElm,
            vm.$options._refElm
        )
    // 存在旧节点,尽量找到最小的差异部分,然后进行更新
    } else {
        vm.$el = vm.__patch__(
            prevVnode,
            vnode
        )
    }
}
// __patch__是通过createPatchFunction来创建的
var patch = createPatchFunction();
Vue.prototype.__patch__ = patch;

insert这个函数的主要作用是插入节点,但是插入也会分两种情况

  1. 没有参考兄弟节点,直接插入父节点的子节点的末尾
  2. 有参考的兄弟节点,则插在兄弟节点的前面
function insert(parent, elm, ref) {
    if (parent) {
        // 如果有参考的兄弟节点,则插在兄弟节点的前面
        if (ref) {
            if (ref.parentNode === parent) {
                parent.insertBefore(elm, ref)
            }
        // 如果没有参考兄弟节点,直接插入父节点的子节点的末尾
        } else {
            parent.appendChild(elm)
        }
    }
}

createElm这个函数的作用就是创建节点,创建完节点之后会调用insert去插入节点

function createElm(vnode, parentElm, refElm) {
    var children = vnode.children;
    var tag = vnode.tag;
    // 普通节点
    if (tag) {
        vnode.elm = document.createElement(tag);
        // 处理子节点,用遍历递归的方法逐个处理
        createChildren(vnode, children);
        insert(parentElm, vnode.elm, refElm);
    // 文本节点
    } else {
        vnode.elm = document.createTextNode(vnode.text);
        insert(parentElm, vnode.elm, refElm);
    }
}
function createChildren(vnode, children) {    
    // 如果子节点是数组,则遍历执行createElm逐个进行处理
    if (Array.isArray(children)) {      
        for (var i = 0; i < children.length; ++i) {
            createElm(children[i], vnode.elm, null);
        }
    }
    // 如果子节点text属性有数据,表明是文本节点,则直接创建文本节点,然后插入到父节点中
    else if (        
        typeof vnode.text=== 'string' ||
        typeof vnode.text=== 'number' ||
        typeof vnode.text=== 'boolean'
    ) {
        vnode.elm.appendChild(
            document.createTextNode(vnode.text)
        )
    }
}

createKeyToOldIdx接受一个children数组,生成key与index索引对应的map表

function createKeyToOldIdx(children, beginIdx, endIdx) {
    var i, key;
    var map = {};
    for (i = beginIdx; i <= endIdx; i++) {
        key = children[i].key;
        if (key) {
            map[key] = i;
        }
    }
    return map;
}

/*
    [{ tag: 'div', key: 'key_1' },
    { tag: 'strong', key: 'key_2' },
    { tag: 'span', key: 'key_4'}]
    比如以上旧节点的数组,可以通过createKeyToOldIdx创建一个map
    {
        "key_1":0,
        "key_2":1,
        "key_4":2
    }
*/

这个方法的作用就是判断某个新的VNode是否在旧的VNode数组中,并且拿到它的位置。然后拿到新VNode的key,然后去map表中匹配,是否有相应的节点,有的话,就返回节点的位置。

sameVnode用来判断两个节点是否相同

function sameVnode(a, b) {
  // 判断主要依据key, tag, data
  return a.key === b.key &&
    a.tag === b.tag &&
    !!a.data === !!b.data &&
    sameInputType(a, b);
}
function sameInputType(a, b) {
  if (a.tag !== 'input') return true;
  var i;
  var types = [
    'text','number','password',
    'search','email','tel','url'
  ];
  var typeA = (i = a.data) && (i = i.attrs) && i.type;
  var typeB = (i = b.data) && (i = i.attrs) && i.type;

  // input类型一样,或者都属于基本input类型
  return (
    typeA === typeB ||
    types.indexOf(typeA) > -1 &&
    types.indexOf(typeB) > -1
  );
}

分析createPatchFunction

function createPatchFunction() {
    return function patch(oldVnode, vnode, parentElm, refElm) {
        // 没有旧节点,直接生成新节点
        if (!oldVnode) {
            createElm(vnode, parentElm, refElm);
        } else {
            // 如果是一样的VNode
            if (sameVnode(oldVnode, vnode)) {
                // 比较存在的根节点
                // patchVnode其中一个作用就是比较子节点
                patchVnode(oldVnode, vnode);
            } else {
                // 直接替换存在的元素
                var oldElm = oldVnode.elm;
                var _parentElm = oldElm.parentNode;
                // 创建新节点
                createElm(vnode, _parentElm, oldElm.nextSibling);
                // 销毁旧节点
                if (_parentElm) {
                    removeVnodes([oldVnode], 0, 0);
                }
            }
        }
        return vnode.elm;
    }
}

进一步来看patchVnode

function patchVnode(oldVnode, vnode) {    
    if (oldVnode === vnode) return
    var elm = vnode.elm = oldVnode.elm;    
    var oldCh = oldVnode.children;    
    var ch = vnode.children;    
    // Vnode有子节点,则处理比较更新子节点
    if (!vnode.text) {        
        // 存在 oldCh 和 ch 时
        if (oldCh && ch) {            
            if (oldCh !== ch) 
                // 核心!!!
                updateChildren(elm, oldCh, ch);
        }
        // 只有新节点,直接创建
        else if (ch) {
            if (oldVnode.text) elm.textContent = '';
            for (var i = 0; i <= ch.length - 1; ++i) {
                createElm(
                    ch[i],elm, null
                );
            }
        } 
        // 只有旧节点,直接删除
        else if (oldCh) {
            for (var i = 0; i<= oldCh.length - 1; ++i) {
                oldCh[i].parentNode.removeChild(el);
            }
        }
        else if (oldVnode.text) {
            elm.textContent = '';
        }
    }
    // 如果Vnode是文本节点,则更新文本
    else if (oldVnode.text !== vnode.text) {
        elm.textContent = vnode.text;
    }
}

接下来分析核心Diff思想

// 对比新旧节点,逐个循环遍历比较
function updateChildren(parentElm, oldCh, newCh) {
    // 旧节点的两个索引
    var oldStartIdx = 0;
    var oldEndIdx = oldCh.length - 1;
    var oldStartVnode = oldCh[0];
    var oldEndVnode = oldCh[oldEndIdx];
    // 新节点的两个索引
    var newStartIdx = 0;
    var newEndIdx = newCh.length - 1;
    var newStartVnode = newCh[0];
    var newEndVnode = newCh[newEndIdx];
    var oldKeyToIdx, idxInOld, vnodeToMove, refElm;
    // 不断地更新 OldIndex 和 OldVnode ,newIndex 和 newVnode
    while (
        oldStartIdx <= oldEndIdx && 
        newStartIdx <= newEndIdx
    ) {
        if (!oldStartVnode) {
            oldStartVnode = oldCh[++oldStartIdx];
        }
        else if (!oldEndVnode) {
            oldEndVnode = oldCh[--oldEndIdx];
        }
        //  旧头 和新头 比较,如果是相同的,这种情况最简单
        // 直接新旧索引同时向后移动
        // 并且使用patchVnode去进一步比较这两个节点的子节点
        else if (sameVnode(oldStartVnode, newStartVnode)) {
            patchVnode(oldStartVnode, newStartVnode);
            oldStartVnode = oldCh[++oldStartIdx];
            newStartVnode = newCh[++newStartIdx];
        }
        //  旧尾 和新尾 比较,这种情况和上一种基本一样
        // 新旧索引直接向前移动
        // 并且使用patchVnode去进一步比较这两个节点的子节点
        else if (sameVnode(oldEndVnode, newEndVnode)) {
            patchVnode(oldEndVnode, newEndVnode);
            oldEndVnode = oldCh[--oldEndIdx];
            newEndVnode = newCh[--newEndIdx];
        }
        // 旧头 和 新尾 比较,这种情况只能移动DOM
        else if (sameVnode(oldStartVnode, newEndVnode)) {
            patchVnode(oldStartVnode, newEndVnode);
            // oldStartVnode 放到 oldEndVnode 后面,还要找到 oldEndValue 后面的节点
            parentElm.insertBefore(
                oldStartVnode.elm, 
                oldEndVnode.elm.nextSibling
            );
            oldStartVnode = oldCh[++oldStartIdx];
            newEndVnode = newCh[--newEndIdx];
        }
        //  旧尾 和新头 比较,这种情况也只能移动DOM
        else if (sameVnode(oldEndVnode, newStartVnode)) {
            patchVnode(oldEndVnode, newStartVnode);            
            // oldEndVnode 放到 oldStartVnode 前面
            parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
            oldEndVnode = oldCh[--oldEndIdx];
            newStartVnode = newCh[++newStartIdx];
        }        
        // 单个新子节点 在 旧子节点数组中 查找位置
        else {
            // oldKeyToIdx 是一个 把 Vnode 的 key 和 index 转换的 map
            if (!oldKeyToIdx) {
                oldKeyToIdx = createKeyToOldIdx(
                    oldCh, oldStartIdx, oldEndIdx
                );
            }            
            // 使用 newStartVnode 去 OldMap 中寻找 相同节点,默认key存在
            idxInOld = oldKeyToIdx[newStartVnode.key]
            //  新孩子中,存在一个新节点,老节点中没有,需要新建 
            if (!idxInOld) {
                //  把  newStartVnode 插入 oldStartVnode 的前面
                createElm(
                    newStartVnode, 
                    parentElm,
                    oldStartVnode.elm
                );
            }
            else {                
                //  找到 oldCh 中 和 newStartVnode 一样的节点
                vnodeToMove = oldCh[idxInOld];     
                if (sameVnode(vnodeToMove, newStartVnode)) {
                    patchVnode(vnodeToMove, newStartVnode);  
                    // 删除这个 index
                    oldCh[idxInOld] = undefined;
                    // 把 vnodeToMove 移动到  oldStartVnode 前面
                    parentElm.insertBefore(
                        vnodeToMove.elm, 
                        oldStartVnode.elm
                    );
                }
                // 只能创建一个新节点插入到 parentElm 的子节点中
                else {
                    // same key but different element. treat as new element
                    createElm(
                        newStartVnode, 
                        parentElm, 
                        oldStartVnode.elm
                    );
                }
            }            
            // 这个新子节点更新完毕,更新 newStartIdx,开始比较下一个
            newStartVnode = newCh[++newStartIdx];
        }
    }    
    // 处理剩下的节点
    // 旧节点遍历玩了,但新节点还有剩余,则批量创建新节点
    if (oldStartIdx > oldEndIdx) {
        var newEnd = newCh[newEndIdx + 1]
        refElm = newEnd ? newEnd.elm :null;        
        for (; newStartIdx <= newEndIdx; ++newStartIdx) {
            createElm(
               newCh[newStartIdx], parentElm, refElm
            );
        }
    }    
    // 说明新节点比对完了,老节点可能还有,需要删除剩余的老节点
    else if (newStartIdx > newEndIdx) {        
        for (; oldStartIdx<=oldEndIdx; ++oldStartIdx) {
            oldCh[oldStartIdx].parentNode.removeChild(el);
        }
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值