第十三篇 DOM 补充 - 虚拟DOM 、 diff 算法 及 其他
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,最麻烦,最难实现)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!