01- vdom 和模板编译源码
组件渲染的过程
template --> ast --> render --> vDom --> 真实的Dom --> 页面
Runtime-Compiler和Runtime-Only的区别 - 简书
编译步骤
模板编译是Vue中比较核心的一部分。关于 Vue 编译原理这块的整体逻辑主要分三个部分,也可以说是分三步,前后关系如下:
第一步:将模板字符串转换成element ASTs( 解析器 parse)第二步:对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器 optimize)
第三步:使用element ASTs生成render函数代码字符串(代码生成器 generate)


编译后的AST结构
template 模板:
<div class="box"><p>{{name}}</p>
</div>
AST 抽象语法树:
ast: {tag: "div" // 元素标签名type: 1, // 元素节点类型 1标签 2包含字面量表达式的文本节点 3普通文本节点或注释节点staticRoot: false, // 是否静态根节点static: false, // 是否静态节点plain: true, parent: undefined,attrsList: [], // 标签节点的属性名和值的对象集合attrsMap: {}, // 和attrsList类似,不同在它是以键值对保存属性名和值children: [{tag: "p"type: 1,staticRoot: false,static: false,plain: true,parent: {tag: "div", ...},attrsList: [],attrsMap: {},children: [{type: 2,text: "{{name}}",static: false,expression: "_s(name)" // type为2时才有这个属性,表示表达式的内容}]}]
}
generate,将AST转换成可以直接执行的JavaScript字符串
with(this) {return _c('div', [_c('p', [_v(_s(name))]), _v(" "), _m(0)])
}

注意一:平常开发中 我们使用的是不带编译版本的 Vue 版本(runtime-only)直接在 options 传入 template 选项 在开发环境报错
注意二:这里传入的 template 选项不要和.vue 文件里面的模板搞混淆了 vue 单文件组件的 template 是需要 vue-loader 进行处理的
我们传入的 el 或者 template 选项最后都会被解析成 render 函数 这样才能保持模板解析的一致性
以下代码实现是在在entry-runtime-with-compiler.js里面,和runtime-only版本需要区分开
1、模板编译入口
export function initMixin (Vue) {Vue.prototype._init = function (options) {const vm = this;vm.$options = options;initState(vm);// 如果有 el 属性,进行模板渲染if (vm.$options.el) {vm.$mount(vm.$options.el)}}Vue.prototype.$mount = function (el) {const vm = this;const options = vm.$options;el = document.querySelector(el);// 不存在 render 属性的几种情况if (!options.render) {// 不存在 render 但存在 templatelet template = options.template;// 不存在 render 和 template 但是存在 el 属性,直接将模板赋值到 el 所在的外层 html 结构 (就是 el 本身,并不是父元素)if (!template && el) {template = el.outerHTML;}// 最后把处理好的 template 模板转化成 render 函数if (template) {const render = compileToFunctions(template);options.render = render;}}}
}
- 先初始化状态,initState(vm);
- 再在 initMixin 函数中,判断是否有el,有则直接调用 vm.$mount(vm.$options.el) 进行模板渲染,没有则手动调用;
- 在 Vue.prototype.$mount 方法中,判断是否存在 render 属性,存在则给 template 赋值,如果不存在 render 和 template 但存在 el 属性,直接将模板赋值到 el 所在的外层 html 结构(就是el本身,并不是父元素);
- 通过 compileToFunctions 将处理好的 template 模板转换成 render 函数。
咱们主要关心$mount 方法 最终将处理好的 template 模板转成 render 函数
2、模板转化核心方法 compileToFunctions
export function compileToFunctions (template) {// html → ast → render函数// 第一步 将 html 字符串转换成 ast 语法树let ast = parse(template);// 第二步 优化静态节点if (mergeOptions.optimize !== false) {optimize(ast, options);}// 第三步 通过 ast 重新生成代码let code = generate(ast);// 使用 with 语法改变作用域为 this, 之后调用 render 函数可以使用 call 改变 this,方便 code 里面的变量取值。let renderFn = new Function(`with(this){return ${code}}`);return renderFn;
}
html → ast → render函数
这里需要把 html 字符串变成 render 函数,分三步:
(1)parse函数 把 HTML 代码转成 AST 语法树
AST 是用来描述代码本身形成的树形结构,不仅可以描述 HTML,也可以描述 css 和 js 语法;很多库都运用到了 AST,比如 webpack,babel,eslint 等
(2) optimize函数 优化静态节点(主要用来做虚拟DOM的渲染优化)
先遍历 AST,对 AST 进行静态节点标记,(即节点永远不会发生变化)并做出一些特殊的处理。例如:
- 移除静态节点的 v-once 指令,因为它在这里没有任何意义。
- 移除静态节点的 key 属性。因为静态节点永远不会改变,所以不需要 key 属性。
- 将一些静态节点合并成一个节点,以减少渲染的节点数量。例如相邻的文本节点和元素节点可以被合并为一个元素节点。
(3)generate函数 通过 AST 重新生成代码
最后生成的代码需要和 render 函数一样
类似这样的结构:
_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))
- _c 代表 createElement 创建节点
- _v 代表 createTextVNode 创建文本节点
- _s 代表 toString 把对象解析成字符串
模板引擎的实现原理 with + new Function,使用 with 语法改变作用域为 this, 之后调用 render 函数可以使用 call 改变 this,方便 code 里面的变量取值。
parse函数,解析 html 并生成 ast
// src/compiler/parse.js// 以下为源码的正则 对正则表达式不清楚的同学可以参考小编之前写的文章(前端进阶高薪必看 - 正则篇);
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; //匹配标签名 形如 abc-123
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; //匹配特殊标签 形如 abc:234 前面的abc:可有可无
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 匹配标签开始 形如 <abc-123 捕获里面的标签名
const startTagClose = /^\s*(\/?)>/; // 匹配标签结束 >
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾 如 </abc-123> 捕获里面的标签名
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性 形如 id="app"let root, currentParent; //代表根节点 和当前父节点
// 栈结构 来表示开始和结束标签
let stack = [];
// 标识元素和文本type
const ELEMENT_TYPE = 1;
const TEXT_TYPE = 3;
// 生成ast方法
function createASTElement(tagName, attrs) {return {tag: tagName,type: ELEMENT_TYPE,children: [],attrs,parent: null,};
}// 对开始标签进行处理
function handleStartTag({ tagName, attrs }) {let element = createASTElement(tagName, attrs);if (!root) {root = element;}currentParent = element;stack.push(element);
}// 对结束标签进行处理
function handleEndTag(tagName) {// 栈结构 []// 比如 <div><span></span></div> 当遇到第一个结束标签</span>时 会匹配到栈顶<span>元素对应的ast 并取出来let element = stack.pop();// 当前父元素就是栈顶的上一个元素 在这里就类似divcurrentParent = stack[stack.length - 1];// 建立parent和children关系if (currentParent) {element.parent = currentParent;currentParent.children.push(element);}
}// 对文本进行处理
function handleChars(text) {// 去掉空格text = text.replace(/\s/g, "");if (text) {currentParent.children.push({type: TEXT_TYPE,text,});}
}// 解析标签生成ast核心
export function parse(html) {while (html) {// 查找<let textEnd = html.indexOf("<");// 如果<在第一个 那么证明接下来就是一个标签 不管是开始还是结束标签if (textEnd === 0) {// 如果开始标签解析有结果const startTagMatch = parseStartTag();if (startTagMatch) {// 把解析好的标签名和属性解析生成asthandleStartTag(startTagMatch);continue;}// 匹配结束标签</const endTagMatch = html.match(endTag);if (endTagMatch) {advance(endTagMatch[0].length);handleEndTag(endTagMatch[1]);continue;}}let text;// 形如 hello<div></div>if (textEnd >= 0) {// 获取文本text = html.substring(0, textEnd);}if (text) {advance(text.length);handleChars(text);}}// 匹配开始标签function parseStartTag() {const start = html.match(startTagOpen);if (start) {const match = {tagName: start[1],attrs: [],};//匹配到了开始标签 就截取掉advance(start[0].length);// 开始匹配属性// end代表结束符号> 如果不是匹配到了结束标签// attr 表示匹配的属性let end, attr;while (!(end = html.match(startTagClose)) &&(attr = html.match(attribute))) {advance(attr[0].length);attr = {name: attr[1],value: attr[3] || attr[4] || attr[5], //这里是因为正则捕获支持双引号 单引号 和无引号的属性值};match.attrs.push(attr);}if (end) {// 代表一个标签匹配到结束的>了 代表开始标签解析完毕advance(1);return match;}}}//截取html字符串 每次匹配到了就往前继续匹配function advance(n) {html = html.substring(n);}// 返回生成的astreturn root;
}
generate函数,把 ast 转化成 render 函数结构
// src/compiler/codegen.jsconst defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; //匹配花括号 {{ }} 捕获花括号里面的内容function gen(node) {// 判断节点类型// 主要包含处理文本核心// 源码这块包含了复杂的处理 比如 v-once v-for v-if 自定义指令 slot等等 咱们这里只考虑普通文本和变量表达式{{}}的处理// 如果是元素类型if (node.type == 1) {// 递归创建return generate(node);} else {// 如果是文本节点let text = node.text;// 不存在花括号变量表达式if (!defaultTagRE.test(text)) {return `_v(${JSON.stringify(text)})`;}// 正则是全局模式 每次需要重置正则的lastIndex属性 不然会引发匹配buglet lastIndex = (defaultTagRE.lastIndex = 0);let tokens = [];let match, index;while ((match = defaultTagRE.exec(text))) {// index代表匹配到的位置index = match.index;if (index > lastIndex) {// 匹配到的{{位置 在tokens里面放入普通文本tokens.push(JSON.stringify(text.slice(lastIndex, index)));}// 放入捕获到的变量内容tokens.push(`_s(${match[1].trim()})`);// 匹配指针后移lastIndex = index + match[0].length;}// 如果匹配完了花括号 text里面还有剩余的普通文本 那么继续pushif (lastIndex < text.length) {tokens.push(JSON.stringify(text.slice(lastIndex)));}// _v表示创建文本return `_v(${tokens.join("+")})`;}
}// 处理attrs属性
function genProps(attrs) {let str = "";for (let i = 0; i < attrs.length; i++) {let attr = attrs[i];// 对attrs属性里面的style做特殊处理if (attr.name === "style") {let obj = {};attr.value.split(";").forEach((item) => {let [key, value] = item.split(":");obj[key] = value;});attr.value = obj;}str += `${attr.name}:${JSON.stringify(attr.value)},`;}return `{${str.slice(0, -1)}}`;
}// 生成子节点 调用gen函数进行递归创建
function getChildren(el) {const children = el.children;if (children) {return `${children.map((c) => gen(c)).join(",")}`;}
}
// 递归创建生成code
export function generate(el) {let children = getChildren(el);let code = `_c('${el.tag}',${el.attrs.length ? `${genProps(el.attrs)}` : "undefined"}${children ? `,${children}` : ""})`;return code;
}
code 字符串生成 render 函数
export function compileToFunctions (template) {let ast = parseHTML(template);let code = genCode(ast);// 将模板变成 render 函数,通过 with + new Function 的方式让字符串变成 JS 语法来执行const render = new Function(`with(this){return ${code}}`);return render;
}
https://juejin.cn/spost/7267858684667035704
Vue 模板 AST 详解 - 文章教程 - 文江博客
滑动验证页面
手写Vue2.0源码(二)-模板编译原理|技术点评_慕课手记
前端鲨鱼哥 的个人主页 - 文章 - 掘金
前端进阶高薪必看-正则篇 - 掘金
从vue模板解析学习正则表达式
相关文章:
01- vdom 和模板编译源码
组件渲染的过程 template --> ast --> render --> vDom --> 真实的Dom --> 页面 Runtime-Compiler和Runtime-Only的区别 - 简书 编译步骤 模板编译是Vue中比较核心的一部分。关于 Vue 编译原理这块的整体逻辑主要分三个部分,也可以说是分三步&am…...
C++入门知识点——解决C语言不足
😶🌫️Take your time ! 😶🌫️ 💥个人主页:🔥🔥🔥大魔王🔥🔥🔥 💥代码仓库:🔥🔥魔…...
探秘分布式大数据:融合专业洞见,燃起趣味火花,启迪玄幻思维
文章目录 一 数据导论二 大数据的诞生三 大数据概论3.1 大数据的5V特征3.2 大数据的工作核心 四 大数据软件生态4.1 数据存储软件4.2 数据计算软件4.3 数据传输软件 五 Apache Hadoop概述5.1 Apache Hadoop框架5.2 Hadoop的功能5.3 Hadoop的发展5.4 Hadoop发行版本 一 数据导论…...
什么是 SPI,和API有什么区别?
面试回答 Java 中区分 API 和 SPI,通俗的讲:API 和 SPI 都是相对的概念,他们的差别只在语义上,API 直接被应用开发人员使用,SPI 被框架扩展人员使用。 API Application Programming Interface 大多数情况下ÿ…...
python3 安装clickhouse_sqlalchemy(greenlet) 失败
环境信息: centos7操作系统,python3.8 执行pip3 install clickhouse_sqlalchemy或者pip3 install greenlet报以下报错: Command "/opt/python3.6.10-customized/bin/python3.6 -u -c "import setuptools, tokenize;file/tmp/pip-in…...
五款拿来就能用的炫酷表白代码
「作者主页」:士别三日wyx 「作者简介」:CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」:小白零基础《Python入门到精通》 五款炫酷表白代码 1、无限弹窗表白2、做我女朋友好吗,不同意就关机3、…...
Springboot 封装整活 Mybatis 动态查询条件SQL自动组装拼接
前言 ps:最近在参与3100保卫战,战况很激烈,刚刚打完仗,来更新一下之前写了一半的博客。 该篇针对日常写查询的时候,那些动态条件sql 做个简单的封装,自动生成(抛砖引玉,搞个小玩具&a…...
宝塔部署Java+Vue前后端分离项目经验总结
前言 之前部署服务器都是在Linux环境下自己一点一点安装软件,听说用宝塔傻瓜式部署更快,这次浅浅尝试了一把。 确实简单! 1、 买服务器 咋买服务器略,记得服务器装系统就装 Cent OS 7系列即可,我装的7.6。 2、创建…...
【公告】停止更新
CSDN 博客的限制太多了。阅读体验也非常差。后续将不再 CSDN 上更新。 逐步迁移到掘金和个人博客。 欢迎关注 掘金:0xforee 个人博客:0xforee’s blog...
AutoHotKey+VSCode开发扩展推荐
原来一直用的大众推荐的SciTeAHK版,最近发现VSCode更舒服一些,有几个必装的扩展推荐一下: AutoHotkey Plus 请注意不是AutoHotkey Plus Plus。如果在扩展商店里搜索会有两个,一个是Plus,一个是Plus Plus。我选择Pllus&…...
了解 JSON 格式
一、JSON 基础 JSON(JavaScript Object Notation,JavaScript 对象表示法)是一种轻量级的数据交换格式,JSON 的设计目的是使得数据的存储和交换变得简单。 JSON 易于人的阅读和书写,同时也易于机器的解析和生成。尽管 J…...
[RDMA] 高性能异步的消息传递和RPC :Accelio
1. Introduce Accelio是一个高性能异步的可靠消息传递和RPC库,能优化硬件加速。 RDMA和TCP / IP传输被实现,并且其他的传输也能被实现,如共享存储器可以利用这个高效和方便的API的优点。Accelio 是 Mellanox 公司的RDMA中间件,用…...
typescript报错:‘name‘ was also declared here
问题再现 用 Typescript 时, 遇到一个声明常量 name 的报错。代码如下: let name:string"zhangsan"; let num:number1001;执行编译时报错: 原因 在默认状态下,typescript 将 DOM typings 作为全局的运行环境&#…...
第十章:联邦学习视觉案例
代码 传送门...
c语言——输出一个整数的所有因数
//输出一个整数的所有因数 #include<stdio.h> #include<stdlib.h> int main() {int number,i;printf("输入整数:");scanf("%d",&number);printf(" %d 的因数有: ",number);for(i1;i<number;i){if(numb…...
mqtt学习记录
目录 1 匿名登录2 ⽤户名密码登录,配置接收的主题mosquitto 配置文件修改添加⽤户信息添加topic和⽤户的关系登录演示 3 遗嘱机制 1 匿名登录 ⾸先打开三个终端, 启动代理服务:mosquitto -v -v 详细模式 打印调试信息 默认占⽤:…...
爬虫逆向实战(十八)--某得科技登录
一、数据接口分析 主页地址:某得科技 1、抓包 通过抓包可以发现数据接口是AjaxLogin 2、判断是否有加密参数 请求参数是否加密? 查看“载荷”模块可以发现有一个password加密参数和一个__RequestVerificationToken 请求头是否加密? 无…...
Java-数组
什么是数组 数组:可以看成是相同类型元素的一个集合。在内存中是一段连续的空间。 在java中, 数组中存放的元素其类型相同数组的空间是连在一起的每个空间有自己的编号,起始位置的编号为0,即数组的下标。 数组的创建及初始化 数…...
Dart 入门Hello world
1、下载Dart sdk IntelliJ & Android Studio | Dart 2、安装Dart 插件 3、安装后重启IDEA,创建Dart项目 4、创建dart文件 5、编写函数: void main() {print("Hello world"); } 6、运行: 官网学习:Dart 语言开发文…...
HTML是什么?
HTML是什么? 超文本标记语言(英语:HyperText Markup Language,简称:HTML)是一种用于创建网页的标准标记语言。 您可以使用 HTML 来建立自己的 WEB 站点,HTML 运行在浏览器上,由浏览器…...
应用升级/灾备测试时使用guarantee 闪回点迅速回退
1.场景 应用要升级,当升级失败时,数据库回退到升级前. 要测试系统,测试完成后,数据库要回退到测试前。 相对于RMAN恢复需要很长时间, 数据库闪回只需要几分钟。 2.技术实现 数据库设置 2个db_recovery参数 创建guarantee闪回点,不需要开启数据库闪回。…...
云计算——弹性云计算器(ECS)
弹性云服务器:ECS 概述 云计算重构了ICT系统,云计算平台厂商推出使得厂家能够主要关注应用管理而非平台管理的云平台,包含如下主要概念。 ECS(Elastic Cloud Server):即弹性云服务器,是云计算…...
Java如何权衡是使用无序的数组还是有序的数组
在 Java 中,选择有序数组还是无序数组取决于具体场景的性能需求与操作特点。以下是关键权衡因素及决策指南: ⚖️ 核心权衡维度 维度有序数组无序数组查询性能二分查找 O(log n) ✅线性扫描 O(n) ❌插入/删除需移位维护顺序 O(n) ❌直接操作尾部 O(1) ✅内存开销与无序数组相…...
跨链模式:多链互操作架构与性能扩展方案
跨链模式:多链互操作架构与性能扩展方案 ——构建下一代区块链互联网的技术基石 一、跨链架构的核心范式演进 1. 分层协议栈:模块化解耦设计 现代跨链系统采用分层协议栈实现灵活扩展(H2Cross架构): 适配层…...
算法:模拟
1.替换所有的问号 1576. 替换所有的问号 - 力扣(LeetCode) 遍历字符串:通过外层循环逐一检查每个字符。遇到 ? 时处理: 内层循环遍历小写字母(a 到 z)。对每个字母检查是否满足: 与…...
Git 3天2K星标:Datawhale 的 Happy-LLM 项目介绍(附教程)
引言 在人工智能飞速发展的今天,大语言模型(Large Language Models, LLMs)已成为技术领域的焦点。从智能写作到代码生成,LLM 的应用场景不断扩展,深刻改变了我们的工作和生活方式。然而,理解这些模型的内部…...
Golang——9、反射和文件操作
反射和文件操作 1、反射1.1、reflect.TypeOf()获取任意值的类型对象1.2、reflect.ValueOf()1.3、结构体反射 2、文件操作2.1、os.Open()打开文件2.2、方式一:使用Read()读取文件2.3、方式二:bufio读取文件2.4、方式三:os.ReadFile读取2.5、写…...
解决:Android studio 编译后报错\app\src\main\cpp\CMakeLists.txt‘ to exist
现象: android studio报错: [CXX1409] D:\GitLab\xxxxx\app.cxx\Debug\3f3w4y1i\arm64-v8a\android_gradle_build.json : expected buildFiles file ‘D:\GitLab\xxxxx\app\src\main\cpp\CMakeLists.txt’ to exist 解决: 不要动CMakeLists.…...
学习一下用鸿蒙DevEco Studio HarmonyOS5实现百度地图
在鸿蒙(HarmonyOS5)中集成百度地图,可以通过以下步骤和技术方案实现。结合鸿蒙的分布式能力和百度地图的API,可以构建跨设备的定位、导航和地图展示功能。 1. 鸿蒙环境准备 开发工具:下载安装 De…...
算术操作符与类型转换:从基础到精通
目录 前言:从基础到实践——探索运算符与类型转换的奥秘 算术操作符超级详解 算术操作符:、-、*、/、% 赋值操作符:和复合赋值 单⽬操作符:、--、、- 前言:从基础到实践——探索运算符与类型转换的奥秘 在先前的文…...
