【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,一经查实,立即删除!

相关文章

vue实现左侧固定菜单栏锚点及滚动高亮(组件封装)

vue实现左侧固定菜单栏锚点及滚动高亮 先上总代码&#xff1a; 子组件&#xff1a; <!-- LeftSidebar.vue --> <template><div class"left-sidebar"><a v-for"(item, index) in sidebarItems" :key"index" click"s…

WordPress(9)宝塔配置Redis

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、宝塔安装Redis2、安装好先关闭Redis1、Redis密码默认是没有的二、安装php、Redis扩展1.启动Redis三.WordPress 安装Redis1.安装Redis插件2.启动Redis前言 提示:这里可以添加本文要记录的…

Linux ubuntu20.04 安装使用 Intel sgx

文章目录 前言一、简介二、安装Intel SGX Software Stack2.1 安装Intel SGX driver2.2 Build the Intel SGX SDK and Inte SGX PSW Package2.3 Build the Intel SGX SDK and Intel SGX SDK Installer2.4 Install the Intel(R) SGX SDK2.5 Build the Intel SGX PSW and Intel SG…

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;以实现项目框架的…

BiLSTM-CRF的中文命名实体识别

项目地址&#xff1a;NLP-Application-and-Practice/11_BiLSTM-ner-bilstm-crf/11.3-BiLSTM-CRF的中文命名实体识别/ner_bilstm_crf at master zz-zik/NLP-Application-and-Practice (github.com) 读取renmindata.pkl文件 read_file_pkl.py # encoding:utf-8import pickle# …

分享一些基于php商城案例

案例1&#xff1a; ​​​​​​http://www.9520.xin/ 案例2&#xff1a; http://ptll.hasbuy.com/ 案例3&#xff1a; http://likeshop.9520.xin/mobile 案例4&#xff1a; http://www.hasbuy.com/

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

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

CTF ssrf+pin

什么是pin码 pin码是flask在开启debug模式下&#xff0c;进行代码调试模式所需的进入密码&#xff0c;需要正确的PIN码才能进入调试模式,可以理解为自带的webshell pin码如何生成 pin码生成要六要素 1.username 在可以任意文件读的条件下读 /etc/passwd进行猜测 2.modname 默…

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 从上面两张图可以看…

享元模式 (Flyweight Pattern)

定义&#xff1a; 享元模式&#xff08;Flyweight Pattern&#xff09;是一种结构型设计模式&#xff0c;用于优化性能和内存使用。它通过共享尽可能多的相似对象来减少内存占用&#xff0c;特别是在有大量对象时。这种模式通常用于减少应用程序中对象的数量&#xff0c;并在多…

Redis 实战缓存

本篇概要&#xff1a; 1. 设置、查询、获取过期时间&#xff1b;2. 缓存穿透&#xff1a;设置空键&#xff1b;3. 封杀单ip&#xff1b;4. 封杀ip段&#xff1b;5. 缓存预热&#xff1b;6. 使用 hash 数据类型保存新闻的缓存&#xff0c;增加点击量&#xff1b;7. Sorted set&a…

纯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…