虚拟DOM(virtual DOM)
什么是虚拟DOM
通俗的讲,虚拟DOM就是一个JavaScript对象,它是真实DOM的抽象,只保留一些有用的信息,更轻量地描述DOM树的结构。
为什么需要虚拟DOM
1、可以利用MVVM框架解决视图和框架同步问题
2、模板引擎可以简化视图操作,但是没有办法跟踪状态,可以利用虚拟DOM跟踪状态状态(可以获取到上一次状态,通过比较前后两种状态差异更新真实DOM)
3、DOM属性过多,频繁改动操作DOM性能消耗很大(尤其在复杂视图下),相对而言虚拟DOM只是虚拟JavaScript对象,描述属性不是很多,开销较小;
4、无需开发人员手动操作
下面可以感受下二者的区别:
模拟一个虚拟DOM:
// 构造虚拟DOM对象类
function Element(tagName, props, children) {
this.tagName = tagName;
this.props = props;
this.children = children;
}
// 创建虚拟DOM
function createElement(tagName, props, children) {
return new Element(tagName, props, children)
}
// <ul class="ul-wrap">
// <li class="li-item">1</li>
// <li class="li-item">2</li>
// <li class="li-item">3</li>
// </ul>
// 假设我们有如上的DOM结构, 那我我们就可以利用虚拟DOM模拟出一个类似的DOM树结构
let VDOM = createElement("ul", {
class: "ul-wrap",
}, [
createElement("li", {
class: "li-item"
}, ["1"]),
createElement("li", {
class: "li-item"
}, ["2"]),
createElement("li", {
class: "li-item"
}, ["3"]),
]);
console.dir(JSON.stringify(VDOM,null,2));
其中的props是虚拟dom的属性
或者直接利用vue中的h()函数进行创建vnode:
// 在vue实例中
import { h } from 'vue'
const vnode = h(
'ul',//type
{ id: 'foo', class: 'bar' }, //props
[
// children
]
)
再来看下真实的dom:
HTMLCollection [ul.ul-wrap]
0: ul.ul-wrap
accessKey: ""
ariaAtomic: null
ariaAutoComplete: null
ariaBusy: null
ariaChecked: null
ariaColCount: null
ariaColIndex: null
ariaColSpan: null
ariaCurrent: null
ariaDescription: null
ariaDisabled: null
ariaExpanded: null
ariaHasPopup: null
ariaHidden: null
ariaKeyShortcuts: null
ariaLabel: null
ariaLevel: null
ariaLive: null
ariaModal: null
ariaMultiLine: null
ariaMultiSelectable: null
ariaOrientation: null
ariaPlaceholder: null
ariaPosInSet: null
ariaPressed: null
ariaReadOnly: null
ariaRelevant: null
ariaRequired: null
ariaRoleDescription: null
ariaRowCount: null
ariaRowIndex: null
ariaRowSpan: null
ariaSelected: null
ariaSetSize: null
ariaSort: null
ariaValueMax: null
ariaValueMin: null
ariaValueNow: null
ariaValueText: null
assignedSlot: null
attributeStyleMap: StylePropertyMap {size: 0}
attributes: NamedNodeMap {0: class, class: class, length: 1}
autocapitalize: ""
autofocus: false
baseURI: "file:///E:/demo/test3.html"
childElementCount: 3
childNodes: NodeList(7) [text, li.li-item, text, li.li-item, text, li.li-item, text]
children: HTMLCollection(3) [li.li-item, li.li-item, li.li-item]
classList: DOMTokenList ["ul-wrap", value: "ul-wrap"]
className: "ul-wrap"
clientHeight: 63
clientLeft: 0
clientTop: 0
clientWidth: 1154
compact: false
contentEditable: "inherit"
dataset: DOMStringMap {}
dir: ""
draggable: false
elementTiming: ""
enterKeyHint: ""
firstChild: text
firstElementChild: li.li-item
hidden: false
id: ""
innerHTML: "\n <li class=\"li-item\">1</li>\n <li class=\"li-item\">2</li>\n <li class=\"li-item\">3</li>\n "
innerText: "1\n2\n3"
inputMode: ""
isConnected: true
isContentEditable: false
lang: ""
lastChild: text
lastElementChild: li.li-item
localName: "ul"
namespaceURI: "http://www.w3.org/1999/xhtml"
nextElementSibling: script
nextSibling: text
nodeName: "UL"
nodeType: 1
nodeValue: null
nonce: ""
offsetHeight: 63
offsetLeft: 8
offsetParent: body
offsetTop: 16
offsetWidth: 1154
onabort: null
onanimationend: null
onanimationiteration: null
onanimationstart: null
onauxclick: null
onbeforecopy: null
onbeforecut: null
onbeforepaste: null
onbeforexrselect: null
onblur: null
oncancel: null
oncanplay: null
oncanplaythrough: null
onchange: null
onclick: null
onclose: null
oncontextmenu: null
oncopy: null
oncuechange: null
oncut: null
ondblclick: null
ondrag: null
ondragend: null
ondragenter: null
ondragleave: null
ondragover: null
ondragstart: null
ondrop: null
ondurationchange: null
onemptied: null
onended: null
onerror: null
onfocus: null
onformdata: null
onfullscreenchange: null
onfullscreenerror: null
ongotpointercapture: null
oninput: null
oninvalid: null
onkeydown: null
onkeypress: null
onkeyup: null
onload: null
onloadeddata: null
onloadedmetadata: null
onloadstart: null
onlostpointercapture: null
onmousedown: null
onmouseenter: null
onmouseleave: null
onmousemove: null
onmouseout: null
onmouseover: null
onmouseup: null
onmousewheel: null
onpaste: null
onpause: null
onplay: null
onplaying: null
onpointercancel: null
onpointerdown: null
onpointerenter: null
onpointerleave: null
onpointermove: null
onpointerout: null
onpointerover: null
onpointerrawupdate: null
onpointerup: null
onprogress: null
onratechange: null
onreset: null
onresize: null
onscroll: null
onsearch: null
onseeked: null
onseeking: null
onselect: null
onselectionchange: null
onselectstart: null
onstalled: null
onsubmit: null
onsuspend: null
ontimeupdate: null
ontoggle: null
ontransitioncancel: null
ontransitionend: null
ontransitionrun: null
ontransitionstart: null
onvolumechange: null
onwaiting: null
onwebkitanimationend: null
onwebkitanimationiteration: null
onwebkitanimationstart: null
onwebkitfullscreenchange: null
onwebkitfullscreenerror: null
onwebkittransitionend: null
onwheel: null
outerHTML: "<ul class=\"ul-wrap\">\n <li class=\"li-item\">1</li>\n <li class=\"li-item\">2</li>\n <li class=\"li-item\">3</li>\n </ul>"
outerText: "1\n2\n3"
ownerDocument: document
parentElement: body
parentNode: body
part: DOMTokenList [value: ""]
prefix: null
previousElementSibling: null
previousSibling: text
scrollHeight: 63
scrollLeft: 0
scrollTop: 0
scrollWidth: 1154
shadowRoot: null
slot: ""
spellcheck: true
style: CSSStyleDeclaration {additiveSymbols: "", alignContent: "", alignItems: "", alignSelf: "", alignmentBaseline: "", …}
tabIndex: -1
tagName: "UL"
textContent: "\n 1\n 2\n 3\n "
title: ""
translate: true
type: ""
__proto__: HTMLUListElement
length: 1
__proto__: HTMLCollection
由此可以直观的感受到虚拟DOM和真实DOM的差别,真实DOM的属性太多了,创建DOM节点的开销十分的大,相比之下,虚拟DOM是不是十分的小鸟依人!!但是:
那使用了虚拟DOM一定会比直接渲染真实DOM快吗?
答案当然是否定的,这种说法是不严谨的,所谓的“性能更好、更快”是指在频繁修改DOM的情况下运用虚拟DOM与DIFF算法结合,从而提升DOM的复用,减少无谓的节点创建等从而减少性能消耗,实际上底层还是操作的DOM,所以确切一点的说法是虚拟DOM是比操作不当的原生DOM快、性能更佳,下面直接引用大佬的比较过程:
首次渲染👇不采用虚拟DOM的步骤
- 浏览器接受绘制指令
- 创建所有节点
首次渲染👇采用虚拟DOM的步骤
- 浏览器接受绘制指令
- 创建虚拟DOM
- 创建所有节点
首次渲染或者所有节点都需要进行更新的时候。这个时候采用虚拟DOM会比直接操作原生DOM多一重构建虚拟DOM树的操作。这会更大的占用内存和延长渲染时间。
其实,无论是Vue还是React,其引用虚拟DOM的核心目的是为了提高开发效率而不是性能,有虚拟DOM之后,无需我们再关注DOM的操作,可以更加集中在数据的改变上;
虚拟DOM的优势在于我们更新节点时候。它会检查哪些节点需要更新。尽量复用已有DOM,减少DOM的删除和重新创建,从而尽量减少开销。并且这些操作我们是可以通过自己手动操作javascript底层api实现的。只是我们手动操作会非常耗费我们的时间和精力。这个工作由虚拟DOM代劳,会让我们开发更快速便捷。
虚拟DOM库
Vue.js2.x内部使用的虚拟DOM就是改造的Snabbdom
大约200SLOC(single line of code)
通过模块可扩展
源码使用TypeScript开发
最快的Virtual DOM之一
snabbdom的核心
1、init()设置模块.创建patch()函数
2、使用h()函数生成vnode函数,再用vnode函数创建JavaScript对象(Vnode)描述真实DOM
3、patch()比较新旧两个Vnode,把变化的内容更新到真实DOM树
DIFF算法
Diff 的出现,就是为了减少更新量,找到最小差异部分DOM,只更新差异部分DOM
diff 算法要明确一个概念就是 Diff 的对象是虚拟DOM(virtual dom),更新真实 DOM 是 Diff 算法的结果。
做法
1、根节点直接比较
2、只有两个新旧节点是相同节点的时候,才会去比较他们各自的子节点
3、同层级比较,不需要递归
根节点直接比较,父节点相同才比较子节点
比如下图出现的 四次比较(从 first 到 fouth),他们的共同特点都是有 相同的父节点
比如 蓝色方的比较,新旧子节点的父节点是相同节点 1
比如 红色方的比较,新旧子节点的父节点都是 2
所以他们才有比较的机会
而下图中,只有两次比较,就是因为在 蓝色方 比较中,并没有相同节点,所以不会再进行下级子节点比较
Diff 比较的内核是 节点复用,所以 Diff 比较就是为了在 新旧节点中 找到 相同的节点
这个的比较逻辑是建立在上一步说过的同层比较基础之上的
所以说,节点复用,找到相同节点并不是无限制递归查找
比如下图中,的确 旧节点树 和 新节点树 中有相同节点 6,但是然并卵,旧节点6并不会被复用
就算在同一层级,然而父节点不一样,依旧然并卵
只有这种情况的节点会被复用,相同父节点 8
下面说说 Diff 的比较逻辑
1、能不移动,尽量不移动
2、没得办法,只好移动
3、实在不行,新建或删除
比较处理流程是下面这样
在新旧节点中
1、先找到 不需要移动的相同节点,消耗最小
2、再找相同但是需要移动的节点,消耗第二小
3、最后找不到,才会去新建删除节点,保底处理
比较是为了修改DOM 树
其实这里存在 三种树,一个是 页面DOM 树,一个是 旧VNode 树,一个是 新 Vnode 树
页面DOM 树 和 旧VNode 树 节点一一对应的
而 新Vnode 树则是表示更新后 页面DOM 树 该有的样子
这里把 旧Vnode 树 和 新Vnode树 进行比较的过程中
不会对这两棵Vode树进行修改,而是以比较的结果直接对 真实DOM 进行修改
比如说,在 旧 Vnode 树同一层中,找到 和 新Vnode 树 中一样但位置不一样节点
此时需要移动这个节点,但是不是移动 旧 Vnode 树 中的节点
而是 直接移动 DOM
总的来说,新旧 Vnode 树是拿来比较的,页面DOM 树是拿来根据比较结果修改的
如果你有点懵,我们就来就简单说个例子
Diff 简单例子
比如下图存在这两棵 需要比较的新旧节点树 和 一棵 需要修改的页面 DOM树
第一轮比较开始
因为父节点都是 1,所以开始比较他们的子节点
按照我们上面的比较逻辑,所以先找 相同 && 不需移动 的点
毫无疑问,找到 2
拿到比较结果,这里不用修改DOM,所以 DOM 保留在原地
第二轮比较开始
然后,没有 相同 && 不需移动 的节点 了
只能第二个方案,开始找相同的点
找到 节点5,相同但是位置不同,所以需要移动
拿到比较结果,页面 DOM 树需要移动DOM 了,不修改,原样移动
第三轮比较开始
继续,哦吼,相同节点也没得了,没得办法了,只能创建了
所以要根据 新Vnode 中没找到的节点去创建并且插入
然后旧Vnode 中有些节点不存在 新VNode 中,所以要删除
于是开始创建节点 6 和 9,并且删除节点 3 和 4
vue2/vue3中的diff算法简单对比
1、双端DIFF算法
在新旧节点的开始与末尾,分别有一个指针指向,然后在比较过程中往中间收拢;
startIndex不断增大,endIndex不断减小,
oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx时继续比较,老节点或者新节点的开始位置大于结束位置,即startIndex大于endIndex时比较完毕
源码可以参考Vue的diff算法解析
2、快速DIFF算法
vue2.x中的虚拟dom是进行 「全量的对比」,在运行时会对所有节点生成一个虚拟节点树,当页面数据发生变更好,会遍历判断virtual dom所有节点 (包括一些不会变化的节点) 有没有发生变化;虽然说diff算法确实减少了对DOM节点的直接操作,但是这个 「减少是有成本的」,如果是复杂的大型项目,必然存在很复杂的父子关系的VNode,「而Vue2.x的diff算法,会不断地递归调用 patchVNode,不断堆叠而成的几毫秒,最终就会造成 VNode 更新缓慢」。
动静结合 PatchFlag
<div>
<div>{msg}</div>
<div>静态文字</div>
</div>
在Vue3.0中,在这个模版编译时,编译器会在动态标签末尾加上 /* Text*/ PatchFlag。 「也就是在生成VNode的时候,同时打上标记,在这个基础上再进行核心的diff算法」 并且 PatchFlag 会标识动态的属性类型有哪些,比如这里 的TEXT 表示只有节点中的文字是动态的。而patchFlag的类型也很多,这里暂时不细究。
看源码:
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对于不参与更新的元素,做静态标记并提示,只会被创建一次,在渲染时直接复用。」
其中还有cacheHandlers(事件侦听器缓存),以后再了解。