이 기사는 vue의 virtual DOM 및 Diff 알고리즘에 대한 심층적인 이해를 제공하고, virtual DOM 및 Diff 알고리즘을 자세히 분석하는 것이 모든 사람에게 도움이 되기를 바랍니다.
최근에 virtual DOM과 Diff를 검토했고, 많은 정보를 읽었으며, vue에 대한 이해를 깊게 하기 위해 이 긴 글을 요약했습니다.
이 기사에서는 Vue의 가상 DOM 및 Diff 알고리즘을 더 자세히 분석합니다(그래픽 마스터 덕분에). 기사가 좋다고 생각하시면 좋아요를 눌러주세요. 틀린 부분이 있으면 댓글란에 지적해 주시면 됩니다. 물론 이해가 안 되는 부분이 있으면 지적하셔도 좋습니다. 질문을 하세요. [관련 추천: "vue.js Tutorial"]
실제 DOM 렌더링
가상 DOM에 대해 이야기하기 전에 실제 DOM 렌더링에 대해 이야기해 보겠습니다.
브라우저에서 실제 DOM을 렌더링하는 과정은 대략 다음과 같은 부분으로 나뉩니다
DOM 트리 구축. HTML 파서를 통해 HTML 태그를 구문 분석 및 처리하고 이를 DOM 트리로 구축합니다. 파서가 비차단 리소스(이미지, CSS)를 발견하면 계속 구문 분석하지만 스크립트 태그(특히 async 및 defer 속성이 없는 경우)를 발견하면 렌더링을 차단하고 HTML 구문 분석을 중지하므로 스크립트 태그를 본문 아래에 넣는 것이 가장 좋습니다.
CSSOM 트리 구축. DOM을 구축하는 것과 마찬가지로 브라우저는 CSSOM에도 스타일 규칙을 구축합니다. 브라우저는 CSS에 설정된 규칙을 순회하고 CSS 선택기를 기반으로 부모-자식, 형제 등 관계가 있는 노드 트리를 만듭니다.
렌더 트리 구축. 이 단계에서는 DOM과 CSSOM을 연결하고 각 DOM 요소에 어떤 CSS 규칙을 적용해야 하는지 결정합니다. 모든 관련 스타일을 DOM 트리의 표시되는 각 노드와 일치시키고 CSS 계단식을 기반으로 각 노드의 계산된 스타일을 결정합니다. 표시되지 않는 노드(헤드, 디스플레이:없음을 포함한 속성이 있는 노드)는 렌더 트리에 생성되지 않습니다.
레이아웃/리플로우. 브라우저가 처음으로 노드의 위치와 크기를 결정하는 것을 레이아웃이라고 합니다. 이후에 노드의 위치와 크기가 변경되면 이 단계에서 레이아웃 조정이 시작되는데, 이는 reflow입니다.
- 페인트/다시 칠하기
. 텍스트, 색상, 테두리, 그림자, 버튼 및 이미지와 같은 교체된 요소를 포함하여 요소의 보이는 모든 부분을 화면에 그립니다. 텍스트, 색상, 테두리, 그림자 및 기타 요소가 변경되면 Repaint가 실행됩니다. 처음 그리는 것보다 다시 그리는 것이 더 빠르게 이루어지도록 하기 위해 화면의 그림은 여러 레이어로 나누어지는 경우가 많습니다. 콘텐츠를 GPU 레이어(변환, 필터, 변경 의지, 불투명도에 의해 트리거될 수 있음)로 승격하면 그리기 및 다시 그리기 성능이 향상될 수 있습니다.
- 합성
. 이 단계에서는 그리기 프로세스의 레이어를 병합하여 올바른 순서로 그려져 화면에 올바른 콘텐츠가 표시되도록 합니다.
가상 DOM이 필요한 이유
위는 DOM 렌더링 과정입니다. DOM이 업데이트되면 DOM을 다시 렌더링해야 합니다.
<body> <div id="container"> <div class="content" style="color: red;font-size:16px;"> This is a container </div> .... <div class="content" style="color: red;font-size:16px;"> This is a container </div> </div> </body> <script> let content = document.getElementsByClassName('content'); for (let i = 0; i < 1000000; i++) { content[i].innerHTML = `This is a content${i}`; // 触发回流 content[i].style.fontSize = `20px`; } </script>
그러면 DOM이 실제로 작동해야 합니다. 100w 회, 리플로우를 100만 회 트리거합니다. DOM의 모든 업데이트는 프로세스에 따라 수행되며 실제 DOM은 아무런 차이 없이 업데이트됩니다. 그래서 성능 낭비가 많이 발생했습니다. 리플로우 및 다시 그리기를 자주 트리거하는 복잡한 작업이 루프에 있는 경우 성능에 쉽게 영향을 미치고 지연이 발생합니다. 또한 가상 DOM이 DOM보다 빠르다는 것을 의미하지는 않습니다. 가상 DOM의 성능은 템플릿 크기와 긍정적인 관련이 있습니다. 가상 DOM의 비교 프로세스는 데이터의 크기를 구분하지 않습니다. 구성 요소 내부에 동적 노드가 몇 개만 있는 경우 가상 DOM은 여전히 전체 vdom을 탐색하며 이는 직접 렌더링에 비해 추가 작업 계층입니다.
<div class="list"> <p class="item">item</p> <p class="item">item</p> <p class="item">item</p> <p class="item">{{ item }}</p> <p class="item">item</p> <p class="item">item</p> </div>
예를 들어 위의 예에서는 가상 DOM입니다. 동적 노드가 하나뿐이지만 가상 DOM은 여전히 전체 diff 목록의 클래스, 텍스트, 레이블 및 기타 정보를 순회해야 하며 마지막으로 DOM 렌더링이 여전히 필요합니다. 단지 DOM 작업이라면 특정 DOM만 작업한 후 렌더링하면 됩니다. virtual DOM의 핵심 가치는 표현력이 풍부한 js를 통해 실제 DOM을 설명할 수 있다는 점입니다. 선언적 언어 작업을 통해 개발자에게 더 편리하고 빠른 개발 경험을 제공하며, 대부분의 시나리오에서 수동 최적화가 필요하지 않습니다. 성능의 한계가 보장되며 가격 대비 성능 비율이 더 높습니다.
Virtual DOM
Virtual DOM은 본질적으로 객체를 통해 실제 DOM 구조를 나타내는 js 객체입니다. 태그는 태그를 설명하는 데 사용되고, 소품은 속성을 설명하는 데 사용되며, 자식은 중첩된 계층 관계를 나타내는 데 사용됩니다.
const vnode = { tag: 'div', props: { id: 'container', }, children: [{ tag: 'div', props: { class: 'content', }, text: 'This is a container' }] } //对应的真实DOM结构 <div id="container"> <div class="content"> This is a container </div> </div>
가상 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采用的是深度优先,同层比较的策略。
新节点与旧节点的比较主要是围绕三件事来达到渲染目的
创建新节点
删除废节点
更新已有节点
如何比较新旧节点是否一致呢?
function sameVnode(a, b) { return ( a.key === b.key && a.asyncFactory === b.asyncFactory && ( ( a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) //对input节点的处理 ) || ( isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error) ) ) ) } //判断两个节点是否是同一种 input 输入类型 function sameInputType(a, 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 相同或者两个type都是text return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB) }
可以看到,两个节点是否相同是需要比较标签(tag),属性(在vue中是用data表示vnode中的属性props), 注释节点(isComment) 的,另外碰到input的话,是会做特殊处理的。
创建新节点
当新节点有的,旧节点没有,这就意味着这是全新的内容节点。只有元素节点,文本节点,注释节点才能被创建插入到DOM中。
删除旧节点
当旧节点有,而新节点没有,那就意味着,新节点放弃了旧节点的一部分。删除节点会连带的删除旧节点的子节点。
更新节点
新的节点与旧的的节点都有,那么一切以新的为准,更新旧节点。如何判断是否需要更新节点呢?
- 判断新节点与旧节点是否完全一致,一样的话就不需要更新
// 判断vnode与oldVnode是否完全一样 if (oldVnode === vnode) { return; }
- 判断新节点与旧节点是否是静态节点,key是否一样,是否是克隆节点(如果不是克隆节点,那么意味着渲染函数被重置了,这个时候需要重新渲染)或者是否设置了once属性,满足条件的话替换componentInstance
// 是否是静态节点,key是否一样,是否是克隆节点或者是否设置了once属性 if ( isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ) { vnode.componentInstance = oldVnode.componentInstance; return; }
- 判断新节点是否有文本(通过text属性判断),如果有文本那么需要比较同层级旧节点,如果旧节点文本不同于新节点文本,那么采用新的文本内容。如果新节点没有文本,那么后面需要对子节点的相关情况进行判断
//判断新节点是否有文本 if (isUndef(vnode.text)) { //如果没有文本,处理子节点的相关代码 .... } else if (oldVnode.text !== vnode.text) { //新节点文本替换旧节点文本 nodeOps.setTextContent(elm, vnode.text) }
- 判断新节点与旧节点的子节点相关状况。这里又能分为4种情况
新节点与旧节点都有子节点
只有新节点有子节点
只有旧节点有子节点
新节点与旧节点都没有子节点
都有子节点
对于都有子节点的情况,需要对新旧节点做比较,如果他们不相同,那么需要进行diff操作,在vue中这里就是updateChildren方法,后面会详细再讲,子节点的比较主要是双端比较。
//判断新节点是否有文本 if (isUndef(vnode.text)) { //新旧节点都有子节点情况下,如果新旧子节点不相同,那么进行子节点的比较,就是updateChildren方法 if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } } else if (oldVnode.text !== vnode.text) { //新节点文本替换旧节点文本 nodeOps.setTextContent(elm, vnode.text) }
只有新节点有子节点
只有新节点有子节点,那么就代表着这是新增的内容,那么就是新增一个子节点到DOM,新增之前还会做一个重复key的检测,并做出提醒,同时还要考虑,旧节点如果只是一个文本节点,没有子节点的情况,这种情况下就需要清空旧节点的文本内容。
//只有新节点有子节点 if (isDef(ch)) { //检查重复key if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(ch) } //清除旧节点文本 if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') //添加新节点 addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } //检查重复key function 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 // 新增节点 addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { // 发现多余的旧节点,执行删除操作 removeVnodes(oldCh, oldStartIdx, oldEndIdx) }
子节点比较总结
上面就是子节点比较更新的一个完整过程,这是完整的逻辑代码
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, newCh, newStartIdx) // 新旧各向后一步 oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { //新后与旧后 //更新子节点 patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) //新旧各向前一步 oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else 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] } else if (sameVnode(oldEndVnode, newStartVnode)) { // 新前与旧后 patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) //将旧后移动插入到最前 canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) //新向后,旧向前 oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { // 对于没有设置key的节点,第一次会通过createKeyToOldIdx建立key与index的映射 {key:index} if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) //通过新节点的key,找到新节点在旧节点中所在的位置下标,如果没有设置key,会执行遍历操作寻找 idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) //如果新节点无法在旧节点中找到自己的位置下标,说明是新元素,执行新增操作 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 // 新增节点 addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { // 发现多余的旧节点,执行删除操作 removeVnodes(oldCh, oldStartIdx, oldEndIdx) } }
更多编程相关知识,请访问:编程入门!!
위 내용은 vue의 가상 DOM 및 Diff 알고리즘에 대한 심층적인 이해의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

vue.js는 여러 기능을 통해 사용자 경험을 향상시킵니다. 1. 응답 시스템은 실시간 데이터 피드백을 실현합니다. 2. 구성 요소 개발은 코드 재사용 성을 향상시킵니다. 3. Vuerouter는 원활한 내비게이션을 제공합니다. 4. 동적 데이터 바인딩 및 전환 애니메이션은 상호 작용 효과를 향상시킵니다. 5. 오류 처리 메커니즘은 사용자 피드백을 보장합니다. 6. 성능 최적화 및 모범 사례는 응용 프로그램 성능을 향상시킵니다.

웹 개발에서 vue.js의 역할은 개발 프로세스를 단순화하고 효율성을 향상시키는 점진적인 JavaScript 프레임 워크 역할을하는 것입니다. 1) 개발자는 반응 형 데이터 바인딩 및 구성 요소 개발을 통해 비즈니스 로직에 집중할 수 있습니다. 2) vue.js의 작동 원리는 반응 형 시스템 및 가상 DOM에 의존하여 성능을 최적화합니다. 3) 실제 프로젝트에서는 Vuex를 사용하여 글로벌 상태를 관리하고 데이터 대응 성을 최적화하는 것이 일반적입니다.

vue.js는 2014 년 Yuxi가 출시하여 사용자 인터페이스를 구축하기 위해 진보적 인 JavaScript 프레임 워크입니다. 핵심 장점은 다음과 같습니다. 1. 응답 데이터 바인딩, 데이터 변경의 자동 업데이트보기; 2. 구성 요소 개발, UI는 독립적이고 재사용 가능한 구성 요소로 분할 될 수 있습니다.

Netflix는 React를 프론트 엔드 프레임 워크로 사용합니다. 1) React의 구성 요소화 된 개발 모델과 강력한 생태계가 Netflix가 선택한 주된 이유입니다. 2) 구성 요소화를 통해 Netflix는 복잡한 인터페이스를 비디오 플레이어, 권장 목록 및 사용자 댓글과 같은 관리 가능한 청크로 분할합니다. 3) React의 가상 DOM 및 구성 요소 수명주기는 렌더링 효율성 및 사용자 상호 작용 관리를 최적화합니다.

프론트 엔드 기술에서 Netflix의 선택은 주로 성능 최적화, 확장 성 및 사용자 경험의 세 가지 측면에 중점을 둡니다. 1. 성능 최적화 : Netflix는 React를 주요 프레임 워크로 선택하고 Speedcurve 및 Boomerang과 같은 도구를 개발하여 사용자 경험을 모니터링하고 최적화했습니다. 2. 확장 성 : 마이크로 프론트 엔드 아키텍처를 채택하여 응용 프로그램을 독립 모듈로 분할하여 개발 효율성 및 시스템 확장 성을 향상시킵니다. 3. 사용자 경험 : Netflix는 재료 -UI 구성 요소 라이브러리를 사용하여 A/B 테스트 및 사용자 피드백을 통해 인터페이스를 지속적으로 최적화하여 일관성과 미학을 보장합니다.

NetflixusesAcustomFrameworkCalled "Gibbon"BuiltonReact, NotreactorVuedirectly.1) TeamExperience : 2) ProjectComplexity : vueforsimplerProjects, 3) CustomizationNeeds : reactoffersmoreflex.4)

Netflix는 주로 프레임 워크 선택의 성능, 확장 성, 개발 효율성, 생태계, 기술 부채 및 유지 보수 비용을 고려합니다. 1. 성능 및 확장 성 : Java 및 SpringBoot는 대규모 데이터 및 높은 동시 요청을 효율적으로 처리하기 위해 선택됩니다. 2. 개발 효율성 및 생태계 : React를 사용하여 프론트 엔드 개발 효율성을 향상시키고 풍부한 생태계를 활용하십시오. 3. 기술 부채 및 유지 보수 비용 : Node.js를 선택하여 유지 보수 비용과 기술 부채를 줄이기 위해 마이크로 서비스를 구축하십시오.

Netflix는 주로 VUE가 특정 기능을 위해 보충하는 프론트 엔드 프레임 워크로 React를 사용합니다. 1) React의 구성 요소화 및 가상 DOM은 Netflix 애플리케이션의 성능 및 개발 효율을 향상시킵니다. 2) VUE는 Netflix의 내부 도구 및 소규모 프로젝트에 사용되며 유연성과 사용 편의성이 핵심입니다.


핫 AI 도구

Undresser.AI Undress
사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover
사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool
무료로 이미지를 벗다

Clothoff.io
AI 옷 제거제

AI Hentai Generator
AI Hentai를 무료로 생성하십시오.

인기 기사

뜨거운 도구

ZendStudio 13.5.1 맥
강력한 PHP 통합 개발 환경

메모장++7.3.1
사용하기 쉬운 무료 코드 편집기

mPDF
mPDF는 UTF-8로 인코딩된 HTML에서 PDF 파일을 생성할 수 있는 PHP 라이브러리입니다. 원저자인 Ian Back은 자신의 웹 사이트에서 "즉시" PDF 파일을 출력하고 다양한 언어를 처리하기 위해 mPDF를 작성했습니다. HTML2FPDF와 같은 원본 스크립트보다 유니코드 글꼴을 사용할 때 속도가 느리고 더 큰 파일을 생성하지만 CSS 스타일 등을 지원하고 많은 개선 사항이 있습니다. RTL(아랍어, 히브리어), CJK(중국어, 일본어, 한국어)를 포함한 거의 모든 언어를 지원합니다. 중첩된 블록 수준 요소(예: P, DIV)를 지원합니다.

에디트플러스 중국어 크랙 버전
작은 크기, 구문 강조, 코드 프롬프트 기능을 지원하지 않음

드림위버 CS6
시각적 웹 개발 도구
