从Vue2源码看diff算法

简介: 从Vue2源码看diff算法

文章目录


基于Talk is cheap. Show me the code原则,本文会有大量的源码,不感兴趣的可以直接跳到答案区。

学习目标

通过看[Vue2.x](Vue_src_code/vue at master · csDeng/Vue_src_code (github.com))的源码, 学习Vnode以及diff算法,达到解决面试难题

学习过程

环境准备

  • 修改scripts命令
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",
  • yarn dev进行打包(注意安装rollup

查看Vnode长什么样

  1. src\core\vdom\vnode.js中的VNode类的constructor里面打印this,观察什么是Vnode,删减代码如下:
export default class VNode {
  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    console.log('Vnode class', this)
  }
  1. 重新打包
  2. 编写测试html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src='../dist/vue.js'></script>
</head>
<body>
    <div id = 'demo'>
        <h1>虚拟dom</h1>
        <p id='p1'>{{foo}}</p>
    </div>
    <script>
        const app = new Vue({
            el:'#demo',
            data:{
                foo:'foo'
            },
            mounted(){
                setTimeout(()=>{
                    this.foo = 'foooooooo'
                },3000)
            }
        })
    </script>
</body>
</html>
  1. 浏览器中打开,查看结果

  1. 打开其中一个节点看看具体结构

  1. 可以看到其实Vnode就是把我们的html标签转化成一个js对象.
  2. Vdom就是一个又一个Vnode构成的

测试Vue的批量异步更新的

  • 环境准备

注释掉前面在vnode里加的注释,重新打包,避免不必要的干扰

  • 测试html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src='../dist/vue.js'></script>
</head>
<body>
    <div id = 'demo'>
        <h1>异步更新</h1>
        <p id='p1'>{{foo}}</p>
    </div>
    <script>
        const app = new Vue({
            el:'#demo',
            data:{
                foo:'ready'
            },
            mounted(){
                /* 说明批量异步更新的代码*/
                setInterval(()=>{
                    this.foo = Math.random()
                    console.log('1', this.foo)
                    this.foo = Math.random()
                    console.log('2',this.foo)
                    this.foo = Math.random()
                    console.log('3', this.foo)
                    // 异步行为,此时内容没变
                    console.log(p1.innerHTML)
                    this.$nextTick(()=>{
                         console.log('$nextTick', p1.innerHTML)
                    })
                },3000)
            }
        })
    </script>
</body>
</html>
  • 浏览器打开观察结果

Diff算法

  • 源码位置src\core\vdom\patch.js
  • 进行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
    // 属性更新
    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)
    }
    // 判断是否是元素, 没有文本则是Element
    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)
    }
  }

总结一下

比较两个VNode, 包括三种操作: 属性更新、文本更新、子节点更新

具体规则:

  1. 新老节点均有子节点,则对子节点进行diff操作,调用updateChildren
  2. 如果老节点没有子节点而新节点有子节点,先清空老节点的文本内容,然后为其新增子节点
  3. 当新节点没有子节点而老节点有子节点的时候,则移除该节点的所有子节点
  4. 当新老节点都无子节点的时候,只是文本替换
  • updateChildren, 进行diff的具体细节,源码如下:
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)
    }
    /**
     * 两边向中间靠拢,dfs
     */
    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)) {
        // 新旧的开始节点相同,同时+1
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 新旧的结束节点相同, 同时-1
        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 {
        // 4种猜想都没有找到相同的,才被迫进行循环查找
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        // 查找在老的数组中的索引key
        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 {
            // 只有key相同,但是内容不相同
            // 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)
    }
  }
  • addVnodes批量增加节点的操作
function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {
    for (; startIdx <= endIdx; ++startIdx) {
      // 批量创建
      createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx)
    }
  }
  • removeVnodes批量删除旧的废弃节点
function removeVnodes (vnodes, startIdx, endIdx) {
    for (; startIdx <= endIdx; ++startIdx) {
      const ch = vnodes[startIdx]
      if (isDef(ch)) {
        if (isDef(ch.tag)) {
          removeAndInvokeRemoveHook(ch)
          invokeDestroyHook(ch)
        } else { // Text node
          removeNode(ch.elm)
        }
      }
    }
  }
  • 比较两个节点是否完全一致的源码
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)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

学习总结(从一道面试题进行总结)

你怎么看Vue中的Diff算法

  1. 必要性 , lifecycle.js里面的 mountComponent

组件内存在很多个data中的key使用

  1. 执行方式 patch.js 里面的 patchVnode

diff具体实现细节在patch.js里面的updateChildren(),执行的是两头比较

新旧的头头,尾尾, 旧头新尾, 新头旧尾,递归搜索是否是相同的虚拟节点,没有搜索到则遍历比较,从而实现两头向中间靠,最后批量更新和删除vnode,从而达到性能的优化。

  1. 整体策略

深度优先,同层比较


答案

  1. diff算法主要是通过新旧的虚拟dom的比对,将变化的地方更新在真实的dom
  2. vue2中为了降低Watcher粒度,每个组件只有一个Watcher与之对应,引入diff可以精确找到发生变化的地方
  3. Vue2中的diff执行的时刻是组件实例执行其更新函数时,他会比对上一次渲染的结果oldVnode与新的渲染结果newVnode,此过程被尤大成为patch
  4. diff的实现细节遵循深度优先,同层比较的策略,两个新旧节点根据自己是否有文本或者children分别进行头头,尾尾,旧头新尾,新头旧尾, 四个猜想进行比对,尝试找到相同的节点,然后递归搜索,实现两头向中间靠拢,最后批量更新中间不一致的部分,此时他还会搜索oldVnode的队列里面是否有可以重用的节点。如果没有找到相同的节点,则进行常规的遍历操作。
  5. 最后diff进行比对的时候,还使用到了key, key不一致则判断不一致,从而加快比对效率。
  6. 不知道我说的有没有不合理的地方,请你指点。(友情客串)
相关文章
|
26天前
|
机器学习/深度学习 前端开发 算法
婚恋交友系统平台 相亲交友平台系统 婚恋交友系统APP 婚恋系统源码 婚恋交友平台开发流程 婚恋交友系统架构设计 婚恋交友系统前端/后端开发 婚恋交友系统匹配推荐算法优化
婚恋交友系统平台通过线上互动帮助单身男女找到合适伴侣,提供用户注册、个人资料填写、匹配推荐、实时聊天、社区互动等功能。开发流程包括需求分析、技术选型、系统架构设计、功能实现、测试优化和上线运维。匹配推荐算法优化是核心,通过用户行为数据分析和机器学习提高匹配准确性。
82 3
|
2月前
|
搜索推荐 算法 C语言
【排序算法】八大排序(下)(c语言实现)(附源码)
本文继续学习并实现了八大排序算法中的后四种:堆排序、快速排序、归并排序和计数排序。详细介绍了每种排序算法的原理、步骤和代码实现,并通过测试数据展示了它们的性能表现。堆排序利用堆的特性进行排序,快速排序通过递归和多种划分方法实现高效排序,归并排序通过分治法将问题分解后再合并,计数排序则通过统计每个元素的出现次数实现非比较排序。最后,文章还对比了这些排序算法在处理一百万个整形数据时的运行时间,帮助读者了解不同算法的优劣。
163 7
|
2月前
|
搜索推荐 算法 C语言
【排序算法】八大排序(上)(c语言实现)(附源码)
本文介绍了四种常见的排序算法:冒泡排序、选择排序、插入排序和希尔排序。通过具体的代码实现和测试数据,详细解释了每种算法的工作原理和性能特点。冒泡排序通过不断交换相邻元素来排序,选择排序通过选择最小元素进行交换,插入排序通过逐步插入元素到已排序部分,而希尔排序则是插入排序的改进版,通过预排序使数据更接近有序,从而提高效率。文章最后总结了这四种算法的空间和时间复杂度,以及它们的稳定性。
135 8
|
3月前
|
算法 JavaScript UED
Diff 算法的实现原理
【10月更文挑战第18天】Diff 算法是 Vue.js 中实现高效 DOM 更新的核心机制,通过合理的比较和优化策略,能够在保证界面正确性的同时,最大程度地减少 DOM 操作,提高应用的性能和用户体验。
56 2
|
3月前
|
算法 JavaScript
Vue 中的 Diff 算法
【10月更文挑战第18天】需要注意的是,Diff 算法虽然能够提高性能,但在某些复杂的场景下,可能仍然会存在一些性能瓶颈。因此,在实际开发中,我们需要根据具体情况合理地使用 Diff 算法,并结合其他优化手段来提高应用的性能。
25 1
|
3月前
|
JavaScript 算法 前端开发
vue 中diff算法
【10月更文挑战第10天】
45 1
|
3月前
|
JavaScript 算法 前端开发
【VUE】Vue的diff算法和React的diff算法
【VUE】Vue的diff算法和React的diff算法
|
3月前
|
存储 算法 安全
ArrayList简介及使用全方位手把手教学(带源码),用ArrayList实现洗牌算法,3个人轮流拿牌(带全部源码)
文章全面介绍了Java中ArrayList的使用方法,包括其构造方法、常见操作、遍历方式、扩容机制,并展示了如何使用ArrayList实现洗牌算法的实例。
27 0
|
7天前
|
算法 数据安全/隐私保护
室内障碍物射线追踪算法matlab模拟仿真
### 简介 本项目展示了室内障碍物射线追踪算法在无线通信中的应用。通过Matlab 2022a实现,包含完整程序运行效果(无水印),支持增加发射点和室内墙壁设置。核心代码配有详细中文注释及操作视频。该算法基于几何光学原理,模拟信号在复杂室内环境中的传播路径与强度,涵盖场景建模、射线发射、传播及接收点场强计算等步骤,为无线网络规划提供重要依据。
|
8天前
|
机器学习/深度学习 数据采集 算法
基于GA遗传优化的CNN-GRU-SAM网络时间序列回归预测算法matlab仿真
本项目基于MATLAB2022a实现时间序列预测,采用CNN-GRU-SAM网络结构。卷积层提取局部特征,GRU层处理长期依赖,自注意力机制捕捉全局特征。完整代码含中文注释和操作视频,运行效果无水印展示。算法通过数据归一化、种群初始化、适应度计算、个体更新等步骤优化网络参数,最终输出预测结果。适用于金融市场、气象预报等领域。
基于GA遗传优化的CNN-GRU-SAM网络时间序列回归预测算法matlab仿真