vue源码之虚拟dom与diff算法

虚拟dom

简介

  • 虚拟dom就是用js对象来描述一个dom节点
  • vue是数据驱动视图的,数据发生变化视图就要随之更新,在更新视图的操作中难免要操作dom,而操作真是dom又是非常耗费性能的,因为浏览器把dom设计的非常复杂
  • 所以我们要尽量少的操作dom,不要盲目的去更新视图,而是通过对比数据变化前后的状态,计算出视图中那些地方需要更新,只更新需要更新的地方
  • 通过js模拟一个dom节点,称为虚拟dom节点·,当数据发生变化时,对比变化前后的虚拟dom节点,通过diff算法计算出需要更新的地方,然后再去更新视图

接下来,我们就来看一下vue中如何实现虚拟dom

VNode类

虚拟dom就是通过这个类实例出来的,也就是说,我们可以通过这个类实例化不同的虚拟dom节点

虚拟dom节点其实就是一个对象,而这个类的作用就是在这个对象上添加各种属性,其源码如下:

export default class VNode {
  tag?: string
  data: VNodeData | undefined
  children?: Array<VNode> | null
  text?: string
  elm: Node | undefined
  ns?: string
  context?: Component // rendered in this component's scope
  key: string | number | undefined
  componentOptions?: VNodeComponentOptions
  componentInstance?: Component // component instance
  parent: VNode | undefined | null // 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 // async component factory function
  asyncMeta: Object | void
  isAsyncPlaceholder: boolean
  ssrContext?: Object | void
  fnContext: Component | void // real context vm for functional nodes
  fnOptions?: ComponentOptions | null // for SSR caching
  devtoolsMeta?: Object | null // used to store functional render context for devtools
  fnScopeId?: string | null // functional scope id support

  constructor(
    tag?: string,  // 当前节点的标签名
    data?: VNodeData,  // 当前节点对应的对象,包含了一些具体的一些数据对象
    children?: Array<VNode> | null,  // 当前节点的子节点组成的一个数组
    text?: string,  // 当前节点的文本
    elm?: Node,  // 当前虚拟节点对应的真实dom节点
    context?: Component,  // 当前组件节点对应的vue实例
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key  // 节点的key属性,被当做节点的标志,后续diff算法就是通过这个属性判断是否是同一个节点
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false  // /*是否为注释节点*/
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child(): Component | void {
    return this.componentInstance
  }
}

vnode类包含了虚拟dom需要的一些类属性,一下几个属性是比较重要的:

  • tag:属性的标签名
  • data:节点包含的信息,比如class类名等
  • children:节点包含的子节电,是一个数组
  • text:节点包含的文本属性
  • elm:虚拟节点对应的真实don节点

VNode类型

不同的真实dom对应不同的虚拟dom类型,并拥有不同的属性,其中,就有一下几种类型:

  • 注释节点
  • 文本节点
  • 元素节点
  • 组件节点
  • 函数式组件节点
  • 克隆节点

VNode的作用

我们在视图渲染之前,把写好的template模板先编译成VNode并缓存下来,等到数据发生变化页面需要重新渲染的时候,我们把数据发生变化后生成的VNode与前一次缓存下来的VNode进行对比,找出差异,然后有差异的VNode对应的真实DOM节点就是需要重新渲染的节点,最后根据有差异的VNode创建出真实的DOM节点再插入到视图中,最终完成一次视图更新。

vue中的diff算法

这个过程就是通过新旧vnode对比,找出差异所在

整体过程:

  • 首先,通过pathc方法比对,判断是否是同一个节点,如果不是同一个节点,就删除旧的节点,插入新的节点。
  • 如果新旧节点是同一个节点,就调用patchVnode方法,比对同一个节点
  • 在patchVnode中,判断新旧节点是否有children属性,没有的话过程就比较简单,有的话就要调用updateChildren方法
  • 在updateChildren方法中会进行diff算法的比对

接下来就来看看各个过程的实现

patch

path方法以新的vnode为基准,改造就的vnode节点使之和新的vnode节点一样,主要做一下内容:

  • 创建节点:新的VNode中有而旧的oldVNode中没有,就在旧的oldVNode中创建。
  • 删除节点:新的VNode中没有而旧的oldVNode中有,就从旧的oldVNode中删除。
  • 更新节点:新的VNode和旧的oldVNode中都有,就以新的VNode为准,更新旧的oldVNode

具体过程:

  • 先判断老节点是否是一个虚拟节点,如果不是的话,先要转化为虚拟节点,才能进行比对

  • 判断新老节点是否是同一个节点,判断的依据是新老节点的tag属性和key属性是否相同,如果相同就是同一个节点,不同则不是同一个节点

  • 如果是同一个节点,就要执行patchVnode方法进行同一个节点的比对

  • 如果不是同一个节点的话,就需要插入新的节点,并删除旧的节点

  • 简易代码实现

    export default function patch(oldVnode, newVnode) {
        // 判断传入的第一个参数,是DOM节点还是虚拟节点?
        if (oldVnode.sel == '' || oldVnode.sel == undefined) {
            // 传入的第一个参数是DOM节点,此时要包装为虚拟节点
            oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
        }
    
        // 判断oldVnode和newVnode是不是同一个节点
        if (oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) {
            console.log('是同一个节点');
            patchVnode(oldVnode, newVnode);
        } else {
            console.log('不是同一个节点,暴力插入新的,删除旧的');
            let newVnodeElm = createElement(newVnode);
            
            // 插入到老节点之前
            if (oldVnode.elm.parentNode && newVnodeElm) {
                oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);
            }
            // 删除老节点
            oldVnode.elm.parentNode.removeChild(oldVnode.elm);
        }
    };
    

在这里,有个createElement方法,是用来创建新节点的,要注意一点的是,如果新节点有children属性的话,还需要依次创建子节点,并插入到新节点中,具体代码如下:

// 真正创建节点。将vnode创建为DOM,是孤儿节点,不进行插入
export default function createElement(vnode) {
    // console.log('目的是把虚拟节点', vnode, '真正变为DOM');
    // 创建一个DOM节点,这个节点现在还是孤儿节点
    let domNode = document.createElement(vnode.sel);
    // 有子节点还是有文本??
    if (vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)) {
        // 它内部是文字
        domNode.innerText = vnode.text;
    } else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
        // 它内部是子节点,就要递归创建节点
        for (let i = 0; i < vnode.children.length; i++) {
            // 得到当前这个children
            let ch = vnode.children[i];
            // 创建出它的DOM,一旦调用createElement意味着:创建出DOM了,并且它的elm属性指向了创建出的DOM,但是还没有上树,是一个孤儿节点。
            let chDOM = createElement(ch);
            // 上树
            domNode.appendChild(chDOM);
        }
    }
    // 补充elm属性
    vnode.elm = domNode;
   
    // 返回elm,elm属性是一个纯DOM对象
    return vnode.elm;
};

patchVnode

当新旧节点是同一个节点的时候,就需要调用这个方法比对同一个节点

静态节点

静态节点就是只包含纯文字的节点,比如<p>123456</p>,这个节点没有包含任何可变的变量,也就是说,不管数据怎么变化,这个节点只要渲染后,以后就永远不会改变了,任何数据变化都与它无关。所以pathVnode方法在碰到静态节点的时候,会直接跳过,无需处理

patchVnode在比对新旧同一个节点的时候,会经过以下过程:

  • 如果新旧节点是同一个节点的话,直接返回

  • 如果vnode和oldvnode均为静态节点的话,直接返回,因为此时新旧节点是同一个不会改变的节点!

  • 如果vnode是文本节点,则判断oldvnode节点的text属性是否相等,如果不相等的话,则直接将oldvnode的内容更改为vnode的文本,这个时候就不需要知道oldvnode里面包含了什么了

  • 如果vnode是元素节点

    • vnode包含子节点

      • 如果oldvnode也包含子节点,就需要调用updateChildren方法进行diff算法的比对了
      • 如果oldVnode不包含子节点,则直接清空oldvnode节点的内容,再将vnode的子节点添加在oldvnode对应的真实dom节点上
    • vnode不包含子节点

      如果该节点不包含子节点,同时它又不是文本节点,那就说明该节点是个空节点,那就好办了,不管旧节点之前里面都有啥,直接清空即可。

  • 简易版代码如下:

    // 对比同一个虚拟节点
    export default function patchVnode(oldVnode, newVnode) {
        // 判断新旧vnode是否是同一个对象
        if (oldVnode === newVnode) return;
        // 判断新vnode有没有text属性
        if (newVnode.text != undefined && (newVnode.children == undefined || newVnode.children.length == 0)) {
            // 新vnode有text属性
            console.log('新vnode有text属性');
            if (newVnode.text != oldVnode.text) {
                // 如果新虚拟节点中的text和老的虚拟节点的text不同,那么直接让新的text写入老的elm中即可。如果老的elm中是children,那么也会立即消失掉。
                oldVnode.elm.innerText = newVnode.text;
            }
        } else {
            // 新vnode没有text属性,有children
            console.log('新vnode没有text属性');
            // 判断老的有没有children
            if (oldVnode.children != undefined && oldVnode.children.length > 0) {
                // 老的有children,新的也有children,此时就是最复杂的情况。
                updateChildren(oldVnode.elm, oldVnode.children, newVnode.children);
            } else {
                // 老的没有children,新的有children
                // 清空老的节点的内容
                oldVnode.elm.innerHTML = '';
                // 遍历新的vnode的子节点,创建DOM,上树
                for (let i = 0; i < newVnode.children.length; i++) {
                    let dom = createElement(newVnode.children[i]);
                    oldVnode.elm.appendChild(dom);
                }
            }
        }
    }
    

对应源码:

function patchVnode(
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly?: any
  ) {
    if (oldVnode === vnode) {
      return
    }

    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // clone reused vnode
      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
    }

    // 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.
    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)
    }
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch)
          updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        if (__DEV__) {
          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

在这个方法中,需要比对新旧节点的子节点,如果直接双层for循环判断新节点每一个子节点是否存在于旧节点的子节点中,效率就太低了,所以在vue中借鉴了diff算法进行比对,通过比较在特殊位置的新旧节点的子节点,如下:

  • 先把newChildren数组里的所有未处理子节点的第一个子节点和oldChildren数组里所有未处理子节点的第一个子节点做比对,如果相同,那就直接进入更新节点的操作;
  • 如果不同,再把newChildren数组里所有未处理子节点的最后一个子节点和oldChildren数组里所有未处理子节点的最后一个子节点做比对,如果相同,那就直接进入更新节点的操作;
  • 如果不同,再把newChildren数组里所有未处理子节点的最后一个子节点和oldChildren数组里所有未处理子节点的第一个子节点做比对,如果相同,那就直接进入更新节点的操作,更新完后再将oldChildren数组里的该节点移动到与newChildren数组里节点相同的位置;
  • 如果不同,再把newChildren数组里所有未处理子节点的第一个子节点和oldChildren数组里所有未处理子节点的最后一个子节点做比对,如果相同,那就直接进入更新节点的操作,更新完后再将oldChildren数组里的该节点移动到与newChildren数组里节点相同的位置;
  • 最后四种情况都试完如果还不同,那就按照之前循环的方式来查找节点。

为了达到上面所说的比对过程,需要定义四个指针分别指向newChildren和oldChildren的首尾位置,并通过上面四个规则去移动指针

  • newChildren数组里的所有未处理子节点的第一个子节点称为:新前;
  • newChildren数组里的所有未处理子节点的最后一个子节点称为:新后;
  • oldChildren数组里的所有未处理子节点的第一个子节点称为:旧前;
  • oldChildren数组里的所有未处理子节点的最后一个子节点称为:旧后;

上面的比对规则,可以归纳为以下四种比较,从上往下如果命中哪种规则后,对应的指针就会移动位置

  • 新前与旧前
  • 新后与旧后
  • 新后与旧前
  • 新前与旧后

接下来,就详细介绍比对的过程

  • 首先,定义四个指针指向新老children首尾位置,并定义四个变量存放对应位置的节点

  • 首先比对"新前与旧前"和"新后与旧后",如果命中了,对应指针变换位置,注意,命中之后,要调用patchVnode方法比对同一个节点

    else if (checkSameVnode(oldStartVnode, newStartVnode)) {
                // 新前和旧前
                console.log('新前和旧前命中');
                patchVnode(oldStartVnode, newStartVnode);
                oldStartVnode = oldCh[++oldStartIdx];
                newStartVnode = newCh[++newStartIdx];
            } else if (checkSameVnode(oldEndVnode, newEndVnode)) {
                // 新后和旧后
                console.log('新后和旧后命中');
                patchVnode(oldEndVnode, newEndVnode);
                oldEndVnode = oldCh[--oldEndIdx];
                newEndVnode = newCh[--newEndIdx];
            } 
    
  • 如果前两种都没有命中的话,就要判断"新后与旧前","新前与旧后"了,如果这两种命中的话,就证明元素的位置发生了变化,需要移动对应oldchildren节点位置,具体移动位置如下:

    • 如果命中"新后与旧前"的话,证明元素得往后移动了,注意,移动的时候要以未处理节点为基准,所以要把节点移动到旧后对应节点(此时还未处理该节点)的后面
    • 如果命中"新前与旧后"的话,证明元素被提前了,此时需要将元素移动到新后对应节点的前面
     else if (checkSameVnode(oldStartVnode, newEndVnode)) {
                // 新后和旧前
                console.log('新后和旧前命中');
                patchVnode(oldStartVnode, newEndVnode);
                // 当③新后与旧前命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧后的后面
                // 如何移动节点??只要你插入一个已经在DOM树上的节点,它就会被移动
                parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
                oldStartVnode = oldCh[++oldStartIdx];
                newEndVnode = newCh[--newEndIdx];
            } else if (checkSameVnode(oldEndVnode, newStartVnode)) {
                // 新前和旧后
                console.log('新前和旧后命中');
                patchVnode(oldEndVnode, newStartVnode);
                // 当④新前和旧后命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧前的前面
                parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
                // 如何移动节点??只要你插入一个已经在DOM树上的节点,它就会被移动
                oldEndVnode = oldCh[--oldEndIdx];
                newStartVnode = newCh[++newStartIdx];
            }
    
  • 如果四种规则都没有命中的话,就要遍历oldchildren,判断新前对应的节点是否存在于oldchildren中,这里可以利用哈希表,就不用每次都遍历一遍了

    • 如果找到的话,就插入到旧前对应节点前面,同时指针移动位置,注意此时需要将oldchildren对应子节点的元素设置为undefined,表示该节点已经处理过了
    • 如果没有找到的话,创建节点并插入
    else {
                // 四种命中都没有命中
                // 制作keyMap一个映射对象,这样就不用每次都遍历老对象了。
                if (!keyMap) {
                    keyMap = {};
                    // 从oldStartIdx开始,到oldEndIdx结束,创建keyMap映射对象
                    for (let i = oldStartIdx; i <= oldEndIdx; i++) {
                        const key = oldCh[i].key;
                        if (key != undefined) {
                            keyMap[key] = i;
                        }
                    }
                }
                console.log(keyMap);
                // 寻找当前这项(newStartIdx)这项在keyMap中的映射的位置序号
                const idxInOld = keyMap[newStartVnode.key];
                console.log(idxInOld);
                if (idxInOld == undefined) {
                    // 判断,如果idxInOld是undefined表示它是全新的项
                    // 被加入的项(就是newStartVnode这项)现不是真正的DOM节点
                    parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm);
                } else {
                    // 如果不是undefined,不是全新的项,而是要移动
                    const elmToMove = oldCh[idxInOld];
                    patchVnode(elmToMove, newStartVnode);
                    // 把这项设置为undefined,表示我已经处理完这项了
                    oldCh[idxInOld] = undefined;
                    // 移动,调用insertBefore也可以实现移动。
                    parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);
                }
                // 指针下移,只移动新的头
                newStartVnode = newCh[++newStartIdx];
            }
    
  • 基于上一种没有命中规则的情况可能出现oldchildren元素为undefined,所以需要首先就判断元素是否为undefined,是的话直接移动指针位置

  • 循环执行上面的操作,直到新前位置大于旧前或者旧前位置大于旧后位置,表示某一个数组已经全部遍历一遍了

  • 接着判断是由于哪个条件不成立退出循环了,也就是哪一个数组还有元素还没有遍历,如果oldchildren还有元素的话,证明这些元素是新的vnode中没有的子节点,所以需要依次删除;如果newchildren还有元素,则证明新的vnode增加了新的元素,也需要依次插入对应位置,此时要插入旧前对应节点的前面

    注意,这里要插入到旧前节点的前面,原因是:此时旧后在旧前的前面(退出循环的条件),也就是包括旧后与旧前这些元素的位置都确定了,所以插入只能在旧前与旧后之间,因为是从新前位置开始遍历还没有插入的子节点,如果一次从旧后节点插入的话,新插入元素位置会发生变化(逆序),而依次插入到旧前的前面就不会

     // 继续看看有没有剩余的。循环结束了start还是比old小
        if (newStartIdx <= newEndIdx) {
            console.log('new还有剩余节点没有处理,要加项。要把所有剩余的节点,都要插入到oldStartIdx之前');
            // 遍历新的newCh,添加到老的没有处理的之前
            for (let i = newStartIdx; i <= newEndIdx; i++) {
         // insertBefore方法可以自动识别null,如果是null就会自动排到队尾去。和appendChild是一致了。
                // newCh[i]现在还没有真正的DOM,所以要调用createElement()函数变为DOM
                parentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIdx].elm);
            }
        } else if (oldStartIdx <= oldEndIdx) {
            console.log('old还有剩余节点没有处理,要删除项');
            // 批量删除oldStart和oldEnd指针之间的项
            for (let i = oldStartIdx; i <= oldEndIdx; i++) {
                if (oldCh[i]) {
                    parentElm.removeChild(oldCh[i].elm);
                }
            }
        }
    
  • 对应源码如下:

 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 (__DEV__) {
      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)) {
        // Vnode moved right
        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)) {
        // Vnode moved left
        patchVnode(
          oldEndVnode,
          newStartVnode,
          insertedVnodeQueue,
          newCh,
          newStartIdx
        )
        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,
              newCh,
              newStartIdx
            )
            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(oldCh, oldStartIdx, oldEndIdx)
    }
  }

自此,虚拟dom与diff算法就写完了,还有很多不足的地方,后面会继续完善!!

参考链接:

https://vue-js.com/learn-vue/virtualDOM/

https://www.bilibili.com/video/BV1iX4y1K72v?spm_id_from=333.1007.top_right_bar_window_view_later.content.click&vd_source=8bcb7d684da1204ddf081fb53feb2a84

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值