by caix in 深圳
虚拟 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 方法,将它们绘制出来。
注意点:
1、DOM 树的构建是文档加载完成开始的? 构建 DOM 树是一个渐进过程,为达到更好的用户体验,渲染引擎会尽快将内容显示在屏幕上,它不必等到整个 HTML 文档解析完成之后才开始构建 render 树和布局。
2、Render 树是 DOM 树和 CSS 样式表构建完毕后才开始构建的? 这三个过程在实际进行的时候并不是完全独立的,而是会有交叉,会一边加载,一边解析,以及一边渲染。
3、CSS 的解析注意点? CSS 的解析是从右往左逆向解析的,嵌套标签越多,解析越慢。
4、JS 操作真实 DOM 的代价?
用我们传统的开发模式,原生 JS 或 JQ 操作 DOM 时,浏览器会从构建 DOM 树开始从头到尾执行一遍流程。
在一次操作中,我需要更新 10 个 DOM 节点,浏览器收到第一个 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,最麻烦,最难实现)
标签:undefined,DOM,text,虚拟,diff,十三篇,data,节点
From: https://www.cnblogs.com/caix-1987/p/17291194.html