经过对 Vue2.0 源码阅读,想写一写本身的理解,能力有限故从尤大佬2016.4.11第一次提交开始读,准备陆续写:javascript
其中包含本身的理解和源码的分析,尽可能通俗易懂!因为是2.0的最先提交,因此和最新版本有不少差别、bug,后续将陆续补充,敬请谅解!包含中文注释的Vue源码已上传...vue
先说一下为何会有虚拟dom比较这一阶段,咱们知道了Vue是数据驱动视图(数据的变化将引发视图的变化),但你发现某个数据改变时,视图是局部刷新而不是整个从新渲染,如何精准的找到数据对应的视图并进行更新呢?那就须要拿到数据改变先后的dom结构,找到差别点并进行更新!java
虚拟dom实质上是针对真实dom提炼出的简单对象。就像一个简单的div包含200多个属性,但真正须要的可能只有tagName
,因此对真实dom直接操做将大大影响性能!node
简化后的虚拟节点(vnode)大体包含如下属性:git
{ tag: 'div', // 标签名 data: {}, // 属性数据,包括class、style、event、props、attrs等 children: [], // 子节点数组,也是vnode结构 text: undefined, // 文本 elm: undefined, // 真实dom key: undefined // 节点标识 }
虚拟dom的比较,就是找出新节点(vnode)和旧节点(oldVnode)之间的差别,而后对差别进行打补丁(patch)。大体流程以下github
整个过程仍是比较简单的,新旧节点若是不类似,直接根据新节点建立dom;若是类似,先是对data比较,包括class、style、event、props、attrs等,有不一样就调用对应的update函数,而后是对子节点的比较,子节点的比较用到了diff算法,这应该是这篇文章的重点和难点吧。算法
值得注意的是,在Children Compare
过程当中,若是找到了类似的childVnode
,那它们将递归进入新的打补丁过程。segmentfault
此次的源码解析写简洁一点,写太多发现本身都不肯意看 (┬_┬)api
先来看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
)的更新函数的大体思路就是:
setAttribute()
修改;removeAttribute()
删除。你会发现里面有个booleanAttrsDict[key]
的判断,是用于判断在不在布尔类型属性字典中。
['allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 'compact', 'controls', 'declare', ......]eg:
<video autoplay></video>
,想关闭自动播放,须要移除该属性。
全部数据比较完后,就到子节点的比较了。先判断当前vnode是否为文本节点,若是是文本节点就不用考虑子节点的比较;如果元素节点,就须要分三种状况考虑:
后面两种状况都比较简单,咱们直接对第一种状况,子节点的比较进行分析。
子节点比较这部分代码比较多,先说说原理后面再贴代码。先看一张子节点比较的图:
图中的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序列中的尾移到头,进入下一次循环;不类似,进入第五步。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
;g
节点插入后,真实dom序列为:a,e,g,c,d,b,f
,未确认dom序列为:c,d
;h
节点插入后,真实dom序列为:a,e,g,h,c,d,b,f
,未确认dom序列为:c,d
;但结束循环后,有两种状况须要考虑:
newStartIdx > newEndIdx
)。那就须要把多余的旧dom(oldStartIdx -> oldEndIdx
)都删除,上述例子中就是c,d
;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篇文章写了两个月......真是佩服本身的执行力!但发现写博客好像确实挺费时的(┬_┬),不过之后必定会常常写,先两周一篇?😄