首页 > 编程语言 >第十三篇 DOM 补充 - 虚拟DOM 、 diff 算法 及 其他

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

时间:2023-04-05 22:46:18浏览次数:43  
标签:undefined DOM text 虚拟 diff 十三篇 data 节点

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 方法,将它们绘制出来。

注意点: 

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

相关文章

  • cpp generate random array then sort by quick sort
    #include<chrono>#include<ctime>#include<iomainp>#include<iostream>#include<random>#include<sstream>std::stringget_time_now(){std::chrono::time_point<std::chrono::high_resolution_clock>now=st......
  • 【win10】本地化部署stable diffusion AI绘图
    一、环境本地化部署运行虽然很好,但是也有一些基本要求(1)需要拥有NVIDIA显卡,GT1060起,显存4G以上(2)操作系统需要win10(3)电脑内存16G或者以上。(4)有些网页打不开,有时下载很慢。 二、安装miniconda1.安装miniconda这个是用来管理python版本的,他可以实现python的多版本切换。下......
  • 第四十三篇 vue - 进阶主题 - 渲染机制
    渲染机制Vue是如何将一份模板转换为真实的DOM节点的,又是如何高效地更新这些节点的呢?我们接下来就将尝试通过深入研究Vue的内部渲染机制来解释这些问题虚拟DOM你可能已经听说过“虚拟DOM”的概念了,Vue的渲染系统正是基于这个概念构建的虚拟DOM(VirtualDOM,简称VDOM......
  • js dom className classList
    classListdom.classList.contains('black')//删除blackdom.classList.remove('black')//新增.reddom.classList.add('red')classNameletcName=elementNodeReference.className;elementNodeReference.className=cName;详情见官网......
  • js dom 类型判断
    Node对象中的nodeName获取指定节点的节点名称(返回的是大写字母表示的)Node对象中的nodeType获取指定节点的节点类型元素节点属性节点文本节点123Node对象中的nodeValue获取指定节点的值详情见官网:https://developer.mozilla.org/......
  • DOM概述&Element对象获取与使用
    概述Document Object Model文档对象模型将标记语言的各个组成部分封装为对象JavaScript 通过DOM,就能够对HTML进行操作了Document:整个文档对象Element:元素对象Attribute:属性对象Text: 文本对象Comment:注释对象改变HTML元素的内容改变HTML元素的样式(CSS)对H......
  • How to Configure Nginx reverse proxy the domain
    未测试过,自己记录待用http{resolver8.8.8.8;upstreamexample{serverhttp://example.comresolve[use_last]...;keepalive1024;}第二种负载均衡upstreammytarget{serveraaa.tar.com:443max_fails=3fail_timeout=60s;serverbbb.tar.com:443backup;}server......
  • 使用 diffusers 训练你自己的 ControlNet
    简介ControlNet这个神经网络模型使得用户可以通过施加额外条件,细粒度地控制扩散模型的生成过程。这一技术最初由AddingConditionalControltoText-to-ImageDiffusionModels这篇论文提出,并很快地风靡了扩散模型的开源社区。作者开源了8个不同的模型,使得用户可以用8种......
  • DOM:让一个元素跟随鼠标移动而移动
    <!DOCTYPEhtml><htmllang="en"><head>  <metacharset="UTF-8">  <metahttp-equiv="X-UA-Compatible"content="IE=edge">  <metaname="viewport"content="width=......
  • Information Gathering - SubDomains Finding
    SubDomainsFindingByOnlineWebsiteshttps://crt.sh/crt.sh通过证书记录查询子域名,%为通配符ByOpensourceToolssublist3r安装在kali中使用aptinstallsublist3r即可安装,或在githubhttps://github.com/aboul3la/Sublist3r下载运行apt安装直接输入sublist3r......