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
这个函数的主要作用是插入节点,但是插入也会分两种情况
- 没有参考兄弟节点,直接插入父节点的子节点的末尾
- 有参考的兄弟节点,则插在兄弟节点的前面
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);
}
}
}