Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

How to deeply understand the Virtual DOM and Diff algorithms in vue

2025-02-23 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/02 Report--

How to deeply understand the virtual DOM and Diff algorithm in vue? aiming at this problem, this article introduces the corresponding analysis and solution in detail, hoping to help more partners who want to solve this problem to find a more simple and feasible method.

Rendering of real DOM

Before we talk about virtual DOM, let's talk about the rendering of real DOM.

The process of browser real DOM rendering is roughly divided into the following parts

Build the DOM tree. Use HTML parser parsing to process HTML tags and build them into DOM trees (DOM tree). When the parser encounters non-blocking resources (images, css), it will continue parsing, but if it encounters script tags (especially without async and defer attributes), it will block rendering and stop html parsing, which is why it is best to put script tags under body.

Build the CSSOM tree. Similar to building DOM, browsers build style rules into CSSOM. The browser will traverse the rule set in CSS and create a node tree with parent-son, sibling and other relationships according to the css selector.

Build the Render tree. This step associates DOM with CSSOM to determine what CSS rules should be applied to each DOM element. Match all related styles to each visible node in the DOM tree, and determine the calculation style of each node based on the CSS cascade. Invisible nodes (head, attributes including display:none nodes) are not generated into the Render tree.

Layout / reflux (Layout/Reflow). The browser determines the location and size of the node for the first time. If the location and size of the subsequent nodes change, this step triggers the layout adjustment, that is, backflow.

Draw / redraw (Paint/Repaint). Draw each visual part of the element to the screen, including text, colors, borders, shadows, and replacement elements such as buttons and images. Repaint is triggered if elements such as text, color, borders, shadows, and so on change. To ensure that the redrawing is faster than the initial drawing, the drawing on the screen is usually broken down into several layers. Promoting content to the GPU layer (which can be triggered by tranform,filter,will-change,opacity) can improve the performance of drawing and redrawing.

Synthesis (Compositing). This step merges the layers in the drawing process to ensure that they are drawn to the screen in the correct order to display the correct content.

Why do you need virtual DOM

The above is a DOM rendering process. If dom is updated, dom needs to render again, if the following situation exists

This is a container.... This is a container let content = document.getElementsByClassName ('content'); for (let I = 0; I

< 1000000; i++) { content[i][xss_clean] = `This is a content${i}`; // 触发回流 content[i].style.fontSize = `20px`; } 那么需要真实的操作DOM100w次,触发了回流100w次。每次DOM的更新都会按照流程进行无差别的真实dom的更新。所以造成了很大的性能浪费。如果循环里面是复杂的操作,频繁触发回流与重绘,那么就很容易就影响性能,造成卡顿。另外这里要说明一下的是,虚拟DOM并不是意味着比DOM就更快,性能需要分场景,虚拟DOM的性能跟模板大小是正相关。虚拟DOM的比较过程是不会区分数据量大小的,在组件内部只有少量动态节点时,虚拟DOM依然是会对整个vdom进行遍历,相比直接渲染而言是多了一层操作的。 item item item {{ item }} item item 比如上面这个例子,虚拟DOM。虽然只有一个动态节点,但是虚拟DOM依然需要遍历diff整个list的class,文本,标签等信息,最后依然需要进行DOM渲染。如果只是dom操作,就只要操作一个具体的DOM然后进行渲染。虚拟DOM最核心的价值在于,它能通过js描述真实DOM,表达力更强,通过声明式的语言操作,为开发者提供了更加方便快捷开发体验,而且在没有手动优化,大部分情景下,保证了性能下限,性价比更高。 虚拟DOM 虚拟DOM本质上是一个js对象,通过对象来表示真实的DOM结构。tag用来描述标签,props用来描述属性,children用来表示嵌套的层级关系。 const vnode = { tag: 'div', props: { id: 'container', }, children: [{ tag: 'div', props: { class: 'content', }, text: 'This is a container' }]}//对应的真实DOM结构 This is a container 虚拟DOM的更新不会立即操作DOM,而是会通过diff算法,找出需要更新的节点,按需更新,并将更新的内容保存为一个js对象,更新完成后再挂载到真实dom上,实现真实的dom更新。通过虚拟DOM,解决了操作真实DOM的三个问题。 无差别频繁更新导致DOM频繁更新,造成性能问题 频繁回流与重绘 开发体验 另外由于虚拟DOM保存的是js对象,天然的具有跨平台的能力,而不仅仅局限于浏览器。 优点 总结起来,虚拟DOM的优势有以下几点 小修改无需频繁更新DOM,框架的diff算法会自动比较,分析出需要更新的节点,按需更新 更新数据不会造成频繁的回流与重绘 表达力更强,数据更新更加方便 保存的是js对象,具备跨平台能力 不足 虚拟DOM同样也有缺点,首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢。 虚拟DOM实现原理 主要分三部分 通过js建立节点描述对象 diff算法比较分析新旧两个虚拟DOM差异 将差异patch到真实dom上实现更新 Diff算法 为了避免不必要的渲染,按需更新,虚拟DOM会采用Diff算法进行虚拟DOM节点比较,比较节点差异,从而确定需要更新的节点,再进行渲染。vue采用的是深度优先,同层比较的策略。

The comparison between the new node and the old node mainly focuses on three things to achieve the purpose of rendering.

Create a new node

Delete a scrap node

Update existing nodes

How to compare whether the old and new nodes are consistent?

Function sameVnode (a, b) {return (a.key = b.key & & a.asyncFactory = b.asyncFactory & & a.isComment = b.isComment & & isDef (a.data) = isDef (b.data) & & sameInputType (a) B) / / processing of a pair of input nodes) | | (isTrue (a.isAsyncPlaceholder) & & isUndef (b.asyncFactory.error))} / / determine whether two nodes are function sameInputType (a) of the same input input type B) {if (a.tag! = = 'input') return true let i const typeA = isDef (I = a.data) & & isDef (I = i.attrs) & & i.type const typeB = isDef (I = b.data) & & isDef (I = i.attrs) & & i.type / / input type is the same or both type are text return typeA = = typeB | | isTextInputType (typeA) & & isTextInputType (typeB)}

As you can see, whether the two nodes are the same is the need to compare tags (tag), attributes (data is used in vue to represent the attribute props in vnode), comment nodes (isComment), and special treatment will be done if you encounter input.

Create a new node

When there is a new node and the old node does not, it means that this is a new content node. Only element nodes, text nodes, and comment nodes can be created and inserted into the DOM.

Delete old node

When the old node has, but the new node does not, it means that the new node gives up part of the old node. Deleting a node will in turn delete the children of the old node.

Update Node

There are both the new node and the old node, so everything is based on the new node, and the old node is updated. How to determine whether the node needs to be updated?

Judge whether the new node is exactly the same as the old node, so there is no need to update

/ / determine whether vnode and oldVnode are exactly the same if (oldVnode = vnode) {return;}

Determine whether the new node is static with the old node, whether the key is the same, and whether it is a cloned node (if it is not a cloned node, it means that the rendering function has been reset and needs to be re-rendered) or whether the once attribute is set, and replace componentInstance if the condition is met

/ / whether it is a static node, whether the key is the same, whether it is a clone node, or whether the once attribute if (isTrue (vnode.isStatic) & & isTrue (oldVnode.isStatic) & & vnode.key = oldVnode.key & & (isTrue (vnode.isCloned) | | isTrue (vnode.isOnce) {vnode.componentInstance = oldVnode.componentInstance; return;}

Judge whether the new node has text (judged by the text attribute), if there is text, then need to compare the same level of old nodes, if the old node text is different from the new node text, then use the new text content. If the new node has no text, then the relevant situation of the child node needs to be judged later.

/ / determine whether the new node has text if (isUndef (vnode.text)) {/ / if there is no text, process the relevant code of the child node.} else if (oldVnode.text! = = vnode.text) {/ / the new node text replaces the old node text nodeOps.setTextContent (elm, vnode.text)}

Judge the correlation between the new node and the child nodes of the old node. It can be divided into four situations.

Both the new node and the old node have children

Only the new node has child nodes

Only the old node has child nodes

Neither the new node nor the old node has child nodes.

All have child nodes.

For cases where all have child nodes, you need to compare the new and old nodes. If they are different, you need to perform diff operation. In vue, this is the updateChildren method. We will talk about it in detail later. The comparison of child nodes is mainly a two-end comparison.

/ / determine whether the new node has text if (isUndef (vnode.text)) {/ / if both the new and old nodes have child nodes, if the new and old child nodes are different, then compare the child nodes Is the updateChildren method if (isDef (oldCh) & & isDef (ch)) {if (oldCh! = = ch) updateChildren (elm, oldCh, ch, insertedVnodeQueue, removeOnly)}} else if (oldVnode.text! = = vnode.text) {/ / the new node text replaces the old node text nodeOps.setTextContent (elm, vnode.text)}

Only the new node has child nodes

Only the new node has a child node, which means that this is new content. Then a new child node is added to the DOM. Before the new node is added, a duplicate key will be detected and reminded. At the same time, consider that if the old node is only a text node without child nodes, you need to empty the text content of the old node.

/ / only the new node has a child node if (isDef (ch)) {/ / check for duplicate key if (process.env.NODE_ENV! = = 'production') {checkDuplicateKeys (ch)} / / clear the old node text if (isDef (oldVnode.text)) nodeOps.setTextContent (elm,'') / / add a new node addVnodes (elm, null, ch, 0, ch.length-1) InsertedVnodeQueue)} / / check for duplicate keyfunction checkDuplicateKeys (children) {const seenKeys = {} for (let I = 0) I

< children.length; i++) { const vnode = children[i] //子节点每一个Key const key = vnode.key if (isDef(key)) { if (seenKeys[key]) { warn( `Duplicate keys detected: '${key}'. This may cause an update error.`, vnode.context ) } else { seenKeys[key] = true } } }} 只有旧节点有子节点 只有旧节点有,那就说明,新节点抛弃了旧节点的子节点,所以需要删除旧节点的子节点 if (isDef(oldCh)) { //删除旧节点 removeVnodes(oldCh, 0, oldCh.length - 1)} 都没有子节点 这个时候需要对旧节点文本进行判断,看旧节点是否有文本,如果有就清空 if (isDef(oldVnode.text)) { //清空 nodeOps.setTextContent(elm, '')} 整体的逻辑代码如下 function patchVnode( oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) { // 判断vnode与oldVnode是否完全一样 if (oldVnode === vnode) { return } if (isDef(vnode.elm) && isDef(ownerArray)) { // 克隆重用节点 vnode = ownerArray[index] = cloneVNode(vnode) } const elm = vnode.elm = oldVnode.elm if (isTrue(oldVnode.isAsyncPlaceholder)) { if (isDef(vnode.asyncFactory.resolved)) { hydrate(oldVnode.elm, vnode, insertedVnodeQueue) } else { vnode.isAsyncPlaceholder = true } return } // 是否是静态节点,key是否一样,是否是克隆节点或者是否设置了once属性 if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ) { vnode.componentInstance = oldVnode.componentInstance return } let i const data = vnode.data if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode) } const oldCh = oldVnode.children const ch = vnode.children if (isDef(data) && isPatchable(vnode)) { //调用update回调以及update钩子 for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode) } //判断新节点是否有文本 if (isUndef(vnode.text)) { //新旧节点都有子节点情况下,如果新旧子节点不相同,那么进行子节点的比较,就是updateChildren方法 if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) { //只有新节点有子节点 if (process.env.NODE_ENV !== 'production') { //重复Key检测 checkDuplicateKeys(ch) } //清除旧节点文本 if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') //添加新节点 addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) { //只有旧节点有子节点,删除旧节点 removeVnodes(oldCh, 0, oldCh.length - 1) } else if (isDef(oldVnode.text)) { //新旧节点都无子节点 nodeOps.setTextContent(elm, '') } } else if (oldVnode.text !== vnode.text) { //新节点文本替换旧节点文本 nodeOps.setTextContent(elm, vnode.text) } if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode) }} 配上流程图会更清晰点 子节点的比较更新updateChildren 新旧节点都有子节点的情况下,这个时候是需要调用updateChildren方法来比较更新子节点的。所以在数据上,新旧节点子节点,就保存为了两个数组。 const oldCh = [oldVnode1, oldVnode2,oldVnode3];const newCh = [newVnode1, newVnode2,newVnode3]; 子节点更新采用的是双端比较的策略,什么是双端比较呢,就是新旧节点比较是通过互相比较首尾元素(存在4种比较),然后向中间靠拢比较(newStartIdx,与oldStartIdx递增,newEndIdx与oldEndIdx递减)的策略。 比较过程 向中间靠拢 这里对上面出现的新前,新后,旧前,旧后做一下说明 新前,指的是新节点未处理的子节点数组中的第一个元素,对应到vue源码中的newStartVnode 新后,指的是新节点未处理的子节点数组中的最后一个元素,对应到vue源码中的newEndVnode 旧前,指的是旧节点未处理的子节点数组中的第一个元素,对应到vue源码中的oldStartVnode 旧后,指的是旧节点未处理的子节点数组中的最后一个元素,对应到vue源码中的oldEndVnode 子节点比较过程 接下来对上面的比较过程以及比较后做的操作做下说明 新前与旧前的比较,如果他们相同,那么进行上面说到的patchVnode更新操作,然后新旧节点各向后一步,进行第二项的比较...直到遇到不同才会换种比较方式 if (sameVnode(oldStartVnode, newStartVnode)) { // 更新子节点 patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) // 新旧各向后一步 oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx]} 新后与旧后的比较,如果他们相同,同样进行pathchVnode更新,然后新旧各向前一步,进行前一项的比较...直到遇到不同,才会换比较方式 if (sameVnode(oldEndVnode, newEndVnode)) { //更新子节点 patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) // 新旧向前 oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx]} 新后与旧前的比较,如果它们相同,就进行更新操作,然后将旧前移动到所有未处理旧节点数组最后面,使旧前与新后位置保持一致,然后双方一起向中间靠拢,新向前,旧向后。如果不同会继续切换比较方式 if (sameVnode(oldStartVnode, newEndVnode)) { patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) //将旧子节点数组第一个子节点移动插入到最后 canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) //旧向后 oldStartVnode = oldCh[++oldStartIdx] //新向前 newEndVnode = newCh[--newEndIdx] 新前与旧后的比较,如果他们相同,就进行更新,然后将旧后移动到所有未处理旧节点数组最前面,是旧后与新前位置保持一致,,然后新向后,旧向前,继续向中间靠拢。继续比较剩余的节点。如果不同,就使用传统的循环遍历查找。 if (sameVnode(oldEndVnode, newStartVnode)) { patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) //将旧后移动插入到最前 canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) //旧向前 oldEndVnode = oldCh[--oldEndIdx] //新向后 newStartVnode = newCh[++newStartIdx]} 循环遍历查找,上面四种都没找到的情况下,会通过key去查找匹配。 进行到这一步对于没有设置key的节点,第一次会通过createKeyToOldIdx建立key与index的映射 {key:index} // 对于没有设置key的节点,第一次会通过createKeyToOldIdx建立key与index的映射 {key:index}if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) 然后拿新节点的key与旧节点进行比较,找到key值匹配的节点的位置,这里需要注意的是,如果新节点也没key,那么就会执行findIdxInOld方法,从头到尾遍历匹配旧节点 //通过新节点的key,找到新节点在旧节点中所在的位置下标,如果没有设置key,会执行遍历操作寻找idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)//findIdxInOld方法function findIdxInOld(node, oldCh, start, end) { for (let i = start; i < end; i++) { const c = oldCh[i] //找到相同节点下标 if (isDef(c) && sameVnode(node, c)) return i }} 如果通过上面的方法,依旧没找到新节点与旧节点匹配的下标,那就说明这个节点是新节点,那就执行新增的操作。 //如果新节点无法在旧节点中找到自己的位置下标,说明是新元素,执行新增操作if (isUndef(idxInOld)) { createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)} 如果找到了,那么说明在旧节点中找到了key值一样,或者节点和key都一样的旧节点。如果节点一样,那么在patchVnode之后,需要将旧节点移动到所有未处理节点之前,对于key一样,元素不同的节点,将其认为是新节点,执行新增操作。执行完成后,新节点向后一步。 //如果新节点无法在旧节点中找到自己的位置下标,说明是新元素,执行新增操作if (isUndef(idxInOld)) { // 新增元素 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)} else { // 在旧节点中找到了key值一样的节点 vnodeToMove = oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { // 相同子节点更新操作 patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) // 更新完将旧节点赋值undefined oldCh[idxInOld] = undefined //将旧节点移动到所有未处理节点之前 canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // 如果是相同的key,不同的元素,当做新节点,执行创建操作 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 / / add node addVnodes (parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)} else if (newStartIdx > newEndIdx) {/ / find redundant old nodes and perform delete operation removeVnodes (oldCh, oldStartIdx, oldEndIdx)}

Comparison and summary of child nodes

The above is a complete process of updating the child nodes, which is the complete logic code.

Function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {let oldStartIdx = 0 let newStartIdx = 0 let oldEndIdx = oldCh.length-1 let oldStartVnode = oldCh [0] / / the old let oldEndVnode = oldCh [oldEndIdx] / / the old let newEndIdx = newCh.length-1 let newStartVnode = newCh [0] / / the new let newEndVnode = newCh [newEndIdx] / / the new let oldKeyToIdx, idxInOld, vnodeToMove RefElm / / removeOnly is a special flag used only by / / to ensure removed elements stay in correct relative positions / / during leaving transitions const canMove =! removeOnly if (process.env.NODE_ENV! = = 'production') {checkDuplicateKeys (newCh)} / / double-ended comparison traversal while (oldStartIdx newEndIdx) {/ / find redundant old nodes Perform delete operation removeVnodes (oldCh, oldStartIdx, oldEndIdx)}} on how to deeply understand the virtual DOM and Diff algorithms in vue. I hope the above content can be of some help to everyone. If you still have a lot of doubts to be solved, you can follow the industry information channel for more related knowledge.

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report