XingYun blog
  • JS基础

    • 图解js原型链
    • JS Event Loop
    • 对象的底层数据结构
    • 让你的JavaScript代码简单又高效
    • 函数参数按值传递
    • 判断数据类型
    • 浮点数精度问题和解决办法
    • 常用方法snippet
    • 实现Promise
    • 防抖和节流
    • 巧用sort排序
  • CSS && HTML

    • CSS也需要性能优化
    • class命名规范
    • em、px、rem、vh、vw 区别
    • CSS揭秘阅读笔记
  • 浏览器

    • 浏览器是如何渲染页面的
    • 重排和重绘
    • BOM浏览器对象模型
    • DOM事件
    • 浏览器存储
  • 数据结构

    • JS实现链表
    • JS实现栈与栈应用
    • JS实现常见排序
    • 哈夫曼编码
    • MD5算法
  • vue原理浅析

    • Vue虚拟dom与Diff算法
    • 前端打包文件的缓存机制
    • vue数组为什么不是响应式
    • v-for为什么不能用index做key
  • 前端工程化

    • 浏览器是如何渲染页面的
    • 前端打包需要gzip压缩吗
    • 前端打包文件的缓存机制
    • webpack loader和plugin
  • 轮子&&组件库

    • 实现水波浪进度球
  • 文字转语音mp3文件
  • 文件上传前后端实现
  • moment.js给定时间获取自然月、周的时间轴
  • 实现文件上传功能
  • 批量下载照片
  • leaflet改变坐标原点
  • 网络

    • 有了MAC地址 为什么还需要IP地址
    • 为什么IP地址老是变
    • 我们为什么需要IPV6
    • TCP与UDP
  • 计算机组成原理

    • ASCII、Unicode、UTF-8和UTF-16
  • VSCode

    • VSCode图片预览插件 Image preview
    • rsync:linux间的高效传输工具

XingYun

冲!
  • JS基础

    • 图解js原型链
    • JS Event Loop
    • 对象的底层数据结构
    • 让你的JavaScript代码简单又高效
    • 函数参数按值传递
    • 判断数据类型
    • 浮点数精度问题和解决办法
    • 常用方法snippet
    • 实现Promise
    • 防抖和节流
    • 巧用sort排序
  • CSS && HTML

    • CSS也需要性能优化
    • class命名规范
    • em、px、rem、vh、vw 区别
    • CSS揭秘阅读笔记
  • 浏览器

    • 浏览器是如何渲染页面的
    • 重排和重绘
    • BOM浏览器对象模型
    • DOM事件
    • 浏览器存储
  • 数据结构

    • JS实现链表
    • JS实现栈与栈应用
    • JS实现常见排序
    • 哈夫曼编码
    • MD5算法
  • vue原理浅析

    • Vue虚拟dom与Diff算法
    • 前端打包文件的缓存机制
    • vue数组为什么不是响应式
    • v-for为什么不能用index做key
  • 前端工程化

    • 浏览器是如何渲染页面的
    • 前端打包需要gzip压缩吗
    • 前端打包文件的缓存机制
    • webpack loader和plugin
  • 轮子&&组件库

    • 实现水波浪进度球
  • 文字转语音mp3文件
  • 文件上传前后端实现
  • moment.js给定时间获取自然月、周的时间轴
  • 实现文件上传功能
  • 批量下载照片
  • leaflet改变坐标原点
  • 网络

    • 有了MAC地址 为什么还需要IP地址
    • 为什么IP地址老是变
    • 我们为什么需要IPV6
    • TCP与UDP
  • 计算机组成原理

    • ASCII、Unicode、UTF-8和UTF-16
  • VSCode

    • VSCode图片预览插件 Image preview
    • rsync:linux间的高效传输工具
  • 3个提升Vue性能的写法
  • 重读Vue文档
  • moment.js给定时间获取自然月、周的时间轴
  • Vue虚拟dom与Diff算法
    • 深入响应式原理
    • Echart样例
    • 我的Vue指令库
    • 滚动到底部加载更多
    • 实现一键换肤
    • 手写Vue数据劫持
    • vue-router核心原理与手写实现
    • computed和watch
    • vue数组为什么不是响应式
    • v-for为什么不能用index做key
    • webpack loader和plugin
    • keep-alive组件原理
    • vue插槽进化
    • Vue多层嵌套组件
    • vue生命周期hook
    • vue监听dom元素的resize事件
    • 前端打包需要gzip压缩吗
    • 实现水波浪进度球
    • Vue
    XingYun
    2021-11-22
    目录

    Vue虚拟dom与Diff算法

    # Vue 虚拟 DOM 和 Diff 算法

    起初我们在使用 JS/JQuery 时,不可避免的会大量操作 DOM,而 DOM 的变化又会引发回流或重绘,从而降低页面渲染性能。

    为了 减少频繁操作 DOM 而引起回流重绘所引发的性能问题 ,虚拟 DOM 应运而生。

    虚拟 DOM (Virtual DOM )这个概念相信大家都不陌生,从 React 到 Vue ,虚拟 DOM 为这两个框架都带来了跨平台的能力。因为很多人是在学习 React 或者 Vue 的过程中接触到的虚拟 DOM ,所以为先入为主,认为虚拟 DOM 和 JSX 密不可分。其实不然,Vue 即使只使用模版,也能把虚拟 DOM 玩得风生水起。 当然同时也有很多人通过 babel 在 Vue 中使用 JSX。

    很多人认为虚拟 DOM 最大的优势是 diff 算法,减少 JavaScript 操作真实 DOM 的带来的性能消耗。虽然这一个虚拟 DOM 带来的一个优势,但并不是全部。

    虚拟 DOM 最大的优势在于抽象了原本的渲染过程,为函数式的 UI 编程方式打开了大门, 实现了跨平台的能力,而不仅仅局限于浏览器的 DOM,可以是安卓和 IOS 的原生组件,可以是很火热的小程序,也可以是各种软件的 GUI。

    # 虚拟 DOM 的作用是什么?

    1. 虚拟 DOM 最大的优势在于抽象了原本的渲染过程,为函数式的 UI 编程方式打开了大门, 实现了跨平台的能力,而不仅仅局限于浏览器的 DOM
    2. 兼容性好。因为 Vnode 本质是 JS 对象,所以不管 Node 还是浏览器环境,都可以操作
    3. 真实 DOM 在频繁操作时引发的回流重绘导致性能很低,虚拟 DOM 不会进行回流和重绘;虚拟 DOM 频繁修改,然后一次性对比差异并修改真实 DOM,只更新差异部分,所以进行局部回流重绘,减少了真实 DOM 中多次回流重绘引起的性能损耗

    # 虚拟 DOM 对真实 DOM 的抽象过程

    例:真实 dom

    <div id="box">
      <span class="content">xingyun</span>
    </div>
    
    1
    2
    3

    上面的 HTML 转换为虚拟 DOM 如下:

    {
      tag: 'div',
      props: {
        id: 'box'
      },
      chidren: [
        {
          tag: 'span',
          props: {
            className: 'content'
          },
          chidren: [
            'xingyun'
          ]
        }
      ]
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    树形结构的 DOM 结构抽象为了树形结构的 JS 对象,之后通过 Diff 算法生成真实

    过程简化如下

    # DIFF算法

    当数据变化时,vue 如何来更新视图的?

    vue 会执行 render 函数生成两颗树,一棵新树 newVnode,一棵旧树 oldVnode,然后两棵树进行对比更新找差异就是diff,全称difference,在 vue 里面 diff 算法是通过 patch 函数来完成的,所以有的时候也叫patch算法

    当组件创建的时候,组件所依赖的属性或者数据变化时,会运行一个函数 (下面代码中的updateComponent),该函数会做两件事:

    • 运行_render生成一颗新的虚拟 dom 树(vnode tree)

    • 运行_update,传入_render 生成的虚拟 dom 树的根节点,对新旧两棵树进行对比,最终完成对真实 dom 的更新

    核心代码如下,跟原代码有所差异,但都差不多,是这么个意思:

    // vue构造函数
    function Vue() {
      // ... 其他代码
      var updateComponent = () => {
        this._update(this._render())
      }
      new Watcher(updateComponent)
      // ... 其他代码
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    diff过程就发生在_update

    diff过程就是调用patch函数,比较新老节点,一边比较一边给真实DOM打补丁**(patch)**;

    function patch(oldVnode, vnode) {
      // some code
      if (sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode)
      } else {
        const oEl = oldVnode.el // 当前oldVnode对应的真实元素节点
        let parentEle = api.parentNode(oEl) // 父元素
        createEle(vnode) // 根据Vnode生成新元素
        if (parentEle !== null) {
          api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
          api.removeChild(parentEle, oldVnode.el) // 移除以前的旧元素节点
          oldVnode = null
        }
      }
      // some code
      return vnode
    }
    
    function sameVnode(a, b) {
      return (
        a.key === b.key && // key值
        a.tag === b.tag && // 标签名
        a.isComment === b.isComment && // 是否为注释节点
        // 是否都定义了data,data包含一些具体信息,例如onclick , style
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b) // 当标签是<input>的时候,type必须相同
      )
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28

    sameVnode函数用来判断两个节点是否有比较的必要,如果没有,直接替换,如果有必要执行patchVnode

    那如果不是同一节点,但是它们子节点一样怎么办?敲重点:diff 是同层比较,不存在跨级比较的,也就是说 diff 为深度优先遍历!

    patchVnode (oldVnode, vnode) {
        const el = vnode.el = oldVnode.el
        let i, oldCh = oldVnode.children, ch = vnode.children
        if (oldVnode === vnode) return // 新旧节点指向同一个对象 直接return
        if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
            api.setTextContent(el, vnode.text)  // 文本不同 更新文本
        }else {
            updateEle(el, vnode, oldVnode)
        	if (oldCh && ch && oldCh !== ch) {
                updateChildren(el, oldCh, ch) // 如果都有子节点 调用updateChildren
        	}else if (ch){
                createEle(vnode) // 如果新节点有子节点,老节点没有,直接加上子节点
        	}else if (oldCh){
                api.removeChildren(el) // 如果新节点没有子节点,老节点有,直接删除子节点
        	}
        }
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    由上patchVnode函数可以发现, 最复杂的情况也就是新老节点都有子节点,那么 updateChildren 是如何来处理这一问题的,该方法也是 diff 算法的核心,下面我们来了解一下

    # updateChildren

    源码

    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, elmToMove, 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
    
      while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        if (isUndef(oldStartVnode)) {
          oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
        } else if (isUndef(oldEndVnode)) {
          oldEndVnode = oldCh[--oldEndIdx]
        }
    
        // 新旧节点头尾四次比较 start
        else if (sameVnode(oldStartVnode, newStartVnode)) {
          patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
          oldStartVnode = oldCh[++oldStartIdx]
          newStartVnode = newCh[++newStartIdx]
        } else if (sameVnode(oldEndVnode, newEndVnode)) {
          patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
          oldEndVnode = oldCh[--oldEndIdx]
          newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldStartVnode, newEndVnode)) {
          // Vnode moved right
          patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
          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)
          canMove &&
            nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
          oldEndVnode = oldCh[--oldEndIdx]
          newStartVnode = newCh[++newStartIdx]
        }
        // 新旧节点头尾四次比较 end
        else {
          if (isUndef(oldKeyToIdx))
            oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
          idxInOld = isDef(newStartVnode.key)
            ? oldKeyToIdx[newStartVnode.key]
            : null
          if (isUndef(idxInOld)) {
            // 旧节点没有这个key 证明是新节点 直接新增节点
            createElm(
              newStartVnode,
              insertedVnodeQueue,
              parentElm,
              oldStartVnode.elm
            )
            newStartVnode = newCh[++newStartIdx]
          } else {
            elmToMove = oldCh[idxInOld]
            if (sameVnode(elmToMove, newStartVnode)) {
              // 旧节点有这个key,并且节点相同 直接移动到对应位置
              patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
              oldCh[idxInOld] = undefined
              canMove &&
                nodeOps.insertBefore(
                  parentElm,
                  newStartVnode.elm,
                  oldStartVnode.elm
                )
              newStartVnode = newCh[++newStartIdx]
            } else {
              // 旧节点有这个key,但是节点不同 当成新节点处理 直接新增节点
              createElm(
                newStartVnode,
                insertedVnodeQueue,
                parentElm,
                oldStartVnode.elm
              )
              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(parentElm, oldCh, oldStartIdx, oldEndIdx)
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115

    代码中对一些关键位置做了注释

    再用图来解析一下这个过程

    第一步、指针处相互比较,即有四种对比,分别为:a 与 b、千与 g、a 与 g、千与 b,没有相等的继续

    第二步、此处分为俩种情况,有 key 和无 key,无 key 的情况直接创建新的 Node 插入到 oldStartldx 指针对应的 Node (a)之前;有 key 的情况:取 newStartldx 指针处的值,到 old VNode 里面找,发现了有相同的 b,记录此时的 oldkeyToldx,随即调整真实 DOM
    Node, 将 DOM Node 中的 b 移动到 oldStartidx 指针对应的 Node (a)之前。如下图

    然后找到 old VNode 中的 oldkeyToldx 对应的值设置为 undefined, newStartidx 指针往中
    问靠拢,newStartldx++,此时 old VNode 和 new VNode 指针如下

    判断 oldStartldx <= oldEndldx && newStartldx <= newEndldx,结果为真,继续循环流程

    此时四种对比为:a 与 f a 与 g g 与 f f (old VNode)与 f (new VNode)

    最后一个对比结果为真,调整真实 DOM Node,将 DOM Node 中的千移动到 oldStartldx 指针对应的 Node (a)之前,如下图

    然后 newStartldx 指针以及 oldEndldx 指针往中间靠拢,newStartldx++, oldEndildx-

    判断 oldStartldx <= oldEndldx && newStartildx <= newEndldx,结果为假,跳出循
    环,判断此时的指针:

    1、如果 oldStartldx > oldEndldx,说明老节点遍历完成了,新的节点比较多,所以多出
    来的这些新节点,需要创建出来并添加到真实 DOM Node 后面。

    2、如果 newStartldx > newEndldx,说明新节点遍历完成了,老的节点比较多,所以多
    出来的这些老节点,需要从真实 DOM Node 中删除这些节点.

    此时我们符合场景二,所以需要从真实 DOM Node 中删除 [oldStartldx,oldEndldx]区问
    中的 Node 节点,删除过程中,对 node 节点判断,为 false 直接跳过,为 true 直接删除
    即删除掉真实 DOM Node 中的 a、c、d、e 四个节点,如下图解

    Diff 过程完成

    #vue
    上次更新: 2023/04/05, 09:41:10
    moment.js给定时间获取自然月、周的时间轴
    深入响应式原理

    ← moment.js给定时间获取自然月、周的时间轴 深入响应式原理→

    最近更新
    01
    JavaScript-test
    07-20
    02
    二维码的原理
    07-20
    03
    利用ChatGPT优化代码
    07-20
    更多文章>
    Theme by Vdoing | Copyright © 2021-2023 XingYun | MIT License
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式