面试官:什么是虚拟DOM?如何实现一个虚拟DOM?说说你的思路
🎬 岸边的风:个人主页
🔥 个人专栏 :《 VUE 》 《 javaScript 》
⛺️ 生活的理想,就是为了理想的生活 !
目录
一、什么是虚拟DOM
二、为什么需要虚拟DOM
三、如何实现虚拟DOM
小结
一、什么是虚拟DOM
虚拟 DOM (Virtual DOM
)这个概念相信大家都不陌生,从 React
到 Vue
,虚拟 DOM
为这两个框架都带来了跨平台的能力(React-Native
和 Weex
)
实际上它只是一层对真实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"}
})
观察render
的render
,我们能得到虚拟DOM
(function anonymous(
) {with(this){return _c('div',{attrs:{"id":"app"}},[_c('p',{staticClass:"p"},[_v("节点内容")]),_v(" "),_c('h3',[_v(_s(foo))])])}})
通过VNode
,vue
可以对这颗抽象树进行创建节点,删除节点以及修改节点的操作, 经过diff
算法得出一些需要修改的最小单位,再更新视图,减少了dom
操作,提高了性能
二、为什么需要虚拟DOM
DOM
是很慢的,其元素非常庞大,页面的性能问题,大部分都是由DOM
操作引起的
真实的DOM
节点,哪怕一个最简单的div
也包含着很多属性,可以打印出来直观感受一下:
由此可见,操作DOM
的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户的体验
举个例子:
你用传统的原生api
或jQuery
去操作DOM
时,浏览器会从构建DOM
树开始从头到尾执行一遍流程
当你在一次操作时,需要更新10个DOM
节点,浏览器没这么智能,收到第一个更新DOM
请求后,并不知道后续还有9次更新操作,因此会马上执行流程,最终执行10次流程
而通过VNode
,同样更新10个DOM
节点,虚拟DOM
不会立即操作DOM
,而是将这10次更新的diff
内容保存到本地的一个js
对象中,最终将这个js
对象一次性attach
到DOM
树上,避免大量的无谓计算
很多人认为虚拟 DOM 最大的优势是 diff 算法,减少 JavaScript 操作真实 DOM 的带来的性能消耗。虽然这一个虚拟 DOM 带来的一个优势,但并不是全部。虚拟 DOM 最大的优势在于抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的 DOM,可以是安卓和 IOS 的原生组件,可以是近期很火热的小程序,也可以是各种GUI
三、如何实现虚拟DOM
首先可以看看vue
中VNode
的结构
源码位置: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 scopefunctionalContext: Component | void; // only for functional component root nodeskey: string | number | void;componentOptions: VNodeComponentOptions | void;componentInstance: Component | void; // component instanceparent: VNode | void; // component placeholder noderaw: boolean; // contains raw HTML? (server only)isStatic: boolean; // hoisted static nodeisRootInsert: boolean; // necessary for enter transition checkisComment: 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 = childrenchildren = datadata = 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-bindif (isDef(data) && isDef(data.is)) {tag = data.is}if (!tag) {// in case of component :is set to falsy valuereturn createEmptyVNode()}... // support single function children as default scoped slotif (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
函数是用户手写的- 编译
slot
、v-for
的时候会产生嵌套数组
无论是simpleNormalizeChildren
还是normalizeChildren
都是对children
进行规范(使children
变成了一个类型为 VNode
的 Array
),这里就不展开说了
规范化children
的源码位置在:src/core/vdom/helpers/normalzie-children.js
在规范化children
后,就去创建VNode
let vnode, ns
// 对tag进行判断
if (typeof tag === 'string') {let Ctorns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)if (config.isReservedTag(tag)) {// 如果是内置的节点,则直接创建一个普通VNodevnode = 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 / constructorvnode = 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 constructorif (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 componentlet asyncFactoryif (isUndef(Ctor.cid)) {asyncFactory = CtorCtor = 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 creationresolveConstructorOptions(Ctor)// transform component v-model data into props & eventsif (isDef(data.model)) {transformModel(Ctor.options, data)}// extract propsconst propsData = extractPropsFromVNodeData(data, Ctor, tag)// functional componentif (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 listenersconst listeners = data.on// replace with listeners with .native modifier// so it gets processed during parent component patch.data.on = data.nativeOnif (isTrue(Ctor.options.abstract)) {const slot = data.slotdata = {}if (slot) {data.slot = slot}}// 安装组件钩子函数,把钩子函数合并到data.hook中installComponentHooks(data)//实例化一个VNode返回。组件的VNode是没有children的const name = Ctor.options.name || tagconst 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
的过程,每个 VNode
有 children
,children
每个元素也是一个VNode
,这样就形成了一个虚拟树结构,用于描述真实的DOM
树结构
相关文章:

面试官:什么是虚拟DOM?如何实现一个虚拟DOM?说说你的思路
🎬 岸边的风:个人主页 🔥 个人专栏 :《 VUE 》 《 javaScript 》 ⛺️ 生活的理想,就是为了理想的生活 ! 目录 一、什么是虚拟DOM 二、为什么需要虚拟DOM 三、如何实现虚拟DOM 小结 一、什么是虚拟DOM 虚拟 DOM (…...

Ubuntu安装中文拼音输入法
ubuntu安装中文拼音输入法 ubuntu版本为23.04 1、安装中文语言包 首先安装中文输入法必须要让系统支持中文语言,可以在 Language Support 中安装中文语言包。 添加或删除语音选项,添加中文简体,然后会有Applying changes的对话框&#x…...

高端知识竞赛中用到的软件和硬件有哪些
现在单位搞知识竞赛,已不满足于用PPT放题,找几个简单的抢答器、计分牌弄一下了,而是对现场效果和科技感要求更高了。大屏要分主屏侧屏,显示内容要求丰富炫酷;选手和评委也要用到平板等设备;计分要大气些&am…...
Vue 3.3 发布
本文为翻译 原文地址:宣布推出 Vue 3.3 |The Vue Point (vuejs.org) 今天我们很高兴地宣布 Vue 3.3 “Rurouni Kenshin” 的发布! 此版本侧重于开发人员体验改进 - 特别是 TypeScript 的 SFC <script setup> 使用。结合 Vue Language Tools&…...
算法|图论 3
LeetCode 130- 被围绕的区域 题目链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台 题目描述:给你一个 m x n 的矩阵 board ,由若干字符 X 和 O ,找到所有被 X 围绕的区域,并将这些区域…...

【数据结构】二叉树的层序遍历(四)
目录 一,层序遍历概念 二,层序遍历的实现 1,层序遍历的实现思路 2,创建队列 Queue.h Queue.c 3,创建二叉树 BTree.h BTree.c 4,层序遍历的实现 一,层序遍历概念 层序遍历:除了先序…...

macOS文件差异比较最佳工具:Beyond Compare 4
Beyond Compare for mac是一款Scooter Software研发的文件同步对比工具。你可以选择针对多字节的文本、文件夹、源代码,甚至是支持比对adobe文件、pdf文件或是整个驱动器,检查其文件大小、名称、日期等信息。你也可以选择使用Beyond Compare合并两个不同…...

Windows+Pycharm 如何创建虚拟环境
当我们开发一个别人的项目的时候,因为项目里有很多特有的包,比如 Pyqt5.我们不想破坏电脑上原来的包版本,这个时候,新建一个虚拟环境,专门针对这个项目就很有必要了. 简略步骤: 1.新建虚拟环境 1.打开 pycharm 终端(Terminal)安装虚拟环境工具: pip install virtualenv2.创…...

vant 按需导入 vue2
vant 按需导入 vue2 1、通过npm安装 # Vue 3 项目,安装最新版 Vant: npm i vant -S# Vue 2 项目,安装 Vant 2: npm i vantlatest-v2 -S2、自动按需引入组件 babel-plugin-import 是一款 babel 插件,它会在编译过程中…...
Java手写分治算法和分治算法应用拓展案例
Java手写分治算法和分治算法应用拓展案例 1. 算法思维导图 以下是用Mermanid代码表示的分治算法的实现原理: #mermaid-svg-nvJwIm97kPHEXQOR {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-nvJwIm97kP…...
学习 CodeWhisperer 的一些总结
目前一些常见的的 AI 工具 GitHub Copilot:GitHub 与 OpenAI 合作开发的一个人工智能助手。 Codeium:是一个免费的人工智能驱动的代码生成工具 Tabnine:一个自动代码生成工具,免费版本非常有限,只提供简短的代码完成…...

JavaScript 中的 `this` 指向问题与其在加密中的应用
JS中的 this 关键字是一个非常重要的概念,它在不同情况下会指向不同的对象或值。在本文中,我们将深入探讨 JavaScript 中 this 的各种情况,并思考如何将其应用于 JS加密中的一些有趣用途。 1. 全局上下文中的 this 在全局上下文中ÿ…...

深入理解算法的时间复杂度
文章目录 时间复杂度的定义时间复杂度的分类时间复杂度分析常见数据结构和算法的时间复杂度常见数据结构常见算法 常见排序算法说明冒泡排序(Bubble Sort)快速排序(Quick Sort)归并排序(Merge Sort)堆排序(Heap Sort) 时间复杂度的定义 时间复杂度就是一种用来描述算法在输入规…...

2023年度教育部人文社会科学研究一般项目评审结果,已公布!
【SciencePub学术】 9月15日,教育部社科司公示了2023年度教育部人文社会科学研究一般项目评审结果,共3482项。 其中,规划基金、青年基金、自筹经费项目共3029项通过专家评审;西部和边疆地区项目200项,新疆项目20项&a…...

十一、MySql的事务(上)
文章目录 一、引入(一)CURD不加控制,会有什么问题?(二)CURD满足什么属性,能解决上述问题? 二、什么是事务?三、事务的特性(一)原子性:…...
时间序列分析1--生成和导出时间序列数据
时间序列数据的生成 直接录入 1.行录入 ts.(price,startc(2015,1),frequency 12) # price为时间序列变量,start为起始读入时间 frequncy指定每年读入的数据的频率,frequncy4为季度数据、frequncy52为星期数据 2.列录入 scan() 1:101 ....6:7 7:…...

HarmonyOS应用开发—资源分类与访问
应用开发过程中,经常需要用到颜色、字体、间距、图片等资源,在不同的设备或配置中,这些资源的值可能不同。 应用资源:借助资源文件能力,开发者在应用中自定义资源,自行管理这些资源在不同的设备或配置中的表…...

C++中的转换构造函数
在 C/C++ 中,不同的数据类型之间可以相互转换。无需用户指明如何转换的称为自动类型转换(隐式类型转换),需要用户显式地指明如何转换的称为强制类型转换。 自动类型转换示例: int a = 6;a = 7.5 + a; 编译器对 7.5 是作为 double 类型处理的,在求解表达式时,先将 a 转换…...

JSP ssm 特殊人群防走失系统myeclipse开发mysql数据库springMVC模式java编程计算机网页设计
一、源码特点 JSP ssm 特殊人群防走失系统是一套完善的web设计系统(系统采用SSM框架进行设计开发,springspringMVCmybatis),对理解JSP java编程开发语言有帮助,系统具有完整的源 代码和数据库,系统主要…...

怎么实现一个登录时需要输入验证码的功能
今天给项目换了一个登录页面,而这个登录页面设计了验证码,于是想着把这个验证码功能实现一下吧。 这篇文章就如何实现登录时的验证码的验证功能结合代码进行详细地介绍,以及介绍功能实现的思路。 目录 页面效果 实现思路 生成验证码的控制…...
uni-app学习笔记二十二---使用vite.config.js全局导入常用依赖
在前面的练习中,每个页面需要使用ref,onShow等生命周期钩子函数时都需要像下面这样导入 import {onMounted, ref} from "vue" 如果不想每个页面都导入,需要使用node.js命令npm安装unplugin-auto-import npm install unplugin-au…...
深入浅出:JavaScript 中的 `window.crypto.getRandomValues()` 方法
深入浅出:JavaScript 中的 window.crypto.getRandomValues() 方法 在现代 Web 开发中,随机数的生成看似简单,却隐藏着许多玄机。无论是生成密码、加密密钥,还是创建安全令牌,随机数的质量直接关系到系统的安全性。Jav…...
聊一聊接口测试的意义有哪些?
目录 一、隔离性 & 早期测试 二、保障系统集成质量 三、验证业务逻辑的核心层 四、提升测试效率与覆盖度 五、系统稳定性的守护者 六、驱动团队协作与契约管理 七、性能与扩展性的前置评估 八、持续交付的核心支撑 接口测试的意义可以从四个维度展开,首…...
C++八股 —— 单例模式
文章目录 1. 基本概念2. 设计要点3. 实现方式4. 详解懒汉模式 1. 基本概念 线程安全(Thread Safety) 线程安全是指在多线程环境下,某个函数、类或代码片段能够被多个线程同时调用时,仍能保证数据的一致性和逻辑的正确性…...

Linux --进程控制
本文从以下五个方面来初步认识进程控制: 目录 进程创建 进程终止 进程等待 进程替换 模拟实现一个微型shell 进程创建 在Linux系统中我们可以在一个进程使用系统调用fork()来创建子进程,创建出来的进程就是子进程,原来的进程为父进程。…...

关键领域软件测试的突围之路:如何破解安全与效率的平衡难题
在数字化浪潮席卷全球的今天,软件系统已成为国家关键领域的核心战斗力。不同于普通商业软件,这些承载着国家安全使命的软件系统面临着前所未有的质量挑战——如何在确保绝对安全的前提下,实现高效测试与快速迭代?这一命题正考验着…...
Java求职者面试指南:计算机基础与源码原理深度解析
Java求职者面试指南:计算机基础与源码原理深度解析 第一轮提问:基础概念问题 1. 请解释什么是进程和线程的区别? 面试官:进程是程序的一次执行过程,是系统进行资源分配和调度的基本单位;而线程是进程中的…...

wpf在image控件上快速显示内存图像
wpf在image控件上快速显示内存图像https://www.cnblogs.com/haodafeng/p/10431387.html 如果你在寻找能够快速在image控件刷新大图像(比如分辨率3000*3000的图像)的办法,尤其是想把内存中的裸数据(只有图像的数据,不包…...
Vue 模板语句的数据来源
🧩 Vue 模板语句的数据来源:全方位解析 Vue 模板(<template> 部分)中的表达式、指令绑定(如 v-bind, v-on)和插值({{ }})都在一个特定的作用域内求值。这个作用域由当前 组件…...

C++--string的模拟实现
一,引言 string的模拟实现是只对string对象中给的主要功能经行模拟实现,其目的是加强对string的底层了解,以便于在以后的学习或者工作中更加熟练的使用string。本文中的代码仅供参考并不唯一。 二,默认成员函数 string主要有三个成员变量,…...