经由历程对 Vue2.0 源码浏览,想写一写本身的明白,才能有限故从尤大佬2016.4.11第一次提交最先读,预备连续写:
个中包含本身的明白和源码的剖析,只管通俗易懂!由因而2.0的最早提交,所以和最新版本有许多差别、bug,后续将连续补充,敬请体谅!包含中文解释的Vue源码已上传…
最先
先说一下为何会有假造dom比较这一阶段,我们知道了Vue是数据驱动视图(数据的变化将引发视图的变化),但你发明某个数据转变时,视图是部份革新而不是悉数从新衬着,怎样精准的找到数据对应的视图并举行更新呢?那就须要拿到数据转变前后的dom构造,找到差别点并举行更新!
假造dom实质上是针对实在dom提炼出的简朴对象。就像一个简朴的div包含200多个属性,但真正须要的能够只要tagName
,所以对实在dom直接操纵将大大影响机能!
简化后的假造节点(vnode)大抵包含以下属性:
{
tag: 'div', // 标署名
data: {}, // 属性数据,包含class、style、event、props、attrs等
children: [], // 子节点数组,也是vnode构造
text: undefined, // 文本
elm: undefined, // 实在dom
key: undefined // 节点标识
}
假造dom的比较,就是找出新节点(vnode)和旧节点(oldVnode)之间的差别,然后对差别举行打补丁(patch)。大抵流程以下
悉数历程照样比较简朴的,新旧节点假如不相似,直接依据新节点建立dom;假如相似,先是对data比较,包含class、style、event、props、attrs等,有差别就挪用对应的update函数,然后是对子节点的比较,子节点的比较用到了diff算法,这应该是这篇文章的重点和难点吧。
值得注意的是,在Children Compare
历程当中,假如找到了相似的childVnode
,那它们将递归进入新的打补丁历程。
源码剖析
此次的源码剖析写简约一点,写太多发明本身都不情愿看 (┬_┬)
最先
先来看patch()
函数:
function patch (oldVnode, vnode) {
var elm, parent;
if (sameVnode(oldVnode, vnode)) {
// 相似就去打补丁(增编削)
patchVnode(oldVnode, vnode);
} else {
// 不相似就悉数掩盖
elm = oldVnode.elm;
parent = api.parentNode(elm);
createElm(vnode);
if (parent !== null) {
api.insertBefore(parent, vnode.elm, api.nextSibling(elm));
removeVnodes(parent, [oldVnode], 0, 0);
}
}
return vnode.elm;
}
patch()
函数吸收新旧vnode两个参数,传入的这两个参数有个很大的区分:oldVnode的elm
指向实在dom,而vnode的elm
为undefined…但经由patch()
要领后,vnode的elm
也将指向这个(更新过的)实在dom。
推断新旧vnode是不是相似的sameVnode()
要领很简朴,就是比较tag和key是不是一致。
function sameVnode (a, b) {
return a.key === b.key && a.tag === b.tag;
}
打补丁
关于新旧vnode不一致的处置惩罚要领很简朴,就是依据vnode建立实在dom,替代oldVnode中的elm
插进去DOM文档。
关于新旧vnode一致的处置惩罚,就是我们前面常常说到的打补丁了。详细什么是打补丁?看看patchVnode()
要领就知道了:
function patchVnode (oldVnode, vnode) {
// 新节点援用旧节点的dom
let elm = vnode.elm = oldVnode.elm;
const oldCh = oldVnode.children;
const ch = vnode.children;
// 挪用update钩子
if (vnode.data) {
updateAttrs(oldVnode, vnode);
updateClass(oldVnode, vnode);
updateEventListeners(oldVnode, vnode);
updateProps(oldVnode, vnode);
updateStyle(oldVnode, vnode);
}
// 推断是不是为文本节点
if (vnode.text == undefined) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) api.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
api.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
api.setTextContent(elm, vnode.text)
}
}
打补丁实在就是挪用种种updateXXX()
函数,更新实在dom的各个属性。每一个的update函数都相似,就拿updateAttrs()
举例看看:
function updateAttrs (oldVnode, vnode) {
let key, cur, old
const elm = vnode.elm
const oldAttrs = oldVnode.data.attrs || {}
const attrs = vnode.data.attrs || {}
// 更新/增加属性
for (key in attrs) {
cur = attrs[key]
old = oldAttrs[key]
if (old !== cur) {
if (booleanAttrsDict[key] && cur == null) {
elm.removeAttribute(key)
} else {
elm.setAttribute(key, cur)
}
}
}
// 删除新节点不存在的属性
for (key in oldAttrs) {
if (!(key in attrs)) {
elm.removeAttribute(key)
}
}
}
属性(Attribute
)的更新函数的大抵思绪就是:
- 遍历vnode属性,假如和oldVnode不一样就挪用
setAttribute()
修正; - 遍历oldVnode属性,假如不在vnode属性中就挪用
removeAttribute()
删除。
你会发明内里有个booleanAttrsDict[key]
的推断,是用于推断在不在布尔范例属性字典中。
[‘allowfullscreen’, ‘async’, ‘autofocus’, ‘autoplay’, ‘checked’, ‘compact’, ‘controls’, ‘declare’, ……]
eg:
<video autoplay></video>
,想封闭自动播放,须要移除该属性。
一切数据比较完后,就到子节点的比较了。先推断当前vnode是不是为文本节点,假如是文本节点就不必斟酌子节点的比较;如果元素节点,就须要分三种状况斟酌:
- 新旧节点都有children,那就进入子节点的比较(diff算法);
- 新节点有children,旧节点没有,那就轮回建立dom节点;
- 新节点没有children,旧节点有,那就轮回删除dom节点。
背面两种状况都比较简朴,我们直接对第一种状况,子节点的比较举行剖析。
diff算法
子节点比较这部份代码比较多,先说说道理背面再贴代码。先看一张子节点比较的图:
图中的oldCh
和newCh
离别示意新旧子节点数组,它们都有本身的头尾指针oldStartIdx
,oldEndIdx
,newStartIdx
,newEndIdx
,数组内里存储的是vnode,为了轻易明白就用a,b,c,d等替代,它们示意差别范例标签(div,span,p)的vnode对象。
子节点的比较实质上就是轮回举行头尾节点比较。轮回完毕的标志就是:旧子节点数组或新子节点数组遍历完,(即 oldStartIdx > oldEndIdx || newStartIdx > newEndIdx
)。也许看一下轮回流程:
- 第一步 头头比较。若相似,旧头新头指针后移(即
oldStartIdx++
&&newStartIdx++
),实在dom稳定,进入下一次轮回;不相似,进入第二步。 - 第二步 尾尾比较。若相似,旧尾新尾指针前移(即
oldEndIdx--
&&newEndIdx--
),实在dom稳定,进入下一次轮回;不相似,进入第三步。 - 第三步 头尾比较。若相似,旧头指针后移,新尾指针前移(即
oldStartIdx++
&&newEndIdx--
),未确认dom序列中的头移到尾,进入下一次轮回;不相似,进入第四步。 - 第四步 尾头比较。若相似,旧尾指针前移,新头指针后移(即
oldEndIdx--
&&newStartIdx++
),未确认dom序列中的尾移到头,进入下一次轮回;不相似,进入第五步。 - 第五步 若节点有key且在旧子节点数组中找到sameVnode(tag和key都一致),则将其dom挪动到当前实在dom序列的头部,新头指针后移(即
newStartIdx++
);不然,vnode对应的dom(vnode[newStartIdx].elm
)插进去当前实在dom序列的头部,新头指针后移(即newStartIdx++
)。
先看看没有key的状况,放个动图看得更清晰些!
置信看完图片有更好的明白到diff算法的精华,悉数历程照样比较简朴的。上图中一共进入了6次轮回,触及了每一种状况,逐一叙说一下:
- 第一次是头头相似(都是
a
),dom不转变,新旧头指针均后移。a
节点确认后,实在dom序列为:a,b,c,d,e,f
,未确认dom序列为:b,c,d,e,f
; - 第二次是尾尾相似(都是
f
),dom不转变,新旧尾指针均前移。f
节点确认后,实在dom序列为:a,b,c,d,e,f
,未确认dom序列为:b,c,d,e
; - 第三次是头尾相似(都是
b
),当前盈余实在dom序列中的头移到尾,旧头指针后移,新尾指针前移。b
节点确认后,实在dom序列为:a,c,d,e,b,f
,未确认dom序列为:c,d,e
; - 第四次是尾头相似(都是
e
),当前盈余实在dom序列中的尾移到头,旧尾指针前移,新头指针后移。e
节点确认后,实在dom序列为:a,e,c,d,b,f
,未确认dom序列为:c,d
; - 第五次是均不相似,直接插进去到未确认dom序列头部。
g
节点插进去后,实在dom序列为:a,e,g,c,d,b,f
,未确认dom序列为:c,d
; - 第六次是均不相似,直接插进去到未确认dom序列头部。
h
节点插进去后,实在dom序列为:a,e,g,h,c,d,b,f
,未确认dom序列为:c,d
;
但完毕轮回后,有两种状况须要斟酌:
- 新的字节点数组(newCh)被遍历完(
newStartIdx > newEndIdx
)。那就须要把过剩的旧dom(oldStartIdx -> oldEndIdx
)都删除,上述例子中就是c,d
; - 新的字节点数组(oldCh)被遍历完(
oldStartIdx > oldEndIdx
)。那就须要把过剩的新dom(newStartIdx -> newEndIdx
)都增加。
上面说了这么多都是没有key的状况,说增加了:key
能够优化v-for
的机能,究竟是怎么回事呢?由于v-for
大部份状况下天生的都是雷同tag
的标签,假如没有key标识,那末相当于每次头头比较都能胜利。你想一想假如你往v-for
绑定的数组头部push数据,那末悉数dom将悉数革新一遍(假如数组每项内容都不一样),那加了key
会有什么协助呢?这边援用一张图:
有key
的状况,实在就是多了一步婚配查找的历程。也就是上面轮回流程中的第五步,会尝试去旧子节点数组中找到与当前新子节点相似的节点,削减dom的操纵!
有兴致的能够看看代码:
function updateChildren (parentElm, oldCh, newCh) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, elmToMove, before
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // 未定义示意被挪动过
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) { // 头头相似
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) { // 尾尾相似
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // 头尾相似
patchVnode(oldStartVnode, newEndVnode)
api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // 尾头相似
patchVnode(oldEndVnode, newStartVnode)
api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 依据旧子节点的key,天生map映照
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 在旧子节点数组中,找到和newStartVnode相似节点的下标
idxInOld = oldKeyToIdx[newStartVnode.key]
if (isUndef(idxInOld)) {
// 没有key,建立并插进去dom
api.insertBefore(parentElm, createElm(newStartVnode), oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
// 有key,找到对应dom ,挪动该dom并在oldCh中置为undefined
elmToMove = oldCh[idxInOld]
patchVnode(elmToMove, newStartVnode)
oldCh[idxInOld] = undefined
api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
}
}
}
// 轮回完毕时,删除/增加过剩dom
if (oldStartIdx > oldEndIdx) {
before = isUndef(newCh[newEndIdx+1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
末了
愿望看完这篇对假造dom的比较会有肯定的相识!假如有什么毛病记得悄然告诉我啊哈哈。
文笔照样不好,愿望大家能明白o(︶︿︶)o
4篇文章写了两个月……真是信服本身的执行力!但发明写博客彷佛确切挺费时的(┬_┬),不过今后肯定会常常写,先两周一篇?😄