vue训练营4 --- vue中的diff算法

源码分析1

diff的必要性,src\core\instance\lifecycle.jslifecycle.js - mountComponent()
组件中可能存在很多个data中的key使用


源码分析2

diff的执行方式,src\core\vdom\patch.js - patchVnode()
patchVnodediff发生的地方,整体策略:深度优先,同层比较

源码分析3

diff的高效性,src\core\vdom\patch.js - updateChildren()

 

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------

 

源码分析1

diff的必要性,src\core\instance\lifecycle.jslifecycle.js - mountComponent()
组件中可能存在很多个data中的key使用

export function mountComponent (

  vm: Component,

  el: ?Element,

  hydrating?: boolean

): Component {

  vm.$el = el

  if (!vm.$options.render) {

    vm.$options.render = createEmptyVNode

    if (process.env.NODE_ENV !== 'production') {

      /* istanbul ignore if */

      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||

        vm.$options.el || el) {

        warn(

          'You are using the runtime-only build of Vue where the template ' +

          'compiler is not available. Either pre-compile the templates into ' +

          'render functions, or use the compiler-included build.',

          vm

        )

      } else {

        warn(

          'Failed to mount component: template or render function not defined.',

          vm

        )

      }

    }

  }

  callHook(vm, 'beforeMount')

 

  let updateComponent

  /* istanbul ignore if */

  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {

    updateComponent = () => {

      const name = vm._name

      const id = vm._uid

      const startTag = `vue-perf-start:${id}`

      const endTag = `vue-perf-end:${id}`

 

      mark(startTag)

      const vnode = vm._render()

      mark(endTag)

      measure(`vue ${name} render`, startTag, endTag)

 

      mark(startTag)

      vm._update(vnode, hydrating)

      mark(endTag)

      measure(`vue ${name} patch`, startTag, endTag)

    }

  } else {

    // 用户 $mount()时,定义 updateComponent

    updateComponent = () => {

      vm._update(vm._render(), hydrating)

    }

  }

 

  // we set this to vm._watcher inside the watcher's constructor

  // since the watcher's initial patch may call $forceUpdate (e.g. inside child

  // component's mounted hook), which relies on vm._watcher being already defined

 

  /*

      一个组件创建一次 Watcher, Wactcher的实例和组件的实例一一对应

      但是一个组件可能存在多个 data 中的key的使用,

      在更新时,为了确保知道是哪个 key 发生了变化,只能采用 diff 算法

      这也是 diff 存在的必要性

  */

  new Watcher(vm, updateComponent, noop, {

    before () {

      if (vm._isMounted && !vm._isDestroyed) {

        callHook(vm, 'beforeUpdate')

      }

    }

  }, true /* isRenderWatcher */)

  hydrating = false

 

  // manually mounted instance, call mounted on self

  // mounted is called for render-created child components in its inserted hook

  if (vm.$vnode == null) {

    vm._isMounted = true

    callHook(vm, 'mounted')

  }

  return vm

}


源码分析2

执行方式,src\core\vdom\patch.js - patchVnode()
patchVnodediff发生的地方,整体策略:深度优先,同层比较

// diff 算法

  function patchVnode (

    oldVnode,

    vnode,

    insertedVnodeQueue,

    ownerArray,

    index,

    removeOnly

  ) {

    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

 

    // 属性更新  <div style="color: blue">  ==>  <div style="color: red">

    if (isDef(data) && isPatchable(vnode)) {

      // cbs中关于属性更新的数组拿出来[attrFn, classFn, ...]

      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 (process.env.NODE_ENV !== 'production') {

          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)

    }

  }

 

源码分析3

高效性,src\core\vdom\patch.js - updateChildren()

// 重排算法

  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {

    // 4个指针

    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]

      } 

      // 接下去是新旧数组头尾比较4种情况

      // 新旧数组开头相同      

      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]

      } 

      // 上面4种猜想之后没有找到想同的,不得不开始循环查找

      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)

    }

  }

 

测试代码:

<body>

    <div id="demo">
        <h1>虚拟DOM</h1>
        <p>{{foo}}</p>

    </div>

    <script>

        // 创建实例

        const app = new Vue({
            el: '#demo',
            data: { foo: 'foo' },
            mounted() {
                setTimeout(() => {
                    this.foo = 'fooooo'                 }, 1000);

            }

        });

    </script>

</body>

 

结论:

1.diff算法是虚拟DOM技术的必然产物:通过新旧虚拟DOM作对比(即diff),将变化的地方更新在真 DOM上;另外,也需要diff高效的执行对比过程,从而降低时间复杂度为O(n)

2.vue 2.x中为了降低Watcher粒度,每个组件只有一个Watcher与之对应,只有引入diff才能精确找到 发生变化的地方。
3.vuediff执行的时刻是组件实例执行其更新函数时,它会比对上一次渲染结果oldVnode和新的渲染 结果newVnode,此过程称为patch

4.diff过程整体遵循深度优先、同层比较的策略;两个节点之间比较会根据它们是否拥有子节点或者文 本节点做不同操作;比较两组子节点是算法的重点,首先假设头尾节点可能相同做4次比对尝试,如果 没有找到相同节点才按照通用方式遍历查找,查找结束再按情况处理剩下的节点;借助key通常可以非 常精确找到相同节点,因此整个patch过程非常高效。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值