vue的虚拟DOM和diff算法
一 vnode和vdom是什么?
1.vnode本质就是一个js对象。只不过它是以对象的形式,通过不同的属性去描述一个节点结构。
2.vdom:就是多个vnode组成的树形结构
二、为什么要引入vdom而不是直接操作真实dom?
-
-
- DOM 操作非常耗费性能,频繁操作会触发页面的回流和重绘,严重影响性能
- 手动管理DOM操作更行逻辑容易出错,特别在负载的UI场景下
- 虚拟DOM是为了解决上述问题。用JS模拟DOM结构(因为JS执行速度很快)
- 新旧vnode 对比得出最小的更新范围,最后更新DOM。从而明显的提升了渲染效率
-
三、vnode是什么样的?
我们可以用JS模拟一下DOM结构,也就是我们所说的vnode。如下dom结构:
<div id="div1" class="container">
<p>列表</p>
<ul style="font-size: 20px>
<li>a</li>
</ul>
</div>
用JS模拟出的结构如下:
{ tag:'div', props:{ id:'div1', className:'container' }, children:[ { tag:'p', text:'列表' }, { tag: 'ul', props:{ style:'font-size:20px' }, children:[ { tag:'li', text:'a' } ] } ] }
vue在挂载阶段会调用 vm._render() 函数生成虚拟节点vnode,vnode会存储渲染时候需要的信息,如下:
{ tag, //tag名 data,//数据 children,//子dom text,//文本内容 elm,//真实DOM key //标识 后续做diff算法需要key标识 }
eg:<ul><li>a</li><li>a</li><ul> 生成虚拟dom如下:
四、虚拟DOM--diff 算法
1. 为什么需要 diff 算法?
-
-
- 上面我们知道通过比较虚拟DOM,得出最小变范围,以最小代价更新DOM。
- 比较两颗树最小差异的复杂度是o(n^3)(第一,遍历tree1;第二,遍历tree2;第三,排序。所以推算出时间复杂度时o(n^3))
- 这种方式虽然精确,但性能不好。如果1000个节点,那么要就要1亿次。所以这个算法不可用
-
2. 优化:
-
-
- 降低复杂度:从o(n^3) 降到接近o(n)
- 最小化DOM更新:找出变化部分,仅对其进行操作
-
2.虚拟DOM树对比更新的算法优化(diff算法):
-
-
- 只比较同一层级,不跨级比较
- tag 不相同或者tag相同但key不同,则认为是不相同的节点,则直接删掉重建,不再深度比较
- tag 和 key 两者都相同,则认为是相同节点,不再深度比较
-
五、diff算法原理
diff的目的是找出差异,最小化的更新视图。diff算法发生在视图更新阶段,当数据发生变化时,会对新旧虚拟DOM进行对比,指渲染有变化的部分
- 1. 新旧DOM树逐层比较,只比较同一层级,不跨级比较
- 由于树结构是逐层递归,所以整体复杂度接近o(n)
- 2. 优先比较是否是同一类型的节点
- 如果不是同类型节点(即tag标签不同),则直接删除旧节点,新节点替换旧节点
- 如果是同类型节点(即tag标签相同),则继续比较是否有相同的key,即vnode是否相等
- 3. 然后比较vnode 是否相同。(即tag标签相同,继续比较 key)
-
- key 不相同,则表示虚拟节点vnode 不同。直接删除,新节点替换旧节点
- key 相同,则表示vnode 相同。继续下面的比较
4.如果是相同节点(vnode相同,tag和key相同),则通过 patchVnode 进行diff
- 对比新老vnode的属性,有变化的更新到真实dom中,两个节点处理完开始对比子节点
-
newVnode 没有children,oldVnode有children, 也就是新节点是文本,那么直接更新text即可
-
newVnode有children, oldVnode没有chilren, 那么就创建子节点插入元素中
-
新老vnode都有chilren,则继续下面的判断
-
- 新旧节点数组会创建四个指针,分别指向两个节点的首尾。首尾指针不断向中间移动
- 旧头新头 比较
- tag 和 key 相同,则递归比较两个节点的子节点。oldStartIndex++、newStartIndex++
- tag 和 key 不相同,则继续下面的比较
- 旧尾新尾 比较
- tag 和 key 相同,则递归比较两个节点的子节点。oldEndIndex--、newEndIndex--
- tag 和 key 不相同,则继续下面的比较
- 旧头新尾 比较
- tag 和 key 相同,则递归比较两个节点的子节点。oldStartIndex++、newEndIndex--
- tag 和 key 不相同,则继续下面的比较
- 旧尾新头 比较
- tag 和 key 相同,则递归比较两个节点的子节点。oldEndIndex--、newStartIndex++
- tag 和 key 不相同,则继续下面的比较
- 以上都会不符,采用对比查找。新头指针在旧的列表中查找
- 未找到,创建dom元素并插入
- 找到了,比较差异、复用并将其移动到前面去(当前新头指针的位置),旧的移走的位置设为undefined(老的节点为空的话,会自动找下一个节点元素)
五、path算法(或者diff算法)的流程
vue里面diff算法是通过patch函数来完成的,所以也叫作patch算法
先看一下vue中patch代码(core/vdom/path.js)
return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) { if (isUndef(vnode)) { if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); } return } var isInitialPatch = false; var insertedVnodeQueue = []; if (isUndef(oldVnode)) { // empty mount (likely as component), create new root element isInitialPatch = true; createElm(vnode, insertedVnodeQueue, parentElm, refElm); } else { var isRealElement = isDef(oldVnode.nodeType); if (!isRealElement && sameVnode(oldVnode, vnode)) { //比较元素类型 // patch existing root node patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly); //相同则比较元素 } else { // 省略 oldVnode = emptyNodeAt(oldVnode); } // replacing existing element var oldElm = oldVnode.elm; var parentElm$1 = nodeOps.parentNode(oldElm); // create new node // 不同则直接创建新的 createElm( vnode, insertedVnodeQueue, // extremely rare edge case: do not insert if old element is in a // leaving transition. Only happens when combining transition + // keep-alive + HOCs. (#4590) oldElm._leaveCb ? null : parentElm$1, nodeOps.nextSibling(oldElm) ); // 省略 parent相关内容 } } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch); return vnode.elm }
patch(oldVnode,vonode)对新老虚拟节点进行对比:
1. 如果oldVnode是真实元素,则表示是第一次patch(组件第一次挂载的时候,一般是 <div id="app></div>),那么直接 createEllm()创建新的DOM元素
判断是否是真实元素,是根据节点是否存在nodeType 来判断是否是真实元素
var isRealElement = isDef(oldVnode.nodeType); if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly); }
2.如果新的vnode不存在,老的vnode存在,则说明是组件销毁阶段,需要销毁老节点
3.如果老的vnode存在并且不是真实元素,新的vnode也存在,则表示是更新阶段,那么就要判断两个虚拟节点是否相同,判断相同节点代码如下
function sameVnode (a, b) { return ( a.key === b.key && ( ( a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) ) || ( isTrue(a.isAsyncPlaceholder) && a.asyncFactory === b.asyncFactory && isUndef(b.asyncFactory.error) ) ) ) }
根据上面代码可以得出判断相同的条件主要有两个:
1. key 必须相同(都是undefined 则也是相同的),这也说明为什么在vue中列表循环时要加 :key="唯一标识"
2.DOM元素的标签必须相同(比如都是div)
3.1)如果新老vnode不是相同节点,那么就删除旧的创建新的DOM元素
3.2)如果是相同节点,那么就通过 patchVnode进行diff
* 对比新老vnode的属性,有变化的更新到真实dom中,两个节点处理完开始对比子节点
* 新vnode没有children,也就是新节点是文本,那么直接更新text即可
* 新vnode有chilren,老vnode没有chilren,那么就删除旧的子节点
* 新老vnode都有chilren,那么调用 updateChildren 比较他们的差异,这一部分就是diff算法的核心。
下面让我们看看 updateChildren 是如何对孩子们的差异进行比较的
六、updateChildren 孩子差异比较算法,也是diff算法的核心
先看一下uodateChildren代码:
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { 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, vnodeToMove, refElm // removeOnly is a special flag used only by <transition-group> // to ensure removed elements stay in correct relative positions // during leaving transitions const canMove = !removeOnly if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(newCh) } while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) if (isUndef(idxInOld)) { // New element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { vnodeToMove = oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue) oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // same key but different element. treat as new element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } newStartVnode = newCh[++newStartIdx] } } if (oldStartIdx > oldEndIdx) { refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } }
对比子节点流程:
对新老子节点数组使用两个指针,分别指向头和尾,然后不断像中间靠拢进行对比。具体步骤如下:
对比 旧树头指针 和 新树头指针,看看两个节点是否一样。如果不相同,则对比尾指针,如果相同则做如下对比:
- 如果一样,则采用深度优先的方式,递归循环这两个节点是否有子节点,如果有就继续比较。
- 然后将两个头指针向后移动
对比 旧树尾指针 和 新树尾指针。看看两节点是否一致,如果不相同,则需要旧头和新尾交叉对比,如果相同则做如下操作
- 递归比较两个节点的子节点
- 然后将两个尾指针前移
对比 旧树头指针 和 新树尾指针。如果不相同。则需要旧尾和新头交叉对比,如果相同:
- 递归比较两个节点的子节点
- 将旧树头指针后移,新树尾指针前移
对比 旧尾指针 和 新树头指针。如果不相同。则需要拿新头指针在旧的列表中查找,如果相同:
- 递归比较两个节点的子节点
- 将旧尾指针前移,新树头指针后移
新头指针在旧的列表中查找。
- 如果未找到:则创建新dom元素
- 如果找到了先递归比较两个节点的子节点
- 然后将找到的真实dom元素移动到旧树头指针之前
- 最后将新树指针后移
这样一直递归遍历下去,知道整棵树完成对比。借用网上的一张流程:
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性