什么是虚拟DOM?如何实现一个虚拟DOM?

news2025/1/10 3:55:36

面试官:什么是虚拟DOM?如何实现一个虚拟DOM?说说你的思路

一、什么是虚拟DOM

虚拟 DOM (Virtual DOM )这个概念相信大家都不陌生,从 ReactVue ,虚拟 DOM 为这两个框架都带来了跨平台的能力(React-NativeWeex

实际上它只是一层对真实DOM的抽象,以JavaScript 对象 (VNode 节点) 作为基础的树,用对象的属性来描述节点,最终可以通过一系列操作使这棵树映射到真实环境上

Javascript对象中,虚拟DOM 表现为一个 Object 对象。并且最少包含标签名 (tag)、属性 (attrs) 和子元素对象 (children) 三个属性,不同框架对这三个属性的名命可能会有差别

创建虚拟DOM就是为了更好将虚拟的节点渲染到页面视图中,所以虚拟DOM对象的节点与真实DOM的属性一一照应

vue中同样使用到了虚拟DOM技术

定义真实DOM

<div id="app">
    <p class="p">节点内容</p>
    <h3>{{ foo }}</h3>
</div>

实例化vue

const app = new Vue({
    el:"#app",
    data:{
        foo:"foo"
    }
})

观察renderrender,我们能得到虚拟DOM

(function anonymous(
) {
	with(this){return _c('div',{attrs:{"id":"app"}},[_c('p',{staticClass:"p"},
					  [_v("节点内容")]),_v(" "),_c('h3',[_v(_s(foo))])])}})

通过VNodevue可以对这颗抽象树进行创建节点,删除节点以及修改节点的操作, 经过diff算法得出一些需要修改的最小单位,再更新视图,减少了dom操作,提高了性能

二、为什么需要虚拟DOM

DOM是很慢的,其元素非常庞大,页面的性能问题,大部分都是由DOM操作引起的

真实的DOM节点,哪怕一个最简单的div也包含着很多属性,可以打印出来直观感受一下:

由此可见,操作DOM的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户的体验

举个例子:

你用传统的原生apijQuery去操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程

当你在一次操作时,需要更新10个DOM节点,浏览器没这么智能,收到第一个更新DOM请求后,并不知道后续还有9次更新操作,因此会马上执行流程,最终执行10次流程

而通过VNode,同样更新10个DOM节点,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地的一个js对象中,最终将这个js对象一次性attachDOM树上,避免大量的无谓计算

很多人认为虚拟 DOM 最大的优势是 diff 算法,减少 JavaScript 操作真实 DOM 的带来的性能消耗。虽然这一个虚拟 DOM 带来的一个优势,但并不是全部。虚拟 DOM 最大的优势在于抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的 DOM,可以是安卓和 IOS 的原生组件,可以是近期很火热的小程序,也可以是各种GUI

三、如何实现虚拟DOM

首先可以看看vueVNode的结构

源码位置:src/core/vdom/vnode.js

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  functionalContext: Component | void; // only for functional component root nodes
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions
  ) {
    /*当前节点的标签名*/
    this.tag = tag
    /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
    this.data = data
    /*当前节点的子节点,是一个数组*/
    this.children = children
    /*当前节点的文本*/
    this.text = text
    /*当前虚拟节点对应的真实dom节点*/
    this.elm = elm
    /*当前节点的名字空间*/
    this.ns = undefined
    /*编译作用域*/
    this.context = context
    /*函数化组件作用域*/
    this.functionalContext = undefined
    /*节点的key属性,被当作节点的标志,用以优化*/
    this.key = data && data.key
    /*组件的option选项*/
    this.componentOptions = componentOptions
    /*当前节点对应的组件的实例*/
    this.componentInstance = undefined
    /*当前节点的父节点*/
    this.parent = undefined
    /*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
    this.raw = false
    /*静态节点标志*/
    this.isStatic = false
    /*是否作为跟节点插入*/
    this.isRootInsert = true
    /*是否为注释节点*/
    this.isComment = false
    /*是否为克隆节点*/
    this.isCloned = false
    /*是否有v-once指令*/
    this.isOnce = false
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next https://github.com/answershuto/learnVue*/
  get child (): Component | void {
    return this.componentInstance
  }
}

这里对VNode进行稍微的说明:

  • 所有对象的 context 选项都指向了 Vue 实例
  • elm 属性则指向了其相对应的真实 DOM 节点

vue是通过createElement生成VNode

源码位置:src/core/vdom/create-element.js

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}

上面可以看到createElement 方法实际上是对 _createElement 方法的封装,对参数的传入进行了判断

export function _createElement(
    context: Component,
    tag?: string | Class<Component> | Function | Object,
    data?: VNodeData,
    children?: any,
    normalizationType?: number
): VNode | Array<VNode> {
    if (isDef(data) && isDef((data: any).__ob__)) {
        process.env.NODE_ENV !== 'production' && warn(
            `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
            'Always create fresh vnode data objects in each render!',
            context`
        )
        return createEmptyVNode()
    }
    // object syntax in v-bind
    if (isDef(data) && isDef(data.is)) {
        tag = data.is
    }
    if (!tag) {
        // in case of component :is set to falsy value
        return createEmptyVNode()
    }
    ... 
    // support single function children as default scoped slot
    if (Array.isArray(children) &&
        typeof children[0] === 'function'
    ) {
        data = data || {}
        data.scopedSlots = { default: children[0] }
        children.length = 0
    }
    if (normalizationType === ALWAYS_NORMALIZE) {
        children = normalizeChildren(children)
    } else if ( === SIMPLE_NORMALIZE) {
        children = simpleNormalizeChildren(children)
    }
	// 创建VNode
    ...
}

可以看到_createElement接收5个参数:

  • context 表示 VNode 的上下文环境,是 Component 类型

  • tag 表示标签,它可以是一个字符串,也可以是一个 Component

  • data 表示 VNode 的数据,它是一个 VNodeData 类型

  • children 表示当前 VNode 的子节点,它是任意类型的

  • normalizationType 表示子节点规范的类型,类型不同规范的方法也就不一样,主要是参考 render 函数是编译生成的还是用户手写的

根据normalizationType 的类型,children会有不同的定义

if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
} else if ( === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
}

simpleNormalizeChildren方法调用场景是 render 函数是编译生成的

normalizeChildren方法调用场景分为下面两种:

  • render 函数是用户手写的
  • 编译 slotv-for 的时候会产生嵌套数组

无论是simpleNormalizeChildren还是normalizeChildren都是对children进行规范(使children 变成了一个类型为 VNodeArray),这里就不展开说了

规范化children的源码位置在:src/core/vdom/helpers/normalzie-children.js

在规范化children后,就去创建VNode

let vnode, ns
// 对tag进行判断
if (typeof tag === 'string') {
  let Ctor
  ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
  if (config.isReservedTag(tag)) {
    // 如果是内置的节点,则直接创建一个普通VNode
    vnode = new VNode(
      config.parsePlatformTagName(tag), data, children,
      undefined, undefined, context
    )
  } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
    // component
    // 如果是component类型,则会通过createComponent创建VNode节点
    vnode = createComponent(Ctor, data, context, children, tag)
  } else {
    vnode = new VNode(
      tag, data, children,
      undefined, undefined, context
    )
  }
} else {
  // direct component options / constructor
  vnode = createComponent(tag, data, context, children)
}

createComponent同样是创建VNode

源码位置:src/core/vdom/create-component.js

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }
 // 构建子类构造函数 
  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeof Ctor !== 'function') {
    if (process.env.NODE_ENV !== 'production') {
      warn(`Invalid Component definition: ${String(Ctor)}`, context)
    }
    return
  }

  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
    if (Ctor === undefined) {
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  data = data || {}

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor)

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn

  if (isTrue(Ctor.options.abstract)) {
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // 安装组件钩子函数,把钩子函数合并到data.hook中
  installComponentHooks(data)

  //实例化一个VNode返回。组件的VNode是没有children的
  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode)
  }

  return vnode
}

稍微提下createComponent生成VNode的三个关键流程:

  • 构造子类构造函数Ctor
  • installComponentHooks安装组件钩子函数
  • 实例化 vnode

小结

createElement 创建 VNode 的过程,每个 VNodechildrenchildren 每个元素也是一个VNode,这样就形成了一个虚拟树结构,用于描述真实的DOM树结构

参考文献

  • https://ustbhuangyi.github.io/vue-analysis/v2/data-driven/create-element.html#children-%E7%9A%84%E8%A7%84%E8%8C%83%E5%8C%96
  • https://juejin.cn/post/6876711874050818061

更多前端资源==> GitHub

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1388572.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

达摩研究院Paraformer语音识别-中文-通用-16k

原文&#xff1a;https://github.com/alibaba-damo-academy/FunASR/blob/main/runtime/readme_cn.md FunASR软件包路线图 English Version&#xff08;docs&#xff09; FunASR是由阿里巴巴通义实验室语音团队开源的一款语音识别基础框架&#xff0c;集成了语音端点检测、语…

第09章_异常处理拓展练习(代码阅读题,简答题,编程题)

文章目录 第09章_异常处理拓展练习代码阅读题1、阅读代码&#xff0c;分析结果2、阅读代码&#xff0c;分析结果3、阅读代码&#xff0c;分析结果4、阅读代码&#xff0c;分析结果5、阅读代码&#xff0c;分析结果6、阅读代码&#xff0c;分析结果7、阅读代码&#xff0c;分析结…

插件和工具汇总

插件和工具汇总 【一】MyBatis Log插件【二】热部署&#xff08;1&#xff09;适用于IntelliJ IDEA 2021.X以上版本&#xff08;2&#xff09;适用于IntelliJ IDEA 2021.X以下版本 【三】一些快捷键 【一】MyBatis Log插件 能够自动拼接参数生成执行的SQL语句&#xff0c;可以…

蔚来,为何要狂“炫技”?

有人说&#xff0c;蔚来NIO Day2023就是“炫技”的一晚。 当晚&#xff0c;蔚来发布一款更小众的新车型——ET9&#xff0c;定位D级行政旗舰车型&#xff0c;发布会大部分时间在介绍核心自研技术&#xff0c;包括首颗自研智能驾驶芯片——神玑NX9031、全域900V高压架构、天行智…

uniapp微信小程序投票系统实战 (SpringBoot2+vue3.2+element plus ) -投票帖子明细实现

锋哥原创的uniapp微信小程序投票系统实战&#xff1a; uniapp微信小程序投票系统实战课程 (SpringBoot2vue3.2element plus ) ( 火爆连载更新中... )_哔哩哔哩_bilibiliuniapp微信小程序投票系统实战课程 (SpringBoot2vue3.2element plus ) ( 火爆连载更新中... )共计21条视频…

STM32CubeMX配置STM32G071UART+DMA收发数据(HAL库开发)

时钟配置HSI主频配置64M 配置好串口&#xff0c;选择异步模式 配置DMA TX,RX,选择循环模式。 NVIC中勾选使能中断 勾选生成独立的.c和h文件 配置好需要的开发环境并获取代码 串口重定向勾选Use Micro LIB main.c文件修改 增加头文件和串口重定向 #include <string.h&g…

微软推出Copilot Pro高级订阅服务;使用大语言模型处理音频数据;AI新工具Summify-用于总结YouTube视频的人工智能工具

&#x1f989; AI新闻 &#x1f680; 微软推出Copilot Pro高级订阅服务&#xff0c;扩展适用范围到更多设备和应用 摘要&#xff1a;微软为其Copilot助手推出了新的高级订阅服务Copilot Pro&#xff0c;该服务每月收费20美元&#xff0c;支持Windows PC、Web、App使用&#x…

Win11提示"由于启动计算机时出现了页面文件配置问题"解决方法

1.右键此电脑&#xff0c;选择属性 2.选择高级系统设置 3.选择性能 ---设置 4.选择高级--更改 5.勾选...&#xff0c;确定 本文章仅供学习交流使用&#xff0c;如有侵权&#xff0c;联系删除

二叉树的四种遍历详解(先序,中序,后序,层次)

目录 引言&#xff1a; 1&#xff1a;先序遍历 2.中序遍历 3.后续遍历 4.层次遍历 先序中序后序例题&#xff1a; 例题1&#xff1a; 例题2&#xff1a; 层次遍历例题&#xff1a; 结语&#xff1a; 引言&#xff1a; 二叉树的遍历的概念&#xff1a;二叉树遍历是指…

GZ075 云计算应用赛题第10套

2023年全国职业院校技能大赛&#xff08;高职组&#xff09; “云计算应用”赛项赛卷10 某企业根据自身业务需求&#xff0c;实施数字化转型&#xff0c;规划和建设数字化平台&#xff0c;平台聚焦“DevOps开发运维一体化”和“数据驱动产品开发”&#xff0c;拟采用开源OpenS…

管理configration Manager 控制台

一、configration manager客户端安装 1.点击管理&#xff0c;扩展站点配置&#xff0c;点击站点 2.右键主站点&#xff0c;选择客户端安装设置 3.出现客户端请求安装属性对话框后&#xff0c;点击常规。点击启用自动站点范围客户端请求安装 4.点击账户 5.点击安装属性&#x…

Windows 10 Microsoft Edge浏览器无法打

早上到公司发现Microsoft Edge浏览器突然间打不开了&#xff0c;怎么点击快捷方式都不行&#xff0c;然后尝试了以下方法都不行&#xff1a; 1、右键使用“管理者身份运行”打开&#xff0c;无效。 2、账户注销&#xff0c;再次点击快捷方式。 3、重启电脑无效。 然后快捷方式右…

轻松制作商业画册的秘籍

对于许多商业人士来说&#xff0c;制作一本精美的商业画册是一个重要的任务&#xff0c;它不仅代表了公司的形象&#xff0c;也是与客户和潜在客户建立联系的重要工具。然而&#xff0c;制作一本商业画册并不像看起来那么简单。有许多因素需要考虑&#xff0c;包括设计、布局、…

当代大学生是怎么被废掉的?

中式教育以应试为核心&#xff0c;强调知识的灌输和学生被动接受。随着社会的发展&#xff0c;中式教育的短板逐渐显现&#xff0c;创新能力的缺乏、对记忆的过度依赖、忽视个体差异等问题日益突出。 建议所有大学生都能去看看《上海交通大学生存手册》&#xff0c;它道出了中…

前任开发在代码里下毒了,支付下单居然没加幂等

分享是最有效的学习方式。 故事 又是一个风和日丽没好的一天&#xff0c;小猫戴着耳机&#xff0c;安逸地听着音乐&#xff0c;撸着代码&#xff0c;这种没有会议的日子真的是巴适得板。 不料祸从天降&#xff0c;组长火急火燎地跑过来找到了小猫。“快排查一下&#xff0c;目…

C++内存管理机制(侯捷)笔记3

C内存管理机制&#xff08;侯捷&#xff09; 本文是学习笔记&#xff0c;仅供个人学习使用。如有侵权&#xff0c;请联系删除。 参考链接 Youtube: 侯捷-C内存管理机制 Github课程视频、PPT和源代码: https://github.com/ZachL1/Bilibili-plus 第三讲&#xff1a;malloc和…

如何手写一个RPC?

在学习 RPC 框架之前&#xff0c;我们先来手写一个RPC。 我们在学习的过程中&#xff0c;一定要做到知其然&#xff0c;还要知其所以然。 架构演进 单体架构 要知道&#xff0c;在以前单体架构的时候&#xff0c;会将所有的应用功能都集中在一个服务当中。 单体架构初始开发…

学习Java API(三):基础知识点一文通✅

推荐阅读 智能化校园&#xff1a;深入探讨云端管理系统设计与实现&#xff08;一&#xff09; 智能化校园&#xff1a;深入探讨云端管理系统设计与实现&#xff08;二&#xff09; 文章目录 推荐阅读Date 类SimpleDateFormatCalendar Date 类 java.util 包提供了 Date 类来封…

UI自动化测试框架

文章目录 UI自动化基础什么是UI自动化测试框架UI自动化测试框架的模式数据驱动测试框架关键字驱动测试框架行为驱动测试框架 UI自动化测试框架的作用UI自动化测试框架的核心思想UI自动化测试框架的步骤UI自动化测试框架的构成UtilsLog.javaReadProperties.Java coreBaseTest.ja…

屏幕录制软件有哪些?强烈推荐5款免费好用录屏软件

录制全屏视频是制作视频教程、评论、游戏等内容的绝佳方式。它可以包括您计算机屏幕上的任何活动以及您的音频和网络摄像头。特别是&#xff0c;屏幕录像机对喜欢创建软件评论的视频博主很有帮助。如果您是其中之一&#xff0c;那么好消息是您可以在网络上找到很多屏幕录制应用…