vue2从数据变化到视图变化之diff算法图文详解

目录
  • 引言
    • 1、isUndef(oldStartVnode)
    • 2、isUndef(oldEndVnode)
    • 3、sameVnode(oldStartVnode, newStartVnode)
    • 4、sameVnode(oldEndVnode, newEndVnode)
    • 5、sameVnode(oldStartVnode, newEndVnode)
    • 6、sameVnode(oldEndVnode, newStartVnode)
    • 7、如果以上都不满足
  • 小结

引言

vue数据的渲染会引入视图的重新渲染。

从数据到视图的渲染流程可以移步https://www.jb51.net/article/261839.htm,那么从数据的变化到视图的变化是怎样的?

vue在数据的初始化阶段会进行响应式的处理defineReactive

/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }
  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

数据的变化会触发set方法,会让发布者dep执行 dep.notify,当vue所有的同步执行完后,在异步队列中按次序执行到vm的渲染流程,订阅者接收到发布者的通知后会执行到this.get(),指的是

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

vm._render()获取到vNode后,会执行vm._update视图的渲染:

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    // ...
    const prevVnode = vm._vnode
    // ...
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    // ...
  }

主要区别在于数据变化引起的视图变化有prevVnodevm.__patch__(prevVnode, vnode)之后会执行到patch方法:

function patch (oldVnode, vnode, hydrating, removeOnly) {
    // ...
    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        // ...
        // create new node
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )
        // ...
        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }

在数据变化引起的patch过程中isRealElement显然为false,新旧节点是否相同的另一个判断条件是sameVnode

function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

如果sameVnode(oldVnode, vnode)false,则执行createElm以及后续流程,该流程可以参考模板渲染的流程(请移步https://www.jb51.net/article/261850.htm )。

sameVnode(oldVnode, vnode)true的时候,执行到patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)

function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    // ...
    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 (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)
    }
  }

ch = vnode.childrenoldCh = oldVnode.children分别获取到新旧vnode的子元素,choldCh都存在时会执行到updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)

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)) { // 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)
    }
  }

这里定义了四个索引oldStartIdxnewStartIdxoldEndIdxnewEndIdx,也可以称之为指针,通过while循环,进行四个指针的移动:

1、isUndef(oldStartVnode)

如果oldStartVnode不存在,执行oldStartVnode = oldCh[++oldStartIdx],将oldStartIdx指针向右移动一位,进行下次循环。

2、isUndef(oldEndVnode)

如果oldEndVnode不存在,执行oldEndVnode = oldCh[--oldEndIdx],将oldEndIdx指针向左移动一位,进行下次循环。

3、sameVnode(oldStartVnode, newStartVnode)

如果满足sameVnode(oldStartVnode, newStartVnode),执行patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)开始递归执行,结束后oldStartIdxnewStartIdx分别向右移动一位。

4、sameVnode(oldEndVnode, newEndVnode)

如果满足sameVnode(oldEndVnode, newEndVnode),执行patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newStartIdx)开始递归执行,结束后oldEndIdxnewEndIdx分别向左移动一位。

5、sameVnode(oldStartVnode, newEndVnode)

如果满足sameVnode(oldStartVnode, newEndVnode),执行patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newStartIdx)开始递归执行,结束后oldStartVnode向右移动一位,newEndIdx向左移动一位。
并且通过nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))的方式将oldStartVnode.elm插入到oldEndVnode.elm节点之后。

6、sameVnode(oldEndVnode, newStartVnode)

如果满足sameVnode(oldEndVnode, newStartVnode),执行patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)开始递归执行,结束后newStartIdx向右移动一位,oldEndIdx向左移动一位。
并且通过nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)的方式将 oldEndVnode.elm插入到oldStartVnode.elm节点之前。

7、如果以上都不满足

如果新旧vNode首首、首尾、尾首和尾尾对比都没找到相同的,则在旧vNodeoldStartIdxoldEndIdx之间去找。 oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)创建以旧vNode的key为key值,位置索引为value的map映射:

function createKeyToOldIdx (children, beginIdx, endIdx) {
  let i, key
  const map = {}
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key
    if (isDef(key)) map[key] = i
  }
  return map
}

如果通过createKeyToOldIdx找不到,则通过findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)和旧vNode的方式去进行比对,并返回位置索引:

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
    }
}

通过oldKeyToIdx[newStartVnode.key]findIdxInOld (node, oldCh, start, end)的查询会有两种结果:

1、没找到如果没有找到,则以newStartVnode为渲染vNode通过createElm去进行节点的创建。

2、找到了如果找到了,通过vnodeToMove = oldCh[idxInOld]获取到介于oldStartIdxoldEndIdx之间的可以比对的vnode, 执行完patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)后将当前位置的oldCh[idxInOld] = undefined

通过nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)vnodeToMove.elm移动到oldStartVnode.elm之前。

小结

diff算法从两端进行比对,找不到再从中间寻找,是一种 “滑动窗口” 算法的使用,以达到通过节点移动来实现原地复用的目的。

以上就是vue2从数据变化到视图变化之diff算法图文详解的详细内容,更多关于vue2数据视图变化diff算法的资料请关注我们其它相关文章!

(0)

相关推荐

  • vue2从数据变化到视图变化之nextTick使用详解

    目录 引言 1.vue中nextTick的使用场景 2.vue中nextTick在哪里定义 3.vue中nextTick的实现原理 (1)callbacks的定义 (2)timerFunc的定义 (3)cb未传入的处理 4.vue中nextTick的任务分类 小结 引言 nextTick在vue中是一个很重要的方法,在new Vue实例化的同步过程中,将一些需要异步处理的函数推到异步队列中去,可以等new Vue所有的同步任务执行完后,再执行异步队列中的函数. 1.vue中nextTick的使用

  • vue2从数据变化到视图变化发布订阅模式详解

    目录 引言 一.发布订阅者模式的特点 二.vue中的发布订阅者模式 1.dep 2.Object.defineProperty 3.watcher 4.dep.depend 5.dep.notify 6.订阅者取消订阅 小结 引言 发布订阅者模式是最常见的模式之一,它是一种一对多的对应关系,当一个对象发生变化时会通知依赖他的对象,接受到通知的对象会根据情况执行自己的行为. 假设有财经报纸送报员financialDep,有报纸阅读爱好者a,b,c,那么a,b,c想订报纸就告诉financialDe

  • React Diff算法不采用Vue的双端对比原因详解

    目录 前言 React 官方的解析 Fiber 的结构 Fiber 链表的生成 React 的 Diff 算法 第一轮,常见情况的比对 第二轮,不常见的情况的比对 重点如何协调更新位置信息 小结 图文解释 React Diff 算法 最简单的 Diff 场景 复杂的 Diff 场景 Vue3 的 Diff 算法 第一轮,常见情况的比对 第二轮,复杂情况的比对 Vue2 的 Diff 算法 第一轮,简单情况的比对 第二轮,不常见的情况的比对 React.Vue3.Vue2 的 Diff 算法对比

  • vue中虚拟DOM与Diff算法知识精讲

    目录 前言 知识点: 虚拟DOM(Virtual DOM): 虚拟dom库 diff算法 snabbdom的核心 init函数 h函数 patch函数(核心) 题外话:diff算法简介 传统diff算法 snabbdom的diff算法优化 updateChildren(核中核:判断子节点的差异) 新结束节点和旧结束节点(情况2) 旧结束节点/新开始节点(情况4) 前言 面试官:"你了解虚拟DOM(Virtual DOM)跟Diff算法吗,请描述一下它们"; 我:"额,...鹅

  • vue3 diff 算法示例

    目录 一.可能性(常见): 二.找规律 三.算法优化 最长的递增子序列 完整代码 一.可能性(常见): 1. 旧的:a b c新的:a b c d 2. 旧的:  a b c新的:d a b c 3. 旧的:a b c d新的:a b c 4. 旧的:d a b c新的:  a b c 5. 旧的:a b c d e i f g新的:a b e c d h f g 对应的真实虚拟节点(为方便理解,文中用字母代替): // vnode对象 const a = { type: 'div', // 标

  • vue2从数据变化到视图变化之diff算法图文详解

    目录 引言 1.isUndef(oldStartVnode) 2.isUndef(oldEndVnode) 3.sameVnode(oldStartVnode, newStartVnode) 4.sameVnode(oldEndVnode, newEndVnode) 5.sameVnode(oldStartVnode, newEndVnode) 6.sameVnode(oldEndVnode, newStartVnode) 7.如果以上都不满足 小结 引言 vue数据的渲染会引入视图的重新渲染.

  • 微信小程序视图template模板引用的实例详解

    微信小程序视图template模板引用的实例详解 WXML 提供两种文件引用方式import和include. include可以将目标文件除了的整个代码引入,相当于是拷贝到include位置 temlate.wxml <template name="tmp_data" > <view class="content"> <!-- 头像 --> <view class="author-date"> &

  • MySQL的视图和索引用法与区别详解

    MySQL的视图 简单来说MySQL的视图就是对SELECT 命令的定义的一个快捷键,我们查询时会用到非常复杂的SELECT语句,而这个语句我们以后还会经常用到,我们可以经这个语句生产视图.视图是一个虚拟的表,它不存储数据,所用的数据都在真实的表中. 这样做的好处有: 1.防止有未经允许的租户访问到敏感数据 2.将多个物理表抽象成一个逻辑表 3.结果容易理解 4.获得数据更容易,很多人对SQL语句不太了解,我们可以通过创建视图的形式方便用户使用. 5.显示数据更容易. 6.维护程序更方便.调试视

  • Django视图层与模板层实例详解

    目录 theme: channing-cyan 网页伪静态 视图层 1.视图函数的返回值问题 2.视图函数返回json格式数据 3.form表单携带文件数据 4.FBV与CBV 5.CBV源码分析 模板层 1.模板语法传值 2.模板语法传值的范围 3.模板语法值过滤器 4.模板语法标签(类似于python中的流程控制) 5.自定义标签函数.过滤器.inclusion_tag 6.模板的继承 7.模板的导入 theme: channing-cyan 网页伪静态 将动态网页伪装成静态网页,可以提升网

  • 通达OA 使用Ajax和工作流插件实现根据人力资源系统数据增加OA账号(图文详解)

    本次小飞鱼开发的程序主要解决某下属公司在人力系统中增加账号不能马上审批完毕的问题,可以通过这个流程审批后由插件在后台判断自动增加OA账号,增加机制与hr与OA系统同步相同. 只进行增加操作,没有修改.删除的操作.原有已经进行了两个系统的数据自动同步开发,因此这次的开发属于一个补充的内容,仅在此提供一个应用的思路和开发过程的探讨. 前端发起人申请时填写hr系统中已经分配的工号,即可对应查询出其他相关数据.为了避免查出数据后对工号修改,增加一个确认工号输入框.其他信息由Ajax自动获取为只读形式.这

  • 改变vue请求过来的数据中的某一项值的方法(详解)

    由于 JavaScript 的限制, Vue 不能检测以下变动的数组: 当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue 当你修改数组的长度时,例如:vm.items.length = newLength 在 <template> <div> <ul> <li v-for = " (item,index) in list" v-text='`${item} - ${index} `'>&

  • 对pandas中iloc,loc取数据差别及按条件取值的方法详解

    Dataframe使用loc取某几行几列的数据: print(df.loc[0:4,['item_price_level','item_sales_level','item_collected_level','item_pv_level']]) 结果如下,取了index为0到4的五行四列数据. item_price_level item_sales_level item_collected_level item_pv_level 0 3 3 4 14 1 3 3 4 14 2 3 3 4 14

  • 微信小程序视图容器和基本内容组件图文详解

    目录 前言 一,视图容器类组件 1.1 view 1.2 srcoll-view 1.3 swiper和swiper-item 二,基本内容组件 2.1 text 2.2 rich-text 总结 前言 开发者可以通过运用组件快速搭建出页面结构,上一章也有对组件进行介绍,那么本文牛牛就来带大家学习小程序的组件. 我们可以将组件理解为微信内嵌的标签,它在小程序承担的作用与HTML的标签一致,不过组件的功能更加多样.具体. 事不宜迟,让我们开冲! 一,视图容器类组件 1.1 view 普通视图容器,

随机推荐