Virtual DOM + diff【vue 知识汇点6】

301 阅读6分钟

Virtual DOM

Virtual DOM 是对真实 DOM 的一种抽象描述,以对象的形式模拟树形结构,本质上是 JavaScript 对象,它的核心定义无非就几个关键属性,标签名、数据、子节点、键值等,其他属性都是用来扩展 Node 的灵活性以及实现一些特殊的 feature 的。由于 VNode 都是用来映射到真实 DOM 的渲染,不需要包含操作 DOM 的方法所以是很轻量的。

Virtual DOM 的主要思想就是模拟 DOM 的树状结构,在内存中创建保存映射 DOM 信息的节点数据,当需要视图更新时,先通过对节点数据进行 diff 算法,计算出渲染为真实 dom 的最小代价操作,再一次性对 DOM 进行批量更新操作。

这实际上是一种利用 JS 运算成本来换取 DOM 执行成本的操作,而 JS 的运算快很多,所以这是一种划算的做法

为什么需要 Virtual DOM

  1. 真正的 DOM 是非常庞大的,因为浏览器的标准就把 DOM 设计的非常复杂,当我们频繁地去做 DOM 更新,会造成浏览器的回流或重回,这些都是性能杀手。 Virtual DOM 就是用一个原生的 js 对象去描述一个 DOM 节点,所以它比创建一个 DOM 的代价要小很多。
  2. 现代前端框架的一个基本要求就是无须手动操作 DOM,一方面是手动操作 DOM 无法保证程序性能,另一方面是开发效率可能会降低。
  3. 为了更好地跨平台

⚠️注意:Virtual DOM 不一定更快,之所以框架都用 Virtual DOM,是因为可以提高代码的性能下限,并极大地优化大量操作 DOM 时产生的性能损耗。同时这些框架也保证了,即使在少数 Virtual DOM 不太给力的情境下,性能也在我们接受的范围内。

Virtual DOM 解决了什么问题

首先我们知道:

  • DOM 引擎、JS 引擎相互独立,但有工作在同一线程(主线程)
  • js 代码调用 DOM API 必须挂起 JS 引擎,转换传入参数数据、激活 DOM 引擎,DOM 重绘后再转换可能有的返回值,最后激活 JS 引擎并继续执行若有频繁的 DOM API 调用,且浏览器厂商不做“批量处理”优化。
  • 引擎间切换的单位代价将迅速积累若其中有强制重绘的 DOM API 调用,重新计算布局、重新绘制图像会引起更大的性能消耗。

VNode 和真实 DOM 的区别和优化:

  • Virtual DOM 不会立马排版和重绘
  • Virtual DOM 进行频繁修改,然后一次性比较并修改真实 DOM 中需要改的部分,减少 DOM 节点排版与重绘损耗。
  • virtual 有 patch(补丁) 算法,根据新旧 vnode 比较,经过优化查找到不同的节点修补、更新。不会暴力地直接覆盖 DOM。

diff

vue 的 diff 策略

diff 的过程就是调用名为 patch 的函数,比较新旧节点,一边比较一边给真实的 DOM打补丁。在采用 diff 算法比较新旧节点时,比较只会在同层级进行,不会跨层级比较。

diff 流程图

当数据发生变化时,set 方法会让调用 Dep.notify 通知所有订阅者 Watcher,订阅者就会调用 patch 给真实的 DOM 打补丁,更新相应的视图。

patch

function patch(oldVnode, vnode) {
	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;
        }
    }
}

// 判断两个节点是否值得比较,值得比较则执行 patchVnode,不值得比较则用 Vnode 替换 oldVnode
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必须相同
    );
}

function patchVnode(oldVnode, vnode) {
	const el = vnode.el = oldVnode.el;
    let i, oldCh = oldVnode.children, ch = vnode.children;
    // 如果指向同一个对象,return
    if (oldVnode === vnode) return ;
    // 若新旧节点都有文本,并且不相等,那么将 el 的文本节点设置为 Vnode 的文本节点
    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)
        } else if (ch) {  // 如果oldCh没有子节点,而 Vnode 有,则将Vnode 的子节点真实化之后添加到 el
        	createEle(vnode)
        } else if (oldCh) { // 如果 oldVnode 有子节点而 Vnode 没有,则删除子节点
        	api.removeChildren(el)
        }
    }
}

updateChildren 过程:

分别对 oldS、oldE、S、E 两两做 sameVnode 比较,当其中两个能匹配上那么真实 dom 当响应节点会移到 Vnode 相应的位置。

  • 如果 oldS 和 E 匹配上,那么真实 DOM 中的第一个节点会移到最后
  • 如果 oleE 和 S 匹配上,那么真实 DOM 的最后一个节点会移到最前面
  • 如果四种匹配没有一对是成功的,分为两种情况:
    • 如果新旧节点都有 key,那么会根据 oldChild 的key生成一张 hash 表,用 S 的key 与 hash 表做匹配,匹配成功就判断 S 和匹配节点是否为 sameNode,如果是,就在真实 dom 中将成功的节点移到最前面,否则将 S 生成对应的节点插入到 dom 中对应的 oldS 位置,S 指针向中间移动,被匹配 old 中的节点置为 null
    • 如果没有key,则直接将 S 生成新的节点插入真实 DOM

key 有什么用

key 的作用是为了在数据变化时,强制更新组件,以避免“原地服用”带来的副作用,另外某些情况下,不带key可能性能更好

vue 和 react 都用 diff 算法来对比新旧虚拟节点,从而更新节点。在 vue 的 diff 函数中,在交叉对比中,当新节点和旧节点 头尾交叉对比没有结果时,会根据新节点的 key 去对比旧节点数组中的 key,从而找到相应旧节点(key => index 的 map 映射)。如果没找到,就认为是一个新增节点。而如果没有 key,那么就会采用遍历查找的方式去对比旧节点数组中的 key。相比而言,map 映射的速度更快。

vue 和 react 的 diff 算法的区别

  • vue 对比节点,当元素类型相同,但 className 不同,任务是不同类型元素,删除重建,而 react 会只修改节点属性
  • vue 的列表对比,采用两端到中间的比对方式,而 react 则采用从左到右依次比对的方式。vue效率高一些