Vue中采纳了 虚构DOM + Diff算法
缩小了对DOM的操作次数,大大提高了性能,那么咱们明天就来具体的讲一下Vue中这一部分的实现逻辑,心愿能够帮忙还不了解这部分的小伙伴了解这一部分,纯手打,心愿各位小伙伴点个赞反对一下!
首先咱们要明确的是,vnode代表本次批改后新生成的虚构节点,oldVnode代表目前实在DOM构造所对应的虚构节点。所以咱们更新是以vnode为基准,通过oldVnode的构造去操作实在DOM,vnode和oldVnode都不会被扭转,被扭转的只有实在DOM构造。
patch
Vue在首次渲染和数据更新的时候,会去调用 _update
办法,而 _update
办法的外围就是去调用 vm.__patch__
办法,那么咱们就先从patch
办法动手。patch办法中的逻辑比较复杂,这里咱们只看次要流程局部,大抵流程如下图:
function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) { if (isUndef(oldVnode)) { /*oldVnode未定义的时候,创立一个新的节点*/ isInitialPatch = true createElm(vnode, insertedVnodeQueue, parentElm, refElm) } else { /*标记旧的VNode是否有nodeType*/ const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { /*是同一个节点的时候间接批改现有的节点*/ patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) } else { if (isRealElement) { oldVnode = emptyNodeAt(oldVnode) } // 插入到视图中旧节点的旁边 const oldElm = oldVnode.elm const parentElm = nodeOps.parentNode(oldElm) createElm( vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ) if (isDef(parentElm)) { /*移除老节点*/ removeVnodes(parentElm, [oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { /*调用destroy钩子*/ invokeDestroyHook(oldVnode) } } } return vnode.elm}
这里有个 isRealElement
用来标识oldVnode是否有 nodeType
,有nodeType
就阐明这是个真是的dom节点,要通过emptyNodeAt转化为vNode。
patch中用到了两个比拟重要而办法,一个是createElm
,另一个是patchVnode
。
createElm
createElm
的作用是通过虚构节点创立实在的 DOM 并插入到它的父节点中,次要流程如下:
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) { /*创立一个组件节点*/ if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } const data = vnode.data const children = vnode.children const tag = vnode.tag if (isDef(tag)) { vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode) // 创立dom节点 setScope(vnode) if (__WEEX__) { // ... } else { createChildren(vnode, children, insertedVnodeQueue) if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } insert(parentElm, vnode.elm, refElm) } } else if (isTrue(vnode.isComment)) { vnode.elm = nodeOps.createComment(vnode.text) insert(parentElm, vnode.elm, refElm) } else { vnode.elm = nodeOps.createTextNode(vnode.text) insert(parentElm, vnode.elm, refElm) } }
createComponent
只有在vnode是组件的时候才会返回true,这部分当前再剖析。
createChildren
实际上是遍历子虚构节点,递归调用 createElm
,遍历过程中会把 vnode.elm
作为父容器的 DOM 节点传入,因为是递归调用,子元素会优先调用 insert
,所以整个vnode树节点的插入程序是先子后父
。
patchVnode
patchVnode的次要流程和次要代码如下:
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) { /*两个VNode节点雷同则间接返回*/ if (oldVnode === vnode) { return } /* 如果新旧VNode都是动态的,同时它们的key雷同(代表同一节点), 并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次), 那么只须要替换elm以及componentInstance即可。 */ if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) { vnode.elm = oldVnode.elm vnode.componentInstance = oldVnode.componentInstance return } const elm = vnode.elm = oldVnode.elm const oldCh = oldVnode.children const ch = vnode.children /*如果这个VNode节点没有text文本时*/ if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { /*新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren*/ if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) { /*如果老节点没有子节点而新节点存在子节点,先清空elm的文本内容,而后为以后节点退出子节点*/ if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) { /*当新节点没有子节点而老节点有子节点的时候,则移除所有ele的子节点*/ removeVnodes(elm, oldCh, 0, oldCh.length - 1) } else if (isDef(oldVnode.text)) { /*当新老节点都无子节点的时候,只是文本的替换,因为这个逻辑中新节点text不存在,所以间接去除ele的文本*/ nodeOps.setTextContent(elm, '') } } else if (oldVnode.text !== vnode.text) { /*当新老节点text不一样时,间接替换这段文本*/ nodeOps.setTextContent(elm, vnode.text) } }
其实也就几种状况:
- 新旧vnode都有子节点,对子节点进行diff。
- 新vnode有,旧vnode没有,新增。
- 旧vnode有,新vnode没有,删除。
- 如果有text属性,替换文本。
次要来看其中调用的 updateChildren
办法。
updateChildren
在整个 patchVnode
过程中,最简单的就是 updateChildren
办法了,这个办法里进行了新旧虚构节点子节点的比照。
如何比照新旧子节点列表呢?很简略,循环!循环新子节点列表
,其中每一项再去旧子节点列表
里循环查找,而后做解决。但通常状况下,并不是所有子节点的地位都会产生扭转。
举个例子,当初有一个列表,咱们对它的操作大多数就是新增一项,删除一项或者扭转其中的一项,大多数节点的地位是不变的,是可预测的
,没必要每次都去循环查找,所以Vue中应用了一种更快捷的查找形式,大大提高了性能,简略来说就是头尾比拟
:
- 新头:新子节点列表中所有
未解决
的第一个节点 - 旧头:旧子节点列表中所有
未解决
的第一个节点 - 新尾:新子节点列表中所有
未解决
的最初一个节点 - 旧尾:旧子节点列表中所有
未解决
的最初一个节点
Vue中用四个变量 newStartIdx
、newEndIdx
、oldStartIdx
、 oldEndIdx
来标识新旧头部与尾部节点,
1. 新头与旧头
如果新头与旧头是同一节点,它们的地位也一样,所以只需更新节点即可,没有挪动操作。
2. 新尾与旧尾
如果新尾与旧尾是同一节点,它们的地位也一样,所以只需更新节点即可,没有挪动操作。
3. 新尾与旧头
如果新尾与旧头是同一节点,因为它们地位不一样,所以除了更新,还要进行挪动操作。
首先咱们要明确的是更新是以vnode为基准,oldVnode代表的就是实在DOM构造,所以咱们要更新实在DOM其实就是去更新oldVnode。
这里咱们要留神,肯定是挪动到所有 未解决节点
前面,因为新尾是新子节点列表里未解决
的最初一个。
4. 新头与旧尾
如果新头与旧尾是同一节点,因为它们地位不一样,所以除了更新,还要进行挪动操作。
挪动逻辑和下面的 新尾与旧头
大致相同,把旧尾挪动到所有 未解决节点之前
。
如果通过这四次比照还是没有在 旧子节点列表
中找到雷同的节点,那么先用oldVnode生成一个key为oldVnode的key,value为对应下标的哈希表 {key0: 0, key1: 1}
,而后用 新头(newStartVnode)
的key在哈希表里查找,如果找到对应的,判断他们是不是 sameVnode
:
- 如果是,那么就示意在
旧子节点列表
中找到了雷同的节点,进行更新节点操作,最初还要将这个老节点赋值undefined
,防止后续有雷同的key反复比照。 - 如果不是,那么就有可能是标签不一样了,或者input的type扭转了,这时间接创立一个新的节点。
如果在哈希表里没找到对应的,就简略了,间接创立一个新的节点。最初遍历完结,会有两种状况:
- oldStartIdx > oldEndIdx,阐明老节点遍历完结,那么残余新节点就是要新增的,那么一个一个创立进去退出到实在Dom中。
- newStartIdx > newEndIdx,阐明新节点遍历完结,那么残余的老节点就是要删除的,删除即可。
逻辑理分明了,是不是感觉也很简略呢!
结尾
我是周小羊,一个前端萌新,写文章是为了记录本人日常工作遇到的问题和学习的内容,晋升本人,如果您感觉本文对你有用的话,麻烦点个赞激励一下哟~