Vue中的虚拟Dom,diff算法,以及diff的优化

virtual dom:

关键词:
1、 template
2、渲染函数
3、 vnode(virtual dom)
4、patch(diff算法)
5、view
在这里插入图片描述

Vue.js通过编译将template 模板转换成渲染函数(render ) ,执行渲染函数就可以得到一个虚拟节点树
VNode 虚拟节点:它可以代表一个真实的 dom 节点。通过 createElement 方法能将 VNode 渲染成 dom 节点。简单地说,vnode可以理解成节点描述对象,它描述了应该怎样去创建真实的DOM节点。
patch(也叫做patching算法):虚拟DOM最核心的部分,它可以将vnode渲染成真实的DOM,这个过程是对比新旧虚拟节点之间有哪些不同,然后根据对比结果找出需要更新的的节点进行更新。这点我们从单词含义就可以看出, patch本身就有补丁、修补的意思,其实际作用是在现有DOM上进行修改来实现更新视图的目的。Vue的Virtual DOM Patching算法是基于Snabbdom的实现,并在些基础上作了很多的调整和改进
在这里插入图片描述

虚拟DOM在Vue.js主要做了两件事:

1、提供与真实DOM节点所对应的虚拟节点vnode

2、将虚拟节点vnode和旧虚拟节点oldVnode进行对比(diff算法),然后更新视图

总结:「vdom是为了减轻性能压力。dom是昂贵的,昂贵的一方面在dom本身的重量,dom节点在js里的描述是个非常复杂属性很多原型很长的超级对象,另一方面是浏览器渲染进程和js进程是独立分离的,操作dom时的通信和浏览器本身需要重绘的时耗都是很高的。所以用vdom去模拟dom,vdom每个节点都只挂载js操作的必要属性,每次组件update时都先操作vdom,通过vdom的比对,得到一个真实dom的需要操作的集合。整个机制是在JavaScript层面计算,在完成之前并不会操作DOM,等数据稳定之后再实现精准的修改。」

什么是虚拟DOM:

虚拟DOM就是普通的js对象。是一个用来描述真实dom结构的js对象,它不是真实的dom
1、传统的dom数据发生变化的时,我们需要不断的去操作dom,才能更新数据
2、 虽然出现了模板引擎,可以一次性更新多个dom。但它没有可以追踪状态的机制,当引擎内某个数据发生变化时,它依然操作dom去重新渲染整个引擎。
3、而vue借助和改造Snabbdom便是虚拟dom,它可以跟踪当前dom状态,因为它会根据当前数据生成一个描述当前dom结构的虚拟dom,当数据发生变化时,又生成一个新的虚拟dom,而两个虚拟dom恰好保存了变化前后的状态。
4、然后通过diff算法,计算出当前两个虚拟dom之间的差异,得出一个更好的替换方案,解决更新页面时的性能问题


# Snabbdom的使用: 流程
// 引入对应功能(核心功能Init,h)
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'

// 1.通过init  获取到patch函数
const patch = init([])

// 2.通过h创建vNode
let vNode = h('div.app', '新内容')
  
// 3.获取挂载元素
const dom = document.querySelector('#app')

// 4.通过patch,将vNode渲染到dom上
const oldValue = patch(dom, vNode)

// 5.创建新的vnode, 更新到oldValue
vNode = h('p#text.abc', 'p标签内容')
patch(oldValue, vNode)

官方提供:

6个模块如下:
1、 attributes 设置DOM元素的属性,使用setAttribute(); 处理布尔类型的属性
2、props 和attributes模块相似,设置DOM元素的属性element[attr] = value; 不处理布尔类型的属性
3、 class 切换类样式 注意:给元素设置类样式是通过sel选择器
4、设置 data-* 的自定义属性
5、eventlisteners 注册和移除事件
6、style 设置行内样式, 支持动画delayed/remove/destroy

Snabbdom的核心:

介绍如下:
1、init函数 初始化一个patch函数
2、h函数 创建vNode虚拟Dom节点
3、patch() 比较新旧两个 Vnode
4、把变化的内容更新到真实 DOM 树,并渲染到视图上


# patch 整体过程:

分析:
• patch(oldVnode, newVnode)
• 把新节点中变化的内容渲染到真实 DOM,最后返回新节点作为下一次 处理的旧节点
• 对比新旧 VNode 是否相同节点(节点的 key 和 sel 相同)
• 如果不是相同节点,删除之前的内容,重新渲染
• 如果是相同节点,再判断新的 VNode 是否有 text,如果有并且和 oldVnode 的 text 不同,直接更新文本内容
• 如果新的 VNode 有 children,判断子节点是否有变化


diff算法:比较两棵虚拟DOM树的差异

我们知道了,新的虚拟DOM和旧的虚拟DOM是通过diff算法进行比较之后更新的。
这也是 Virtual DOM 算法最核心的部分。两个树的完全的 diff 算法是一个时间复杂度为 O(n^3) 的问题。但是在前端当中,你很少会跨越层级地移动DOM元素。所以 Virtual DOM 只会对同一个层级的元素进行对比:

在这里插入图片描述
上面的div只会和同一层级的div对比,第二层级的只会跟第二层级对比。这样算法复杂度就可以达到 O(n)。
2.1 深度优先遍历,记录差异

在实际的代码中,会对新旧两棵树进行一个深度优先的遍历,这样每个节点都会有一个唯一的标记:
在深度优先遍历的时候,每遍历到一个节点就把该节点和新的的树进行对比。如果有差异的话就记录到一个对象里面。

// diff 函数,对比两棵树
function diff (oldTree, newTree) {
  var index = 0 // 当前节点的标志
  var patches = {} // 用来记录每个节点差异的对象
  dfsWalk(oldTree, newTree, index, patches)
  return patches
}

// 对两棵树进行深度优先遍历
function dfsWalk (oldNode, newNode, index, patches) {
  // 对比oldNode和newNode的不同,记录下来
  patches[index] = [...]

  diffChildren(oldNode.children, newNode.children, index, patches)
}

// 遍历子节点
function diffChildren (oldChildren, newChildren, index, patches) {
  var leftNode = null
  var currentNodeIndex = index
  oldChildren.forEach(function (child, i) {
    var newChild = newChildren[i]
    currentNodeIndex = (leftNode && leftNode.count) // 计算节点的标识
      ? currentNodeIndex + leftNode.count + 1
      : currentNodeIndex + 1
    dfsWalk(child, newChild, currentNodeIndex, patches) // 深度遍历子节点
    leftNode = child
  })
}

例如,上面的div和新的div有差异,当前的标记是0,那么:

patches[0] = [{difference}, {difference}, ...] // 用数组存储新旧节点的不同

同理p是patches[1],ul是patches[3],类推。

Vue3.0 diff:

diff痛点:
• vue2.x中的虚拟dom是进行「全量的对比」,在运行时会对所有节点生成一个虚拟节点树,当页面数据发生变更好,会遍历判断virtual dom所有节点(包括一些不会变化的节点)有没有发生变化;
• Vue2.x的diff算法,会不断地递归调用 patchVNode,不断堆叠而成的几毫秒,最终就会造成 VNode 更新缓慢
• Vue3.0 动静结合 PatchFlag;这个模版编译时,编译器会在动态标签末尾加上 /* Text*/ PatchFlag。「也就是在生成VNode的时候,同时打上标记,在这个基础上再进行核心的diff算法」并且 PatchFlag 会标识动态的属性类型有哪些


export const enum PatchFlags {
  TEXT = 1,
  CLASS = 1 << 1,
  STYLE = 1 << 2,
  PROPS = 1 << 3,
  FULL_PROPS = 1 << 4,
  HYDRATE_EVENTS = 1 << 5,
  STABLE_FRAGMENT = 1 << 6,
  KEYED_FRAGMENT = 1 << 7,
  UNKEYED_FRAGMENT = 1 << 8,
  NEED_PATCH = 1 << 9,
  DYNAMIC_SLOTS = 1 << 10,
  DEV_ROOT_FRAGMENT = 1 << 11,
  HOISTED = -1,
  BAIL = -2
}

在这里插入图片描述
当 patchFlag 的值「大于」 0 时,代表所对应的元素在 patchVNode 时或 render 时是可以被优化生成或更新的。

当 patchFlag 的值「小于」 0 时,代表所对应的元素在 patchVNode 时,是需要被 full diff,即进行递归遍历 VNode tree 的比较更新过程。

看源码:

export function render(_ctx, _cache, $props, $setup, $data, $options) {
 return (_openBlock(), _createBlock("div", null, [
  _createVNode("p", null, "'HelloWorld'"),
  _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
 
 ]))
}

这里的_createVNode(“p”, null, _toDisplayString(_ctx.msg), 1 /* TEXT */)就是对变量节点进行标记。

总结:「Vue3.0对于不参与更新的元素,做静态标记并提示,只会被创建一次,在渲染时直接复用。」

其实diff算法对比的是虚拟Dom,而虚拟Dom是呈树状的,所以我们可以发现,diff算法中充满了递归。总结起来,其实diff算法就是一个 patch —> patchVnode —> updateChildren —> patchVnode —> updateChildren —> patchVnode这样的一个循环递归的过程。

循环中 我们经常用到key,key的主要作用其实就是对比两个虚拟节点时,判断其是否为相同节点。加了key以后,我们可以更为明确的判断两个节点是否为同一个虚拟节点,是的话判断子节点是否有变更(有变更更新真实Dom),不是的话继续比。如果不加key的话,如果两个不同节点的标签名恰好相同,那么就会被判定为同一个节点(key都为undefined),结果一对比这两个节点的子节点发现不一样,这样会凭空增加很多对真实Dom的操作,从而导致页面更频繁得进行重绘和回流。

所以合理利用key可以有效减少真实Dom的变动,从而减少页面重绘和回流的频率,进而提高页面更新的效率。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值