【Vue3】源码解析-虚拟DOM

【Vue3】源码解析

      • 系列文章
      • 什么是虚拟DOM
      • Vue 3虚拟DOM
      • 获取`<template>`内容
      • 生成AST语法树
      • 生成render方法字符串
      • 得到最终VNode对象

系列文章

【Vue3】源码解析-前置
【Vue3】源码解析-响应式原理
【Vue3】源码解析-虚拟DOM

什么是虚拟DOM

在浏览器中,HTML页面由基本的DOM树来组成的,当其中一部分发生变化时,其实就是对应某个DOM节点发生了变化,当DOM节点发生变化时就会触发对应的重绘或者重排,当过多的重绘和重排在短时间内发生时,就会可能引起页面的卡顿,所以改变DOM是有一些代价的,那么如何优化DOM变化的次数以及在合适的时机改变DOM就是开发者需要注意的事情。

虚拟DOM就是为了解决上述浏览器性能问题而被设计出来的。当一次操作中有10次更新DOM的动作,虚拟DOM不会立即操作DOM,而是和原本的DOM进行对比,将这10次更新的变化部分内容保存到内存中,最终一次性的应用在到DOM树上,再进行后续操作,避免大量无谓的计算量。

虚拟DOM实际上就是采用JavaScript对象来存储DOM节点的信息,将DOM的更新变成对象的修改,并且这些修改计算在内存中发生,当修改完成后,再将JavaScript转换成真实的DOM节点,交给浏览器,从而达到性能的提升。 例如下面一段DOM节点,如下代码所示:

<div id="app"><p class="text">Hello</p>
</div>

转换成一般的虚拟DOM对象结构,如下代码所示:

{tag: 'div',props: {id: 'app'},chidren: [{tag: 'p',props: {className: 'text'},chidren: ['Hello']}]
}

上面这段代码就是一个基本的虚拟DOM,但是他并非是Vue中使用的虚拟DOM结构,因为Vue要复杂的多。

Vue 3虚拟DOM

在Vue中,我们写在<template>标签内的内容都属于DOM节点,这部分内容会被最终转换成Vue中的虚拟DOM对象VNode,其中的步骤比较复杂,主要有以下几个过程:

  • 抽取<template>内容进行compile编译。
  • 得到AST语法树,并生成render方法。
  • 执行render方法得到VNode对象。
  • VNode转换真实DOM并渲染到页面。

完整流程如下图:
在这里插入图片描述
我们以一个简单的demo为例子,在Vue 3的源码里去寻找,到底是如何一步一步进行了,demo如下代码所示:

<div id="app"><div>{{name}}</div><p>123</p>
</div>
Vue.createApp({data(){return {name : 'abc'}}
}).mount("#app")

上面代码中,data中定义了一个响应式数据name,并在<template>中使用插值表达式{{name}}进行使用,还有一个静态节点<p>123</p>

获取<template>内容

调用createApp()方法,会进入到源码packages/runtime-dom/src/index.ts里面的createApp()方法,如下代码所示:

export const createApp = ((...args) => {const app = ensureRenderer().createApp(...args)...app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {if (!isFunction(component) && !component.render && !component.template) {// 将#app绑定的HTML内容赋值给template项上component.template = container.innerHTML// 调用mount方法渲染const proxy = mount(container, false, container instanceof SVGElement)return proxy}...return app
}) as CreateAppFunction<Element>

对于根组件来说,<template>的内容由挂载的#app元素里面的内容组成,如果项目是采用npm和Vue Cli+Webpack这种前端工程化的方式,那么对于<template>的内容则主要由对应的loader在构建时对文件进行处理来获取,这和在浏览器运行时的处理方式是不一样的。

生成AST语法树

在得到<template>后,就依据内容生成AST语法树。抽象语法树(Abstract Syntax Tree,AST),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;而类似于if-condition-then这样的条件跳转语句,可以使用带有三个分支的节点来表示。如下代码所示:

while b ≠ 0if a > b
a := a − belse
b := b − a
return a

如果将上述代码转换成广泛意义上的语法树,如图所示。
在这里插入图片描述
对于<template>的内容,其大部分是由DOM组成,但是也会有if-condition-then这样的条件语句,例如v-if,v-for指令等等,在Vue 3中,这部分逻辑在源码packages\compiler-core\src\compile.ts中baseCompile方法,核心代码如下所示:

export function baseCompile(template: string | RootNode,options: CompilerOptions = {}
): CodegenResult {...// 通过template生成ast树结构const ast = isString(template) ? baseParse(template, options) : template...// 转换transform(ast,...)return generate(ast,extend({}, options, {prefixIdentifiers}))
}

baseCompile方法主要做了以下事情:

  • 生成Vue中的AST对象。
  • 将AST对象作为参数传入transform函数,进行转换。
  • 将转换后的AST对象作为参数传入generate函数,生成render函数。

其中,baseParse方法用来创建AST对象,在Vue 3中,AST对象是一个RootNode类型的树状结构,在源码packages\compiler-core\src\ast.ts中,其结构如下代码所示:

export function createRoot(children: TemplateChildNode[],loc = locStub
): RootNode {return {type: NodeTypes.ROOT, // 元素类型children, // 子元素helpers: [],// 帮助函数components: [],// 子组件directives: [], // 指令hoists: [],// 标识静态节点imports: [],cached: 0, // 缓存标志位temps: 0,codegenNode: undefined,// 存储生成render函数字符串loc // 描述元素在AST树的位置信息}
}

其中,children存储的时后代元素节点的数据,这就构成一个AST树结构,type表示元素的类型NodeType,主要分为HTML普通类型和Vue指令类型等,常见的有以下几种:

ROOT,  // 根元素 0
ELEMENT, // 普通元素 1
TEXT, // 文本元素 2
COMMENT, // 注释元素 3
SIMPLE_EXPRESSION, // 表达式 4
INTERPOLATION, // 插值表达式 {{ }} 5
ATTRIBUTE, // 属性 6
DIRECTIVE, // 指令 7
IF, // if节点 9
JS_CALL_EXPRESSION, // 方法调用 14
...

hoists是一个数组,用来存储一些可以静态提升的元素,在后面的transform会将静态元素和响应式元素分开创建,这也是Vue 3中优化的体现,codegenNode则用来存储最终生成的render方法的字符串,loc表示元素在AST树的位置信息。

在生成AST树时,Vue 3在解析<template>内容时,会用一个栈stack来保存解析到的元素标签。当它遇到开始标签时,会将这个标签推入栈,遇到结束标签时,将刚才的标签弹出栈。它的作用是保存当前已经解析了,但还没解析完的元素标签。这个栈还有另一个作用,在解析到某个字节点时,通过stack[stack.length - 1]可以获取它的父元素。

demo代码中生成的AST语法树如下图所示。
在这里插入图片描述

生成render方法字符串

在得到AST对象后,会进入transform方法,在源码packages\compiler-core\src\transform.ts中,其核心代码如下所示:

export function transform(root: RootNode, options: TransformOptions) {
// 数据组装  
const context = createTransformContext(root, options)// 转换代码traverseNode(root, context)// 静态提升if (options.hoistStatic) {hoistStatic(root, context)}// 服务端渲染if (!options.ssr) {createRootCodegen(root, context)}// 透传元信息root.helpers = [...context.helpers.keys()]root.components = [...context.components]root.directives = [...context.directives]root.imports = context.importsroot.hoists = context.hoistsroot.temps = context.tempsroot.cached = context.cachedif (__COMPAT__) {root.filters = [...context.filters!]}
}

transform方法主要是对AST进行进一步转化,为generate函数生成render方法做准备,主要做了以下事情:

  • traverseNode方法将会递归的检查和解析AST元素节点的属性,例如结合helpers方法对@click等事件添加对应的方法和事件回调,对插值表达式、指令、props添加动态绑定等。
  • 处理类型逻辑包括静态提升逻辑,将静态节点赋值给hoists,以及根据不同类型的节点打上不同的patchFlag,便于后续diff使用。
  • 在AST上绑定并透传一些元数据。

generate方法主要是生成render方法的字符串code,在源码packages\compiler-core\src\codegen.ts中,其核心代码如下所示:

export function generate(ast: RootNode,options: CodegenOptions & {onContextCreated?: (context: CodegenContext) => void} = {}
): CodegenResult {const context = createCodegenContext(ast, options)if (options.onContextCreated) options.onContextCreated(context)const {mode,push,prefixIdentifiers,indent,deindent,newline,scopeId,ssr} = context...// 缩进处理indent()deindent()// 单独处理component、directive、filtersgenAssets()// 处理NodeTypes里的所有类型genNode(ast.codegenNode, context)...// 返回code字符串return {ast,code: context.code,preamble: isSetupInlined ? preambleContext.code : ``,// SourceMapGenerator does have toJSON() method but it's not in the typesmap: context.map ? (context.map as any).toJSON() : undefined}
}

generate方法的核心逻辑在genNode方法中,其逻辑是根据不同的NodeTypes类型构造出不同的render方法字符串,部分类型如下代码所示:

switch (node.type) {
case NodeTypes.ELEMENT:
case NodeTypes.IF:
case NodeTypes.FOR:// for关键字元素节点genNode(node.codegenNode!, context)break
case NodeTypes.TEXT:// 文本元素节点genText(node, context)break
case NodeTypes.VNODE_CALL:// 核心:VNode混合类型节点(AST语法树节点)genVNodeCall(node, context)break
case NodeTypes.COMMENT: // 注释元素节点genComment(node, context)break
case NodeTypes.JS_FUNCTION_EXPRESSION:// 方法调用节点genFunctionExpression(node, context)break
...

其中:

  • 节点类型NodeTypes.VNODE_CALL对应genVNodeCall方法和ast.ts文件里面的createVNodeCall方法对应,后者用来返回VNodeCall,前者生成对应的VNodeCall这部分render方法字符串,是整个render方法字符串的核心。
  • 节点类型NodeTypes.FOR对应for关键字元素节点,其内部是递归调用了genNode方法。
  • 节点类型NodeTypes.TEXT对应文本元素节点负责静态文本的生成。
  • 节点类型NodeTypes.JS_FUNCTION_EXPRESSION对应方法调用节点,负责方法表达式的生成。

终于,经过一系列的加工,最终生成的render方法字符串结果如下所示:

(function anonymous(
) {
const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vueconst _hoisted_1 = ["data-a"] // 静态节点
const _hoisted_2 = /*#__PURE__*/_createElementVNode("p", null, "123", -1 /* HOISTED */)// 静态节点return function render(_ctx, _cache) {// render方法with (_ctx) {const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue // helper方法return (_openBlock(), _createElementBlock(_Fragment, null, [_createElementVNode("div", { "data-a": attr }, _toDisplayString(name), 9 /* TEXT, PROPS */, _hoisted_1),_hoisted_2], 64 /* STABLE_FRAGMENT */))}
}
})

_createElementVNode,_openBlock等等上一步传进来的helper方法。其中<p>123</p>这种属于没有响应式绑定的静态节点,会被单独区分,而对于动态节点会使用createElementVNode方法来创建,最终这两种节点会进入createElementBlock方法进行VNode的创建。

render方法中使用了with关键字,with的作用如下代码所示:

const obj = {a:1
}
with(obj){console.log(a) // 打印1
}

在with(_ctx)包裹下,我们在data中定义的响应式变量才能正常使用,例如调用_toDisplayString(name),其中name就是响应式变量。

得到最终VNode对象

最终,这是一段可执行代码,会赋值给组件Component.render方法上,其源码在packages\runtime-core\src\component.ts中,如下所示:

...
Component.render = compile(template, finalCompilerOptions)
...
if (installWithProxy) { // 绑定代理installWithProxy(instance)
}
...

compile方法是最初baseCompile方法的入口,在完成赋值后,还需要绑定代理,执行installWithProxy方法,其源码在runtime-core/src/component.ts中,如下所示:

export function registerRuntimeCompiler(_compile: any) {compile = _compileinstallWithProxy = i => {if (i.render!._rc) {i.withProxy = new Proxy(i.ctx, RuntimeCompiledPublicInstanceProxyHandlers)}}
}

这主要是给render里_ctx的响应式变量添加绑定,当上面render方法里的name被使用时,可以通过代理监听到调用,这样就会进入响应式的监听收集track,当触发trigger监听时,进行diff。

在runtime-core/src/componentRenderUtils.ts源码里的renderComponentRoot方法里会执行render方法得到VNode对象,其核心代码如下所示:

export function renderComponentRoot(){// 执行renderlet result = normalizeVNode(render!.call(proxyToUse,proxyToUse!,renderCache,props,setupState,data,ctx))...return result
}

demo代码中最终得到的VNode对象如下图所示。
在这里插入图片描述
上图就是通过render方法运行后得到的VNode对象,可以看到children和dynamicChildren区分,前者包括了两个子节点分别是<div><p>这个和在<template>里面定义的内容是对应的,而后者只存储了动态节点,包括动态props即data-a属性。同时VNode也是树状结构,通过children和dynamicChildren一层一层递进下去。

在通过render方法得到VNode的过程也是对指令,插值表达式,响应式数据,插槽等一系列Vue语法的解析和构造过程,最终生成结构化的VNode对象,可以将整个过程总结成流程图,便于读者理解,如下图所示。
在这里插入图片描述
另外一个需要关注的属性是patchFlag这个是后面进行VNode的diff时所用到的标志位,数字64表示稳定不需要改变。最后得到VNode对象后需要转换成真实的DOM节点,这部分逻辑是在虚拟DOM的diff中完成的

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

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

相关文章

5 面试题--redis

伪客户端&#xff1a; 伪客户端的 fd 属性值为 -1&#xff1b;伪客户端处理的命令请求来源于 AOF ⽂件或者 Lua 脚本&#xff0c;⽽不是⽹络&#xff0c;所以这种客户端不需要套接字连接&#xff0c;⾃然也不需要记录套接字描述符。⽬前 Redis 服务器会在两个地⽅ ⽤到伪客户端…

ThermalLabel SDK for .NET 13.0.23.1113 Crack

ThermalLabel SDK for .NET 是一个 .NET 典型类库&#xff0c;它允许用户和开发人员创建非常创新的条码标签并将其发布在 zebra ZPL、EPL、EPSON ESC、POS 以及 Honeywell intermec 指纹中通过在 VB.NET 或 C# 上编写 .NET 纯代码来实现热敏打印机&#xff0c;以实现项目框架的…

Ubuntu Linux玩童年小霸王插卡游戏

1.下载安装模拟器 在Windows平台模拟器非常多&#xff0c;而且效果也很优秀&#xff0c;Linux平台的用户常常很羡慕&#xff0c;却因为系统的缘故&#xff0c;无法使用这样的模拟器&#xff0c;但是随着时代的发展&#xff0c;Linux平台也出现了许多优秀的模拟器&#xff0c;现…

navigator.clipboard is undefined in JavaScript issue [Fixed]

navigator.clipboard 在不安全的网站是无法访问的。 在本地开发使用localhost或127.0.0.1没有这个问题。因为它不是不安全网站。 在现实开发中&#xff0c;可能遇到测试环境为不安全网站。 遇到这个问题&#xff0c;就需要将不安全网站标记为非不安全网站即可。 外网提供了3…

【HTML】VScode不打开浏览器实时预览html

1. 问题描述 预览HTML时&#xff0c;不想打开浏览器&#xff0c;想在VScode中直接实时预览 2. 解决方案 下载Microsoft官方的Live Preview 点击预览按钮即可预览

Unity中Shader优化通用规则

文章目录 前言一、精度优化1、三种精度 fixed / half / float2、位置坐标、物理坐标类使用float3、HDR颜色、方向向量类使用half4、普通纹理、颜色类使用 fixed5、实际上&#xff0c;使用的精度取决于 平台 和 GPU6、现在桌面级GPU都是直接采用 float , Shader中的 fixed / hal…

J2EE征程——第一个纯servletCURD

第一个纯servletCURD 前言在此之前 一&#xff0c;概述二、CURD1介绍2查询并列表显示准备实体类country编写 CountryListServlet配置web.xml为web应用导入mysql-jdbc的jar包 3增加准备增加的页面addc.html编写 CAddServlet配置web.xml测试 4删除修改CountryListServlet&#xf…

RabbitMQ消息模型之Routing-Topic

Routing Topic Topic类型的Exchange与Direct相比&#xff0c;都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key的时候使用通配符&#xff01;这种模型Routingkey一般都是由一个或多个单词组成&#xff0c;多个单词之间以”…

ESP32-Web-Server编程- WebSocket 编程

ESP32-Web-Server编程- WebSocket 编程 概述 在前述 ESP32-Web-Server 实战编程-通过网页控制设备的 GPIO 中&#xff0c;我们创建了一个基于 HTTP 协议的 ESP32 Web 服务器&#xff0c;每当浏览器向 Web 服务器发送请求&#xff0c;我们将 HTML/CSS 文件提供给浏览器。 使用…

智能手表上的音频(四):语音通话

上篇讲了智能手表上音频文件播放。本篇开始讲语音通话。同音频播放一样有两种case&#xff1a;内置codec和BT。先看这两种case下audio data path&#xff0c;分别如下图&#xff1a; 内置codec下的语音通话audio data path 蓝牙下的语音通话audio data path 从上面两张图可以看…

纯js实现录屏并保存视频到本地的尝试

前言&#xff1a;先了解下&#xff1a;navigator.mediaDevices&#xff0c;mediaDevices 是 Navigator 只读属性&#xff0c;返回一个 MediaDevices 对象&#xff0c;该对象可提供对相机和麦克风等媒体输入设备的连接访问&#xff0c;也包括屏幕共享。 const media navigator…

【刷题】DFS

DFS 递归&#xff1a; 1.判断是否失败终止 2.判断是否成功终止&#xff0c;如果成功的&#xff0c;记录一个成果 3.遍历各种选择&#xff0c;在这部分可以进行剪枝 4.在每种情况下进行DFS&#xff0c;并进行回退。 199. 二叉树的右视图 给定一个二叉树的 根节点 root&#x…

深度学习之十二(图像翻译AI算法--UNIT(Unified Neural Translation))

概念 UNIT(Unified Neural Translation)是一种用于图像翻译的 AI 模型。它是一种基于生成对抗网络(GAN)的框架,用于将图像从一个域转换到另一个域。在图像翻译中,这意味着将一个风格或内容的图像转换为另一个风格或内容的图像,而不改变图像的内容或语义。 UNIT 的核心…

IDEA2022 Git 回滚及回滚内容恢复

IDEA2022 Git 回滚 ①选择要回滚的地方&#xff0c;右键选择 ②选择要回滚的模式 ③开始回滚 ④soft模式回滚的内容会保留在暂存区 ⑤输入git push -f &#xff0c;然后重新提交&#xff0c;即可同步远程 注意观察IDEA右下角分支的标记&#xff0c;蓝色代表远程内容未同步到本…

数据结构 / day06 作业

1.下面的代码打印在屏幕上的值是多少? /下面的代码打印在屏幕上的值是多少?#include "stdio.h"int compute_data(int arr[], unsigned int len) {long long int result 0;if(result len)return arr[0];resultcompute_data(arr,--len);printf("len%d, res…

elementui中table进行表单验证

<el-form :model"ruleForm" ref"ruleForm" class"demo-ruleForm"><el-table :data"ruleForm.tableDataShou" border style"width: 100%;"><el-table-column type"index" label"序号" wi…

【23种设计模式·全精解析 | 自定义Spring框架篇】Spring核心源码分析+自定义Spring的IOC功能,依赖注入功能

文章目录 ⭐⭐⭐Spring核心源码分析自定义Spring框架⭐⭐⭐一、Spring使用回顾二、Spring核心功能结构1、Spring核心功能2、bean概述 三、Spring IOC相关接口分析1、BeanFactory解析2、BeanDefinition解析3、BeanDefinitionReader解析4、BeanDefinitionRegistry解析5、创建容器…

伪集群配置

编辑core-site 配置core-site 配置hdfs-site 将以下的文件配置进去 启动一下hadoop产生tmp文件 产生这个叫namenode的文件并格式化 回到~目录 再配置以下信息 配置以下信息 重启文件 再重新格式化配置namenode 再启动一下&#xff0c;然后jps看看&#xff0c;出现这样就…

java后端自学总结

自学总结 MessageSource国际化接口总结 MessageSource国际化接口 今天第一次使用MessageSource接口,比较意外遇到了一些坑 messageSource是spring中的转换消息接口&#xff0c;提供了国际化信息的能力。MessageSource用于解析 消息&#xff0c;并支持消息的参数化和国际化。 S…

运维知识点-openResty

openResty 企业级实战——畅购商城SpringCloud-网站首页高可用解决方案-openRestynginxlua——实现广告缓存测试企业级实战——畅购商城SpringCloud-网站首页高可用解决方案-openRestynginxlua——OpenResty 企业级实战——畅购商城SpringCloud-网站首页高可用解决方案-openRes…