Vue 渲染流程详解
在 Vue 里渲染一块内容,会有以下步骤及流程:
第一步,解析语法,生成AST
第二步,根据AST结果,完成data数据初始化
第三步,根据AST结果和DATA数据绑定情况,生成虚拟DOM
第四步,将虚拟DOM 生成真正的DOM插入到页面中,进行页面渲染。
那怎么理解这个流程呢?
一、解析语法生成AST
AST 语法树,实际就是抽象语法树(Abstract Syntax Tree),是指通过构建语法树的形式将源代码中的语句映射到树中的每一个节点上。
DOM 结构树,也是AST中的一种,把HTML DOM语法解析并生成最终页面。
我们详细看看这个过程:
1、捕获语法
在生成AST的过程中,会涉及到编译器的原理, 会经过以下过程:
(1)、语法分析
语法分析的任务是在词法分析的基础上将单词序列组合成各类语法短语。如 :程序、语句、表达式等。语法分析程序判断源程序在结构上是否正确, 如 v-if` / v-for 这样的指令 ,也有``这样的自定义 DOM 标签,还有`click`/`props 这样的简化绑定语法。需要将它们一一解析出来,并相应地进行后续处理。
(2)、语义分析
语义分析是审查源程序有无语义错误,为代码生成阶段收集类型信息,一般类型检查也会在这个过程中进行。如我们绑定了某个不存在的变量或者事件,又或者是使用了某个未定义的自定义组件等,都会在这个阶段进行报错提示。
(3) 、生成 AST
在Vue 里,语法分析、语义分析基本上是通过正则的方式来处理,生成 AST其实就是将解析出来的元素、指令、属性、父子节点关系等内容进行处理,得到一个 AST 对象,以下是简化后的源码:
/*** HTML编译成AST对象*/
export function parse(template: string,options: CompilerOptions
): ASTElement | void
{// 返回AST对象// 篇幅原因,一些前置定义省略// 此处开始解析HTML模板parseHTML(template, {expectHTML: options.expectHTML,isUnaryTag: options.isUnaryTag,shouldDecodeNewlines: options.shouldDecodeNewlines,start(tag, attrs, unary) {// 一些前置检查和设置、兼容处理此处省略// 此处定义了初始化的元素AST对象const element: ASTElement = {type: 1,tag,attrsList: attrs,attrsMap: makeAttrsMap(attrs),parent: currentParent,children: []};// 检查元素标签是否合法(不是保留命名)if (isForbiddenTag(element) && !isServerRendering()) {element.forbidden = true;process.env.NODE_ENV !== "production" &&warn("Templates should only be responsible for mapping the state to the " +"UI. Avoid placing tags with side-effects in your templates, such as " +`<${tag}>` +", as they will not be parsed.");}// 执行一些前置的元素预处理for (let i = 0; i < preTransforms.length; i++) {preTransforms[i](element, options);}// 是否原生元素if (inVPre) {// 处理元素元素的一些属性processRawAttrs(element);} else {// 处理指令,此处包括v-for/v-if/v-once/key等等processFor(element);processIf(element);processOnce(element);processKey(element); // 删除结构属性// 确定这是否是一个简单的元素element.plain = !element.key && !attrs.length;// 处理ref/slot/component等属性processRef(element);processSlot(element);processComponent(element);for (let i = 0; i < transforms.length; i++) {transforms[i](element, options);}processAttrs(element);}// 后面还有一些父子节点等处理,此处省略}// 其他省略});return root;
}
2、DOM 元素捕获
假如我们需要捕获一个<div>
元素,再生成一个<div>
元素。
有一段模板,我们可以对它进行捕获:
<div><a>111</a><p>222<span>333</span> </p>
</div>
捕获后我们可以得到这样一个对象:
divObj = {dom: {type: "dom",ele: "div",nodeIndex: 0,children: [{type: "dom",ele: "a",nodeIndex: 1,children: [{ type: "text", value: "111" }]},{type: "dom",ele: "p",nodeIndex: 2,children: [{ type: "text", value: "222" },{type: "dom",ele: "span",nodeIndex: 3,children: [{ type: "text", value: "333" }]}]}]}
};
这个对象保存了我们需要的一些信息:
HTML元素里需要绑定哪些变量,因为变量更新的时候需要更新该节点内容。
以怎样的方式来拼接,是否有逻辑指令,如v-if、v-for等
哪些节点绑定了什么监听事件,是否匹配一些常用的事件能力支持
Vue 会根据 AST 对象生成一段可执行的代码,我们看看这部分的实现:
// 生成一个元素
function genElement(el: ASTElement): string {// 根据该元素是否有相关的指令、属性语法对象,来进行对应的代码生成if (el.staticRoot && !el.staticProcessed) {return genStatic(el);} else if (el.once && !el.onceProcessed) {return genOnce(el);} else if (el.for && !el.forProcessed) {return genFor(el);} else if (el.if && !el.ifProcessed) {return genIf(el);} else if (el.tag === "template" && !el.slotTarget) {return genChildren(el) || "void 0";} else if (el.tag === "slot") {return genSlot(el);} else {// component或者element的代码生成let code;if (el.component) {code = genComponent(el.component, el);} else {const data = el.plain ? undefined : genData(el);const children = el.inlineTemplate ? null : genChildren(el, true);code = `_c('${el.tag}'${data ? `,${data}` : "" // data}${children ? `,${children}` : "" // children})`;}// 模块转换for (let i = 0; i < transforms.length; i++) {code = transforms[i](el, code);}// 返回最后拼装好的可执行的代码return code;}
}
3、模板引擎赋能
通过以上介绍,或许大家会说,原本就是一个<div>,经过 AST 生成一个对象,最终还是生成一个<div>,这不是多余的步骤吗?
其实 ,在这个过程中我们可以实现一些功能:
排除无效 DOM 元素,并在构建过程可进行报错
使用自定义组件的时候,可匹配出来
可方便地实现数据绑定、事件绑定等功能
为虚拟 DOM Diff 过程打下铺垫
HTML 转义预防 XSS 漏洞
通用的模板引擎能处理很多低效又重复的工作,例如浏览器兼容、全局事件的统一管理和维护、模板更新的虚拟 DOM 机制、树状组织管理组件。这样我们知道了模板引擎都做了什么事情后,就可以区分 Vue 框架提供的能力和我们需要自行处理的逻辑,可以更专注于业务开发。
二、虚拟DOM
虚拟 DOM 大概可分成三个过程:
第一步,用 JS 对象模拟 DOM 树,得到一棵虚拟 DOM 树。
第二步,当页面数据变更时,生成新的虚拟 DOM 树,比较新旧两棵虚拟 DOM 树的差异。
第三步,把差异应用到真正的 DOM 树上。
1、用 JS 对象模拟 DOM 树
为什么要用到虚拟 DOM ? 因为一个真正的 DOM 元素非常庞大,拥有很多的属性值,而实际上我们并不是全部都会用到,通常包括节点内容、元素位置、样式、节点的添加删除等方法。所以,我们通过用 JS 对象表示 DOM 元素的方式,可以大大降低了比较差异的计算量。
我们来看一下 VNode 源码,只有以下20来个属性:
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
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
// strictly internal
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?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnContext: Component | void; // real context vm for functional nodes
fnOptions: ?ComponentOptions; // for SSR caching
devtoolsMeta: ?Object; // used to store functional render context fordevtools
fnScopeId: ?string; // functional scope id support
2 、比较新旧两棵虚拟 DOM 树的差异
虚拟 DOM 中,差异对比是很关键的一步,当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异。这样的差异需要记录:
需要替换掉原来的节点
移动、删除、新增子节点
修改了节点的属性
对于文本节点的文本内容改变
下图,我们对比两棵 DOM 树,得到的差异有:
p 元素插入了一个 span 元素子节点
原先的文本节点挪到了 span 元素子节点下面
3、应用差异到真正的 DOM 树
通过前面的示例,我们知道差异记录要应用到真正的 DOM 树上,需要进行一些操作,例如节点的替换、移动、删除,文本内容的改变等。
在 Vue 中是怎么进行 DOM Diff 呢? 简单看这段代码感受下, 虽然代码里很多函数没贴出来,但其实看函数名也可以大概理解都是什么作用,例如updateChildren、addVnodes、removeVnodes、setTextContent等。
// 对比差异后更新
const oldCh = oldVnode.children;
const ch = vnode.children;
if (isDef(data) && isPatchable(vnode)) {for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);if (isDef((i = data.hook)) && isDef((i = i.update))) i(oldVnode, vnode);
}
if (isUndef(vnode.text)) {if (isDef(oldCh) && isDef(ch)) {if (oldCh !== ch)updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);} else if (isDef(ch)) {if (process.env.NODE_ENV !== "production") {checkDuplicateKeys(ch);}if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, "");addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);} else if (isDef(oldCh)) {removeVnodes(elm, oldCh, 0, oldCh.length - 1);} else if (isDef(oldVnode.text)) {nodeOps.setTextContent(elm, "");}
} else if (oldVnode.text !== vnode.text) {nodeOps.setTextContent(elm, vnode.text);
}
if (isDef(data)) {if (isDef((i = data.hook)) && isDef((i = i.postpatch))) i(oldVnode, vnode);
}
三、数据绑定
在 Vue 中,最基础的模板语法是数据绑定。
例如:
<div>{{ message }}</div>
这里使用插值表达式{{}}
绑定了一个message
的变量,开发者在 Vue 实例data
中绑定该变量:
new Vue({data: {message: "test"}
});
最终页面展示内容为<div>test</div>
。那这是怎么做到的呢?
1、 数据绑定的实现
这种使用双大括号来绑定变量的方式,我们称之为数据绑定。
数据绑定的过程其实不复杂:
(1) 、解析语法生成 AST
(2) 、根据 AST 结果生成 DOM
(3) 、将数据绑定更新至模板
这个过程是 Vue 中模板引擎在做的事情,我们来看看上面在 Vue 里的代码片段<div></div>,我们可以通过 DOM 元素捕获,解析后获得这样一个 AST 对象:
divObj = {dom: {type: "dom",ele: "div",nodeIndex: 0,children: [{ type: "text", value: "" }]},binding: [{ type: "dom", nodeIndex: 0, valueName: "message" }]
};
我们在生成 DOM 的时候,添加对message
的监听,数据更新时会找到对应的nodeIndex
更新值:
// 假设这是一个生成 DOM 的过程,包括 innerHTML 和事件监听
function generateDOM(astObject) {const { dom, binding = [] } = astObject;// 生成DOM,这里假设当前节点是baseDombaseDom.innerHTML = getDOMString(dom);// 对于数据绑定的,来进行监听更新baseDom.addEventListener("data:change", (name, value) => {// 寻找匹配的数据绑定const obj = binding.find(x => x.valueName == name);// 若找到值绑定的对应节点,则更新其值。if (obj) {baseDom.find(`[data-node-index="${obj.nodeIndex}"]`).innerHTML = value;}});
}// 获取DOM字符串,这里简单拼成字符串
function getDOMString(domObj) {// 无效对象返回''if (!domObj) return "";const { type, children = [], nodeIndex, ele, value } = domObj;if (type == "dom") {// 若有子对象,递归返回生成的字符串拼接const childString = "";children.forEach(x => {childString += getDOMString(x);});// dom对象,拼接生成对象字符串return `<${ele} data-node-index="${nodeIndex}">${childString}</${ele}>`;} else if (type == "text") {// 若为textNode,返回text的值return value;}
}
这样,我们就能在message
变量更新的时候,通过该变量关联的引用,来自动更新对应展示的内容。而要知道message
变量什么时候进行了改变,我们需要对数据进行监听。
2、数据更新监听
加粗样式
我们能看到,上面的简单代码描述过程中,使用的数据监听方法是用了addEventListener("data:change", Function)的方式。
在 Vue 中,数据更新的时候就执行了模板更新、watch、computed 等一些工作,主要是依赖了Getter/Setter。而 Vue3.0 将使用Proxy的方式来进行:
Object.defineProperty(obj, key, {enumerable: true,configurable: true,// getterget: function reactiveGetter() {const value = getter ? getter.call(obj) : val;if (Dep.target) {dep.depend();if (childOb) {childOb.dep.depend();if (Array.isArray(value)) {dependArray(value);}}}return value;},// setter最终更新后会通知set: function reactiveSetter(newVal) {const value = getter ? getter.call(obj) : val;if (newVal === value || (newVal !== newVal && value !== value)) {return;}if (process.env.NODE_ENV !== "production" && customSetter) {customSetter();}if (getter && !setter) return;if (setter) {setter.call(obj, newVal);} else {val = newVal;}childOb = !shallow && observe(newVal);dep.notify();}
});
Vue 中大多数能力都依赖于模板引擎,包括组件化管理、事件管理、Vue 实例、生命周期等,相信只要理解了 AST、虚拟 DOM、数据绑定相关的机制后,再去翻阅 Vue 源码 ,了解更多的能力就不是问题了。
相关文章:

Vue 渲染流程详解
在 Vue 里渲染一块内容,会有以下步骤及流程: 第一步,解析语法,生成AST 第二步,根据AST结果,完成data数据初始化 第三步,根据AST结果和DATA数据绑定情况,生成虚拟DOM 第四步&…...

10分钟内入门 ArcGIS Pro
本文来源:GIS荟 大家好,这篇文章大概会花费你10分钟的时间,带你入门 ArcGIS Pro 的使用,不过前提是你有 ArcMap 使用经验。 我将从工程文件组织方式、软件界面、常用功能、编辑器、制图这5个维度给大家介绍。 演示使用的 ArcGI…...

【ribbon】Ribbon的使用与原理
负载均衡介绍 负载均衡(Load Balance),其含义就是指将负载(工作任务)进行平衡、分摊到多个操作单元上进行运行,例如FTP服务器、Web服务器、企业核心应用服务器和其它主要任务服务器等,从而协同…...
axios封装到reques.js文件中
封装到js中,避免每次都import 然后写一大堆 import axios from axios /* 可复用的发 ajax 请求的函数: axios */ let baseURLhttp://localhost:3000/ export default function promiseAjax(url,methodget,datanull,params) {return new Promise((resolve, reject) …...

学好Elasticsearch系列-核心概念
本文已收录至Github,推荐阅读 👉 Java随想录 文章目录 节点角色master:候选节点data:数据节点Ingest:预处理节点ml:机器学习节点remote_ cluster_ client:候选客户端节点transform:…...

扩展点都不知道不要说你用了Spring Boot
文章目录 前言1.扩展点1.1. 应用程序生命周期扩展点1.1.1 SpringApplicationRunListener1.1.2 ApplicationEnvironmentPreparedEvent1.1.3 ApplicationPreparedEvent1.1.4 ApplicationStartedEvent1.1.5 ApplicationReadyEvent1.1.6 ApplicationFailedEvent 1.2. 容器扩展点1.2…...

LangChain大型语言模型(LLM)应用开发(五):评估
LangChain是一个基于大语言模型(如ChatGPT)用于构建端到端语言模型应用的 Python 框架。它提供了一套工具、组件和接口,可简化创建由大型语言模型 (LLM) 和聊天模型提供支持的应用程序的过程。LangChain 可以轻松管理与语言模型的交互&#x…...

Angular:动态依赖注入和静态依赖注入
问题描述: 自己写的服务依赖注入到组件时候是直接在构造器内初始化的。 直到看见代码中某大哥写的 private injector: Injector 动态依赖注入和静态依赖注入 在 Angular 中,使用构造函数注入的方式将服务注入到组件中是一种静态依赖注入的方式。这种方…...
Java前后端交互long类型溢出的解决方案
问题描述: 前端根据id发起请求查找对象的时候一直返回找不到对象,然后查看了请求报文,发现前端传给后台的数据id不对,原本的id是1435421253099634623,可前端传过来的id是 1435421253099634700,后三位变成了…...
Lua学习-1 基础数据类型
文章目录 基础数据类型分类nilbooleannumberstringfunctionuserDatathreadtable 如何判断类型(type)不同类型数据常见操作nilnumberstring(字符串)function普通函数匿名函数不定参数函数 table 基础数据类型分类 nil 表示无效值 boolean 只有 true 和…...
普通的计算机专业大学生如何学习才能找到好offer
2023年已经将近8月份了,回想到开始努力提高自己的时候还是在今年1月1号。开学就要大二了。 一、目标达成情况总结: 一月份,无意间在网上刷到鹏哥的C语言课程,在鸡汤实力课程已拿到大厂offer的同学喜报 ,让我萌发了学技…...

iOS私钥证书和证书profile文件的生成攻略
在使用uniapp打包ios app的时候,要求我们提供一个私钥证书和一个证书profile文件,私钥证书可以使用mac电脑的钥匙串访问程序来生成,也可以使用香蕉云编来生成。证书profile文件可以直接在苹果开发者中心生成。 有部分刚接触ios开发的同学们&…...

前端 | ( 十二)CSS3简介及基本语法(中)| 变换、过渡与动画 | 尚硅谷前端html+css零基础教程2023最新
学习来源:尚硅谷前端htmlcss零基础教程,2023最新前端开发html5css3视频 系列笔记: 【HTML4】(一)前端简介【HTML4】(二)各种各样的常用标签【HTML4】(三)表单及HTML4收尾…...

【BOOST程序库】时间日期库
基本概念这里不再浪费时间介绍了,这里给出时间日期库的常见使用方法: #define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <string> #include <boost/version.hpp> #include <boost/config.hpp>//时间库࿱…...
Windows 命令提示符 (cmd. exe) 命令行字符串长度限制
在Windows中,命令提示符 (cmd. exe) 命令行字符串是有长度限制的,本文帮助你了解命令行中的字符串长度限制,以免你的批处理脚本踩坑。 (以下在Win10环境测试过) 字符串长度限制 可在命令提示符处使用的字符串的最大长…...
Kafka 入门到起飞系列
Kafka 入门到起飞系列 [Kakfa 为什么牛? 为什么这么火?有什么优势呢?](https://blog.csdn.net/FightingITPanda/article/details/131941293)[工欲善其事,必先利其器 - 核心概念(术语解释)](https://blog.cs…...

[RabbitMQ] RabbitMQ简单概述,用法和交换机模型
MQ概述: Message Queue(消息队列),实在消息的传输过程中保存消息的容器,都用于分布式系统之间进行通信 分布式系统通信的两种方式:直接远程调用 和 借助第三昂 完成间接通信 发送方称谓生产者,接收方称为消费者 MQ优…...

Oracle 多条记录根据某个字段获取相邻两条数据间的间隔天数,小于31天的记录都筛选出来
需求描述:在Oracle中 住院记录记录表为v_hospitalRecords,表中FIHDATE入院时间,FBIHID是住院号, 我想查询出每个患者在他们的所有住院记录中是否在一个月内再次入院(相邻的两条记录进行比较),并且住院记录大于一的患者…...

【数据挖掘】如何修复时序分析缺少的日期
一、说明 我撰写本文的目的是通过引导您完成一个示例来帮助您了解 TVF 以及如何使用它们,该示例解决了时间序列分析中常见的缺失日期问题。 我们将介绍: 如何生成日期以填补数据中缺失的空白如何创建 TVF 和参数的使用如何呼叫 TVF我们将考虑扩展我们的日…...
CDN、P2P、PCDN的区别是什么
本篇文章为大家介绍一下与网络加速有关的几个重要概念,一起了解一下CDN,P2P和PCDN究竟是什么吧! 1. CDN CDN即Content Delivery Network,中文全称为内容分发网络。 如果内容离用户远,用户可能无法获得及时的响应,那…...
【网络】每天掌握一个Linux命令 - iftop
在Linux系统中,iftop是网络管理的得力助手,能实时监控网络流量、连接情况等,帮助排查网络异常。接下来从多方面详细介绍它。 目录 【网络】每天掌握一个Linux命令 - iftop工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景…...
深入浅出:JavaScript 中的 `window.crypto.getRandomValues()` 方法
深入浅出:JavaScript 中的 window.crypto.getRandomValues() 方法 在现代 Web 开发中,随机数的生成看似简单,却隐藏着许多玄机。无论是生成密码、加密密钥,还是创建安全令牌,随机数的质量直接关系到系统的安全性。Jav…...

LeetCode - 394. 字符串解码
题目 394. 字符串解码 - 力扣(LeetCode) 思路 使用两个栈:一个存储重复次数,一个存储字符串 遍历输入字符串: 数字处理:遇到数字时,累积计算重复次数左括号处理:保存当前状态&a…...

DIY|Mac 搭建 ESP-IDF 开发环境及编译小智 AI
前一阵子在百度 AI 开发者大会上,看到基于小智 AI DIY 玩具的演示,感觉有点意思,想着自己也来试试。 如果只是想烧录现成的固件,乐鑫官方除了提供了 Windows 版本的 Flash 下载工具 之外,还提供了基于网页版的 ESP LA…...
MySQL账号权限管理指南:安全创建账户与精细授权技巧
在MySQL数据库管理中,合理创建用户账号并分配精确权限是保障数据安全的核心环节。直接使用root账号进行所有操作不仅危险且难以审计操作行为。今天我们来全面解析MySQL账号创建与权限分配的专业方法。 一、为何需要创建独立账号? 最小权限原则…...

九天毕昇深度学习平台 | 如何安装库?
pip install 库名 -i https://pypi.tuna.tsinghua.edu.cn/simple --user 举个例子: 报错 ModuleNotFoundError: No module named torch 那么我需要安装 torch pip install torch -i https://pypi.tuna.tsinghua.edu.cn/simple --user pip install 库名&#x…...

使用LangGraph和LangSmith构建多智能体人工智能系统
现在,通过组合几个较小的子智能体来创建一个强大的人工智能智能体正成为一种趋势。但这也带来了一些挑战,比如减少幻觉、管理对话流程、在测试期间留意智能体的工作方式、允许人工介入以及评估其性能。你需要进行大量的反复试验。 在这篇博客〔原作者&a…...
go 里面的指针
指针 在 Go 中,指针(pointer)是一个变量的内存地址,就像 C 语言那样: a : 10 p : &a // p 是一个指向 a 的指针 fmt.Println(*p) // 输出 10,通过指针解引用• &a 表示获取变量 a 的地址 p 表示…...
Python 训练营打卡 Day 47
注意力热力图可视化 在day 46代码的基础上,对比不同卷积层热力图可视化的结果 import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader import matplotlib.pypl…...
Python竞赛环境搭建全攻略
Python环境搭建竞赛技术文章大纲 竞赛背景与意义 竞赛的目的与价值Python在竞赛中的应用场景环境搭建对竞赛效率的影响 竞赛环境需求分析 常见竞赛类型(算法、数据分析、机器学习等)不同竞赛对Python版本及库的要求硬件与操作系统的兼容性问题 Pyth…...