第十三篇 DOM 补充 - 虚拟DOM 、 diff 算法 及 其他

by caix in 深圳

2361b48c2618dcbf0865b5fd4b0d2298.png

虚拟 DOM ( Virtual DOM )

什么是 虚拟 DOM ( Virtual DOM )

虚拟DOM 是⽤ JavaScript 对象 表示的 DOM 信息和结构;当 DOM 更新后 通过 diff 算法 使之与真实 dom 保持同步
虚拟DOM 是一个 JavsScript对象,里面包含 sel选择器,data数据,text文本内容,children 子标签等等,一层嵌套一层,这样就表达了一个 虚拟 DOM 结构
虚拟DOM 是 JavaScript按照DOM的结构来创建的虚拟树型结构对象,是对DOM 的抽象,比 DOM 更加轻量型
所以 虚拟DOM 是 HTML DOM 的抽象
处理 虚拟DOM 的方式总比处理 真实的 DOM 要简单并且高效,所以 diff算法 是发生在 虚拟DOM 上的
总之:
Virtual DOM 其实就是一棵以 js对象(VNode节点)作为基础的树
用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象
Virtual DOM 以js对象作为基础,不依赖真实的环境,所以具有跨平台性,可以运行在
Vue 通过建立一个虚拟 DOM 对真实 DOM 发生的变化保持追踪
例 真实 DOM 如下 :
<div class="box">
<h2>标题</h2>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
</div>
例 虚拟 DOM 如下 :
转为 虚拟DOM 之后的结构
{
sel: "div",
elm: undefined, // 表示虚拟节点还没有上树
key: undefined, // 唯一标识
data: {
class: { "box" : true}
},
children: [
{
sel: "h2",
data: {},
text: "标题"
},
{
sel: "ul",
data: {},
children: [
{ sel: li, data: {}, text: "1"},
{ sel: li, data: {}, text: "2"},
{ sel: li, data: {}, text: "3"}
]
}
]
}

真实 DOM 和 其解析流程

所有的浏览器渲染引擎工作流程大致分为 5 步

1、创建 DOM 树

2、创建 Style Rules

3、构建 Render 树

4、布局 Layout

5、绘制 Painting

第一步,构建 DOM 树:用 HTML 分析器,分析 HTML 元素,构建一棵 DOM 树;
第二步,生成样式表:用 CSS 分析器,分析 CSS 文件和元素上的 inline 样式,生成页面的样式表;
第三步,构建 Render 树:将 DOM 树和样式表关联起来,构建一棵 Render 树(Attachment)。每个 DOM 节点都有 attach 方法,接受样式信息,返回一个 render 对象(又名 renderer),这些 render 对象最终会被构建成一棵 Render 树;
第四步,确定节点坐标:根据 Render 树结构,为每个 Render 树上的节点确定一个在显示屏上出现的精确坐标;
第五步,绘制页面:根据 Render 树和节点显示坐标,然后调用每个节点的 paint 方法,将它们绘制出来。
注意点:
1DOM 树的构建是文档加载完成开始的? 构建 DOM 树是一个渐进过程,为达到更好的用户体验,渲染引擎会尽快将内容显示在屏幕上,它不必等到整个 HTML 文档解析完成之后才开始构建 render 树和布局。
2Render 树是 DOM 树和 CSS 样式表构建完毕后才开始构建的? 这三个过程在实际进行的时候并不是完全独立的,而是会有交叉,会一边加载,一边解析,以及一边渲染。
3CSS 的解析注意点? CSS 的解析是从右往左逆向解析的,嵌套标签越多,解析越慢。
4JS 操作真实 DOM 的代价?
用我们传统的开发模式,原生 JSJQ 操作 DOM 时,浏览器会从构建 DOM 树开始从头到尾执行一遍流程。
在一次操作中,我需要更新 10DOM 节点,浏览器收到第一个 DOM 请求后并不知道还有 9 次更新操作,因此会马上执行流程,最终执行10 次。
例如,第一次计算完,紧接着下一个 DOM 更新请求,这个节点的坐标值就变了,前一次计算为无用功。
计算 DOM 节点坐标值等都是白白浪费的性能。即使计算机硬件一直在迭代更新,操作 DOM 的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户体验

为什么要使用 虚拟 DOM

1、操作 DOM 会导致浏览器频繁的出现页面的回流和重绘,⾮常耗性能
2、手动操作 DOM 还是比较麻烦的,要考虑浏览器兼容性问题
3、相对于 DOM对象,js对象处理起来更快,而且更简单,通过 diff算法 对比 新旧vdom 之间的差异,可以 批量的、最⼩化的执行 dom操作,从而提高性能
4、虚拟DOM 进行频繁修改,最终一次性比较并修改 真实DOM 中需要改的部分,最后在 真实DOM中 进行排版与重绘,减少过多 DOM节点 回流与重绘损耗
5、使用 虚拟DOM 改变了当前的状态不需要立即的去更新 DOM 而且更新的内容进行更新,对于没有改变的内容不做任何操作,通过前后两次差异进行比较
6、虚拟DOM 可以实现跨平台渲染,服务器渲染 、小程序、原生应用都使用了 虚拟DOM
7、虚拟 DOM 可以维护程序的状态,跟踪上一次的状态
总之:
虚拟DOM 就是为了解决浏览器性能问题而被设计出来的
若一次操作中有 10 次更新 DOM 的动作,虚拟 DOM 不会立即操作 DOM 而是将这 10 次更新的 diff 内容保存到本地一个 JS 对象中
最终将这个 JS 对象一次性 attch 到 DOM 树上,再进行后续操作,避免大量无谓的计算量
所以:
JS 对象模拟 DOM 节点的好处是,页面的更新可以先全部反映在 JS 对象(虚拟 DOM )上
操作内存中的 JS 对象的速度显然要更快,等更新完成后,再将最终的 JS 对象映射成真实的 DOM,交由浏览器去绘制

创建 虚拟 DOM

用 JavaScript 对象来表示 DOM 节点,使用对象的属性记录节点的类型、属性、子节点等

看 vue 源码的时候,提到了snabbdom,而这个库里有个很重要的函数 h() 函数

h函数 主要用来产生 虚拟节点(vnode)

h函数 为重载函数,根据参数不同生成不同类型的vnode

h() 函数

h函数 是用节点的描述(标签名、属性和事件、子元素)去创建真实节点的,并返回这个真实节点

h函数 就是用节点的描述(标签名、标签的其他自身信息、子元素)创建虚拟节点

其实一个元素的三斧头:标签名、标签的其他自身信息、子元素

作用: h函数 主要用来产生 虚拟节点(vnode)
第一个参数:标签名字、组件的选项对象、函数
第二个参数:标签对应的属性 (可选)
第三个参数:子级虚拟节点,字符串或者是数组形式
如下:
h('a',{ props: {href: 'http://www.baidu.com'}, '百度'})
上面的h函数对应的虚拟节点为:
{ sel: 'a', data: { props: {href: 'http://www.baidu.com'}}, text: "百度"}
真正的DOM节点为:
<a href = "http://www.baidu.com">百度</a>
我们还可以嵌套的使用h函数,比如:
h('ul', {}, [
h('li', {}, '1'),
h('li', {}, '2'),
h('li', {}, '3'),
])
嵌套使用 h 函数,就会生成一个虚拟 DOM
{
sel: "ul",
elm: undefined,
key: undefined,
data: {},
children: [
{ sel: li, elm: undefined, key: undefined, data: {}, text: "1"},
{ sel: li, elm: undefined, key: undefined, data: {}, text: "2"},
{ sel: li, elm: undefined, key: undefined, data: {}, text: "3"}
]
}
为什么有了 h 函数还要 vnode 函数

其实 h 函数,更多的时候,是便于用户传参,用户只需要考虑三个要素:标签名、标签的其他自身信息、子元素

但是一个 vnode 有 6 种属性,其中的 key 是从 data 来的,所以vnode函数需要 5个 参数,用户调用的话,显然增加理解门槛,所以用h函数简化了传参,降低调用门槛,而 h函数 内部调用 vnode函数 生成 vnode

h函数 的参数最多三个,但只有第一个是必传项,第二个参数和第三个都是可传项,所以内部对各种情况作了判断,已生成正确的 vnode

VNode必备属性只有
tag data children text elm
其他属性为 vue功能 需要
如 componetOptions componentInstance 只在组件节点中才被使用
export class VNode {
tag?: string
data?: VNodeData
children?: Array<VNode>
text?: string
elm?: Node
context?: Vue
componentOptions?: VueOptions
componentInstance?: Vue
parent?: VNode
key?: string | number
constructor(
tag?: string,
data?: VNodeData,
children?: Array<VNode>,
text?: string,
elm?: Node,
context?: Vue,
componentOptions?: VueOptions
) {
this.tag = tag
this.data = data || ({} as VNodeData)
this.children = children
this.text = text
this.elm = elm
this.context = context || bindContenxt
this.componentOptions = componentOptions
}
}
VNode 属性含义
tag: 当前节点的标签名
data: 当前节点对应的对象,包含了具体的一些数据信息,是一个 VNodeData 类型,可以参考VNodeData类型中的数据信息
children: 当前节点的子节点,是一个数组
text: 当前节点的文本
elm: 当前虚拟节点对应的真实dom节点
ns: 当前节点的名字空间
context: 当前节点的编译作用域
functionalContext: 函数化组件作用域
key: 节点的key属性,被当作节点的标志,用以优化
componentOptions: 组件的 option 选项
componentInstance: 当前节点对应的组件的实例
parent: 当前节点的父节点
raw: 简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false
isStatic: 是否为静态节点
isRootInsert: 是否作为跟节点插入
isComment: 是否为注释节点
isCloned: 是否为克隆节点
isOnce: 是否有 v-once 指令
在 vue-render 方法中,此处 h 即为创建 虚拟节点 的函数
new Vue({
render (h) {
return h('h1', 'hello world')
}
})
虚拟 DOM 节点类型

我们知道 真实DOM 的节点类型非常多,如Element、Attr、Comment、Document、DocumentFragment、Text 等

而 VNode,只做4种形式:1、组件节点、2、子节点(children属性不为空)、3、文本节点、 4、注释节点

子节点
子节点类型,其 tag 和 children 属性不为空,其 text 属性为空
v1 = h('h1', [h('', 'hello world')])
{
children: [
{
children: undefined,
data: {},
elm: undefined,
tag: undefined,
text: 'hello world'
}
],
data: {},
elm: undefined,
tag: "h1",
text: undefined,
}
文本节点
文本节点类型,其 tag 和 children 属性为空,其 text 属性不为空
v2 = h('', 'hello world')
{
children: undefined,
data: {},
elm: undefined,
tag: undefined,
text: 'hello world'
}
注释节点
文本节点类型,其 tag 属性为 !,children 属性为空,其 text 属性不为空
v3 = h('!', 'hello comment')
{
children: undefined,
data: {},
elm: undefined,
tag: '!',
text: 'hello world'
}
组件节点
组件节点类型,其 componentOptions 属性不为空
v4 = h('button-count', [])
{
children: undefined
componentInstance: Proxy {$refs: {}, $options: {}}
componentOptions: {Ctor: ƒ, propsData: undefined, children: Array(1), tag: "button-counter"}
data: {on: undefined, hook: {}}
elm: button
tag: "vue-component-1-button-counter"
text: undefined
}

渲染 虚拟 DOM ( Virtual DOM )

Vue 通过编译将 template 模板转换成 渲染函数 render ,执行渲染函数 render 会 return 一个 h函数 通过 h函数 就可以创建出对应的虚拟节点树

当有了这个 虚拟的树 之后,就会调用 Patch函数 patch函数 它可以将 vnode 渲染成真实的 DOM

这个过程中会通过 diff算法 对比 新旧虚拟节点 之间有哪些不同,然后根据对比结果找出需要更新的的节点进行更新,其实际作用是在现有 DOM 上进行修改来实现更新视图的目的

虚拟DOM 在 Vue 主要做了两件事:
1、提供与真实DOM节点所对应的虚拟节点vnode
2、将虚拟节点 vnode 和 旧虚拟节点 oldVnode 进行对比,然后更新视图

diff 算法

虚拟DOM 和 虚拟DOM算法 是两种概念

虚拟DOM算法 = 虚拟DOM + Diff算法

Diff算法是一种对比算法
对比两者是 旧虚拟DOM 和 新虚拟DOM
对比出是哪个虚拟节点更改了,找出这个虚拟节点,并只更新这个虚拟节点所对应的真实节点
而不用更新其他数据没发生改变的节点,实现精准地更新真实DOM,进而提高效率。
Diff 算法的原理
Diff 同层对比
新旧 虚拟DOM 对比的时候,Diff算法比较只会在同层级进行, 不会跨层级比较
newVnode 和 oldVnode: 同层的新旧虚拟节点
所以Diff算法是: 深度优先算法 时间复杂度 O(n)
Diff 对比流程
当数据改变时,会触发 setter,并且通过 Dep.notify 去通知所有订阅者Watcher
订阅者们就会调用 patch方法,给真实DOM打补丁,更新相应的视图
patch 方法
这个方法作用就是,对比当前同层的虚拟节点是否为同一种类型的标签
是: 继续执行patchVnode方法进行深层比对
否: 没必要比对了,直接整个节点替换成新虚拟节点
核心: 逐层比较 最小化更新
pactch(oldVnode,newVnode)
把新节点中变化的内容渲染到真实DOM,最后返回新节点作为下一次处理的旧节点(核心)
对比新旧 VNode 是否相同节点(节点的key和sel相同)
如果不是相同节点,删除之前的内容,重新渲染
如果是相同节点,再判断新的VNode是否有text,如果有并且和oldVnode的text不同直接更新文本内容(patchVnode)
如果 新的VNode有children,判断子节点是否有变化(updateChildren,最麻烦,最难实现)
posted @   caix-1987  阅读(40)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
点击右上角即可分享
微信分享提示