Vue 2.5에서 Diff 알고리즘을 사용하는 방법

亚连
풀어 주다: 2018-06-23 17:00:03
원래의
1347명이 탐색했습니다.

이 기사에서는 Vue 2.5.3 버전에서 사용되는 Virtual Dom을 분석합니다. updataChildren은 Diff 알고리즘의 핵심이므로 이 기사에서는 updataChildren에 대한 그래픽 분석을 수행합니다. 이 기사를 통해 Vue 2.5의 Diff 알고리즘을 공유하겠습니다. 필요한 친구는 이를 참조할 수 있습니다

DOM은 "본질적으로 느리기" 때문에 모든 주요 프런트엔드 프레임워크는 DOM 작업을 최적화하는 방법을 제공합니다. 확인하면서 React가 먼저 Virtual Dom을 제안했고 Vue2.0도 React와 유사한 Virtual Dom을 추가했습니다.

이 기사에서는 Vue 2.5.3 버전에서 사용되는 Virtual Dom을 분석합니다.

updataChildren은 Diff 알고리즘의 핵심이므로 이 글에서는 updataChildren의 그래픽 분석을 수행합니다.

1.VNode object

A VNode 인스턴스에는 다음 속성이 포함되어 있습니다. 코드의 이 부분은 src/core/vdom/vnode.js

export default class VNode {
 tag: string | void;
 data: VNodeData | void;
 children: ?Array<VNode>;
 text: string | void;
 elm: Node | void;
 ns: string | void;
 context: Component | void; // rendered in this component&#39;s scope
 key: string | number | void;
 componentOptions: VNodeComponentOptions | void;
 componentInstance: Component | void; // component instance
 parent: VNode | void; // component placeholder node

 // strictly internal
 raw: boolean; // contains raw HTML? (server only)
 isStatic: boolean; // hoisted static node
 isRootInsert: boolean; // necessary for enter transition check
 isComment: boolean; // empty comment placeholder?
 isCloned: boolean; // is a cloned node?
 isOnce: boolean; // is a v-once node?
 asyncFactory: Function | void; // async component factory function
 asyncMeta: Object | void;
 isAsyncPlaceholder: boolean;
 ssrContext: Object | void;
 functionalContext: Component | void; // real context vm for functional nodes
 functionalOptions: ?ComponentOptions; // for SSR caching
 functionalScopeId: ?string; // functioanl scope id support
로그인 후 복사
  • tag에 있습니다. 현재 노드의 태그 이름입니다.

  • data: 현재 노드의 데이터 개체입니다. 특정 필드는 vue 소스 코드 유형/vnode.d.ts의 VNodeData 정의를 참조하세요.

  • children: 하위 노드를 포함한 배열 유형입니다. of the current node

  • text : 현재 노드의 텍스트, 일반적으로 텍스트 노드 또는 주석 노드는 이 속성을 갖습니다

  • elm: 현재 가상 노드에 해당하는 실제 dom 노드

  • ns: 노드의 네임스페이스

  • context: 컴파일 범위

  • functionContext: 기능적 구성 요소의 범위

  • key: 노드의 키 속성으로, 노드의 식별자로 사용되며 패치 최적화에 도움이 됩니다.

  • comComponentOptions: 컴포넌트 인스턴스 생성 시 사용되는 옵션 정보当前Child: 현재 노드에 해당하는 컴포넌트 인스턴스

  • parent: 컴포넌트의 위치 노드

  • Raw: raw html

  • ISSTATIC: 로고 정적 노드의 노드 삽입, isComment: 현재 노드가 주석 노드인지 여부

  • isCloned: 현재 노드가 복제 노드인지 여부

  • isOnce: 현재 노드에 v-once 명령이 있는지 여부

  • 2. VNode 분류
  • VNode는 VueVirtual Dom의 기본 클래스로 이해될 수 있습니다. VNode 생성자를 통해 생성된 VNnode 인스턴스는 다음 범주 중 하나일 수 있습니다.
  • EmptyVNode: 콘텐츠가 없는 주석 노드

TextVNode: 텍스트 노드

ElementVNode: 일반 요소 노드

  • ComponentVNode: 구성 요소 노드

  • CloneVNode: 위의 노드 유형 중 하나일 수 있다는 점만 다릅니다. isCloned 속성이 true입니다

  • 3 .Create-Element 소스 코드 분석
  • 코드의 이 부분은 src/core/vdom/create-element.js에 있습니다. 코드를 붙여넣고 설명을 추가하면 됩니다.
  • export function createElement (
     context: Component,
     tag: any,
     data: any,
     children: any,
     normalizationType: any,
     alwaysNormalize: boolean
    ): VNode {
     // 兼容不传data的情况
     if (Array.isArray(data) || isPrimitive(data)) {
     normalizationType = children
     children = data
     data = undefined
     }
     // 如果alwaysNormalize是true
     // 那么normalizationType应该设置为常量ALWAYS_NORMALIZE的值
     if (isTrue(alwaysNormalize)) {
     normalizationType = ALWAYS_NORMALIZE
     }
     // 调用_createElement创建虚拟节点
     return _createElement(context, tag, data, children, normalizationType)
    }
    
    export function _createElement (
     context: Component,
     tag?: string | Class<Component> | Function | Object,
     data?: VNodeData,
     children?: any,
     normalizationType?: number
    ): VNode {
    
     /**
     * 如果存在data.__ob__,说明data是被Observer观察的数据
     * 不能用作虚拟节点的data
     * 需要抛出警告,并返回一个空节点
     *
     * 被监控的data不能被用作vnode渲染的数据的原因是:
     * data在vnode渲染过程中可能会被改变,这样会触发监控,导致不符合预期的操作
     */
     if (isDef(data) && isDef((data: any).__ob__)) {
     process.env.NODE_ENV !== &#39;production&#39; && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
      &#39;Always create fresh vnode data objects in each render!&#39;,
      context
     )
     return createEmptyVNode()
     }
     // object syntax in v-bind
     if (isDef(data) && isDef(data.is)) {
     tag = data.is
     }
     if (!tag) {
     // 当组件的is属性被设置为一个falsy的值
     // Vue将不会知道要把这个组件渲染成什么
     // 所以渲染一个空节点
     // in case of component :is set to falsy value
     return createEmptyVNode()
     }
     // key为非原始值警告
     // warn against non-primitive key
     if (process.env.NODE_ENV !== &#39;production&#39; &&
     isDef(data) && isDef(data.key) && !isPrimitive(data.key)
     ) {
     warn(
      &#39;Avoid using non-primitive value as key, &#39; +
      &#39;use string/number value instead.&#39;,
      context
     )
     }
     // 作用域插槽
     // support single function children as default scoped slot
     if (Array.isArray(children) &&
     typeof children[0] === &#39;function&#39;
     ) {
     data = data || {}
     data.scopedSlots = { default: children[0] }
     children.length = 0
     }
     // 根据normalizationType的值,选择不同的处理方法
     if (normalizationType === ALWAYS_NORMALIZE) {
     children = normalizeChildren(children)
     } else if (normalizationType === SIMPLE_NORMALIZE) {
     children = simpleNormalizeChildren(children)
     }
     let vnode, ns
     // 如果标签名是字符串类型
     if (typeof tag === &#39;string&#39;) {
     let Ctor
     // 获取标签的命名空间
     ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
     // 如果是保留标签
     if (config.isReservedTag(tag)) {
      // platform built-in elements
      // 就创建这样一个vnode
      vnode = new VNode(
      config.parsePlatformTagName(tag), data, children,
      undefined, undefined, context
      )
      // 如果不是保留字标签,尝试从vm的components上查找是否有这个标签的定义
     } else if (isDef(Ctor = resolveAsset(context.$options, &#39;components&#39;, tag))) {
      // component
      // 如果找到,就创建虚拟组件节点
      vnode = createComponent(Ctor, data, context, children, tag)
     } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      // 兜底方案,创建一个正常的vnode
      vnode = new VNode(
      tag, data, children,
      undefined, undefined, context
      )
     }
     } else {
     // 当tag不是字符串的时候,我们认为tag是组件的构造类
     // 所以直接创建
     // direct component options / constructor
     vnode = createComponent(tag, data, context, children)
     }
     if (isDef(vnode)) {
     // 应用命名空间
     if (ns) applyNS(vnode, ns)
     return vnode
     } else {
     // 返回一个空节点
     return createEmptyVNode()
     }
    }
    function applyNS (vnode, ns, force) {
     vnode.ns = ns
     if (vnode.tag === &#39;foreignObject&#39;) {
     // use default namespace inside foreignObject
     ns = undefined
     force = true
     }
     if (isDef(vnode.children)) {
     for (let i = 0, l = vnode.children.length; i < l; i++) {
      const child = vnode.children[i]
      if (isDef(child.tag) && (isUndef(child.ns) || isTrue(force))) {
      applyNS(child, ns, force)
      }
     }
     }
    }
    로그인 후 복사

  • 4. 패치 원칙

패치 함수는 src/core/vdom/patch.js에 정의되어 있으며 비교적 간단하며 코드가 필요하지 않습니다.

패치 함수는 6개의 매개변수를 받습니다. :

oldVnode: 이전 가상 노드 또는 이전 Real dom 노드

vnode: 새 가상 노드

hydrating: 실제 dom과 혼합할지 여부

  • removeOnly:

  • 에 대한 특수 플래그
  • parentElm: 상위 노드

  • refElm: 새 노드는 refElm

  • 패치 논리는 다음과 같습니다.
  • vnode가 존재하지 않지만 oldVnode가 존재하는 경우, 이전 노드를 파괴하려는 의도를 나타냅니다. 그런 다음 InvokeDestroyHook(oldVnode)를 호출하여 이를 파괴합니다

  • oldVnode가 존재하지 않지만 vnode가 존재하는 경우(새 노드를 생성하려는 의도임을 나타냄) 그런 다음 createElm을 호출하여 새 노드를 생성합니다
  • else vnode와 oldVnode가 모두 존재하는 경우

  • oldVnode와 vnode가 동일한 노드인 경우 patchVnode를 호출하여 패치합니다.

vnode와 oldVnode가 동일한 노드가 아닌 경우 oldVnode가 실제 DOM 노드이거나 하이드레이팅이 true로 설정된 경우 하이드레이트 함수를 사용하여 가상을 매핑해야 합니다. dom과 실제 dom을 연결한 후 해당 가상 dom에 oldVnode를 설정하여 oldVnode를 찾습니다. elm의 부모 노드는 vnode를 기반으로 실제 dom 노드를 생성하고 이를 부모 노드의 oldVnode.elm

에 삽입합니다. patchVnode의 논리는 다음과 같습니다.

1. oldVnode가 vnode와 완전히 일치하면 아무 것도 할 필요가 없습니다.

2.oldVnode와 vnode가 모두 정적 노드이고 동일한 키를 갖고 vnode가 복제 노드인 경우 또는 v-once 명령으로 제어되는 노드의 경우 oldVnode.elm 및 oldVnode.child를 모두 vnode에 복사하기만 하면 되며 다른 작업은 필요하지 않습니다.

3 그렇지 않은 경우 vnode가 텍스트 노드 또는 주석이 아닙니다. node

oldVnode와 vnode 모두 하위 노드가 있고 두 당사자의 하위 노드가 완전히 일치하지 않는 경우 updateChildren

을 실행합니다.

oldVnode에만 하위 노드가 있는 경우 해당 노드를 삭제합니다

만약 vnode에 자식 노드가 있으면 이러한 자식 노드를 만듭니다

  • oldVnode나 vnode에 자식 노드가 없지만 oldVnode가 텍스트 노드이거나 주석 노드인 경우 vnode.elm의 텍스트를 빈 문자열로 설정하세요

4.如果vnode是文本节点或注释节点,但是vnode.text != oldVnode.text时,只需要更新vnode.elm的文本内容就可以

代码如下:

 function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
 // 如果新旧节点一致,什么都不做
 if (oldVnode === vnode) {
  return
 }
 // 让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化
 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
 }
 // reuse element for static trees.
 // note we only do this if the vnode is cloned -
 // if the new node is not cloned it means the render functions have been
 // reset by the hot-reload-api and we need to do a proper re-render.
 // 如果新旧都是静态节点,并且具有相同的key
 // 当vnode是克隆节点或是v-once指令控制的节点时,只需要把oldVnode.elm和oldVnode.child都复制到vnode上
 // 也不用再有其他操作
 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)) {
  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)
 }
 // 如果vnode不是文本节点或者注释节点
 if (isUndef(vnode.text)) {
  // 并且都有子节点
  if (isDef(oldCh) && isDef(ch)) {
  // 并且子节点不完全一致,则调用updateChildren
  if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
  // 如果只有新的vnode有子节点
  } else if (isDef(ch)) {
  if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, &#39;&#39;)
  // elm已经引用了老的dom节点,在老的dom节点上添加子节点
  addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
  // 如果新vnode没有子节点,而vnode有子节点,直接删除老的oldCh
  } else if (isDef(oldCh)) {
  removeVnodes(elm, oldCh, 0, oldCh.length - 1)
  // 如果老节点是文本节点
  } else if (isDef(oldVnode.text)) {
  nodeOps.setTextContent(elm, &#39;&#39;)
  }
  // 如果新vnode和老vnode是文本节点或注释节点
  // 但是vnode.text != oldVnode.text时,只需要更新vnode.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)
 }
 }
로그인 후 복사

5.updataChildren原理

updateChildren的逻辑是:

分别获取oldVnode和vnode的firstChild、lastChild,赋值给oldStartVnode、oldEndVnode、newStartVnode、newEndVnode

如果oldStartVnode和newStartVnode是同一节点,调用patchVnode进行patch,然后将oldStartVnode和newStartVnode都设置为下一个子节点,

如果oldEndVnode和newEndVnode是同一节点,调用patchVnode进行patch,然后将oldEndVnode和newEndVnode都设置为上一个子节点,重复上述流程

如果oldStartVnode和newEndVnode是同一节点,调用patchVnode进行patch,如果removeOnly是false,那么可以把oldStartVnode.elm移动到oldEndVnode.elm之后,然后把oldStartVnode设置为下一个节点,newEndVnode设置为上一个节点,重复上述流程

如果newStartVnode和oldEndVnode是同一节点,调用patchVnode进行patch,如果removeOnly是false,那么可以把oldEndVnode.elm移动到oldStartVnode.elm之前,然后把newStartVnode设置为下一个节点,oldEndVnode设置为上一个节点,重复上述流程

如果以上都不匹配,就尝试在oldChildren中寻找跟newStartVnode具有相同key的节点,如果找不到相同key的节点,说明newStartVnode是一个新节点,就创建一个,然后把newStartVnode设置为下一个节点

如果上一步找到了跟newStartVnode相同key的节点,那么通过其他属性的比较来判断这2个节点是否是同一个节点,如果是,就调用patchVnode进行patch,如果removeOnly是false,就把newStartVnode.elm插入到oldStartVnode.elm之前,把newStartVnode设置为下一个节点,重复上述流程

如果在oldChildren中没有寻找到newStartVnode的同一节点,那就创建一个新节点,把newStartVnode设置为下一个节点,重复上述流程

如果oldStartVnode跟oldEndVnode重合了,并且newStartVnode跟newEndVnode也重合了,这个循环就结束了

具体代码如下:

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
 let oldStartIdx = 0 // 旧头索引
 let newStartIdx = 0 // 新头索引
 let oldEndIdx = oldCh.length - 1 // 旧尾索引
 let newEndIdx = newCh.length - 1 // 新尾索引
 let oldStartVnode = oldCh[0] // oldVnode的第一个child
 let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最后一个child
 let newStartVnode = newCh[0] // newVnode的第一个child
 let newEndVnode = newCh[newEndIdx] // newVnode的最后一个child
 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
 // 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,证明diff完了,循环结束
 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  // 如果oldVnode的第一个child不存在
  if (isUndef(oldStartVnode)) {
  // oldStart索引右移
  oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
  // 如果oldVnode的最后一个child不存在
  } else if (isUndef(oldEndVnode)) {
  // oldEnd索引左移
  oldEndVnode = oldCh[--oldEndIdx]
  // oldStartVnode和newStartVnode是同一个节点
  } else if (sameVnode(oldStartVnode, newStartVnode)) {
  // patch oldStartVnode和newStartVnode, 索引左移,继续循环
  patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
  oldStartVnode = oldCh[++oldStartIdx]
  newStartVnode = newCh[++newStartIdx]
  // oldEndVnode和newEndVnode是同一个节点
  } else if (sameVnode(oldEndVnode, newEndVnode)) {
  // patch oldEndVnode和newEndVnode,索引右移,继续循环
  patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
  oldEndVnode = oldCh[--oldEndIdx]
  newEndVnode = newCh[--newEndIdx]
  // oldStartVnode和newEndVnode是同一个节点
  } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
  // patch oldStartVnode和newEndVnode
  patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
  // 如果removeOnly是false,则将oldStartVnode.eml移动到oldEndVnode.elm之后
  canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
  // oldStart索引右移,newEnd索引左移
  oldStartVnode = oldCh[++oldStartIdx]
  newEndVnode = newCh[--newEndIdx]
  // 如果oldEndVnode和newStartVnode是同一个节点
  } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
  // patch oldEndVnode和newStartVnode
  patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
  // 如果removeOnly是false,则将oldEndVnode.elm移动到oldStartVnode.elm之前
  canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
  // oldEnd索引左移,newStart索引右移
  oldEndVnode = oldCh[--oldEndIdx]
  newStartVnode = newCh[++newStartIdx]
  // 如果都不匹配
  } else {
  if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
  // 尝试在oldChildren中寻找和newStartVnode的具有相同的key的Vnode
  idxInOld = isDef(newStartVnode.key)
   ? oldKeyToIdx[newStartVnode.key]
   : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
  // 如果未找到,说明newStartVnode是一个新的节点
  if (isUndef(idxInOld)) { // New element
   // 创建一个新Vnode
   createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
  // 如果找到了和newStartVnodej具有相同的key的Vnode,叫vnodeToMove
  } else {
   vnodeToMove = oldCh[idxInOld]
   /* istanbul ignore if */
   if (process.env.NODE_ENV !== &#39;production&#39; && !vnodeToMove) {
   warn(
    &#39;It seems there are duplicate keys that is causing an update error. &#39; +
    &#39;Make sure each v-for item has a unique key.&#39;
   )
   }
   // 比较两个具有相同的key的新节点是否是同一个节点
   //不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。
   if (sameVnode(vnodeToMove, newStartVnode)) {
   // patch vnodeToMove和newStartVnode
   patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
   // 清除
   oldCh[idxInOld] = undefined
   // 如果removeOnly是false,则将找到的和newStartVnodej具有相同的key的Vnode,叫vnodeToMove.elm
   // 移动到oldStartVnode.elm之前
   canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
   // 如果key相同,但是节点不相同,则创建一个新的节点
   } else {
   // same key but different element. treat as new element
   createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
   }
  }
  // 右移
  newStartVnode = newCh[++newStartIdx]
  }
 }
로그인 후 복사

6.具体的Diff分析

不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。

diff的遍历过程中,只要是对dom进行的操作都调用api.insertBefore,api.insertBefore只是原生insertBefore的简单封装。

比较分为两种,一种是有vnode.key的,一种是没有的。但这两种比较对真实dom的操作是一致的。

对于与sameVnode(oldStartVnode, newStartVnode)和sameVnode(oldEndVnode,newEndVnode)为true的情况,不需要对dom进行移动。

总结遍历过程,有3种dom操作:上述图中都有

1.当oldStartVnode,newEndVnode值得比较,说明oldStartVnode.el跑到oldEndVnode.el的后边了。

2.当oldEndVnode,newStartVnode值得比较,oldEndVnode.el跑到了oldStartVnode.el的前边,准确的说应该是oldEndVnode.el需要移动到oldStartVnode.el的前边”。

3.newCh中的节点oldCh里没有, 将新节点插入到oldStartVnode.el的前边

在结束时,分为两种情况:

1.oldStartIdx > oldEndIdx,可以认为oldCh先遍历完。当然也有可能newCh此时也正好完成了遍历,统一都归为此类。此时newStartIdx和newEndIdx之间的vnode是新增的,调用addVnodes,把他们全部插进before的后边,before很多时候是为null的。addVnodes调用的是insertBefore操作dom节点,我们看看insertBefore的文档:parentElement.insertBefore(newElement, referenceElement)

如果referenceElement为null则newElement将被插入到子节点的末尾。如果newElement已经在DOM树中,newElement首先会从DOM树中移除。所以before为null,newElement将被插入到子节点的末尾。

2.newStartIdx > newEndIdx이면 newCh가 먼저 탐색된다고 볼 수 있습니다. 현재로서는 oldStartIdx와 oldEndIdx 사이의 vnode가 더 이상 새 하위 노드에 존재하지 않습니다. DOM에서 삭제하려면 deleteVnodes를 호출하세요. 위의 내용은 앞으로 모든 사람에게 도움이 되기를 바랍니다.

관련 기사:

Chrome Firefox에는 디버깅 도구가 함께 제공됩니다(자세한 튜토리얼)

Vue.js가 무한 스크롤 로딩을 구현하는 방법에 대해

Angular를 사용하여 테이블 필터링을 구현하는 방법

구현 방법 JavaScript 계산기 사용하기

위 내용은 Vue 2.5에서 Diff 알고리즘을 사용하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿
회사 소개 부인 성명 Sitemap
PHP 중국어 웹사이트:공공복지 온라인 PHP 교육,PHP 학습자의 빠른 성장을 도와주세요!