vue模板编译原理
前端精髓
共 7877字,需浏览 16分钟
· 2021-09-11
我们都知道 vue 写代码是需要按照固定的格式,比如下面这样:
<template>
<div>{{msg}}</div>
</template>
<script>
export default {
data () {
return {
msg: ''
}
}
}
</script>
把 html 的内容写在 template 里面,经过 vue 的处理,首先会把模板转换为 AST 抽象语法树,然后把 AST 转换为可执行的 render 函数,最后才可以生成真是 DOM,那么今天要分析的内容是如何将模板生成 AST 的。
首先要生成的 AST 结构如下:
function createASTElement (
tag,
attrs,
parent
{
return {
type: 1,
tag: tag,
attrsList: attrs,
attrsMap: attrs,
rawAttrsMap: {},
parent: parent,
children: []
}
}
然后就是把模板的内容都遍历一遍,通过正则去匹配标签的内容。
var unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/;
// Regular Expressions for parsing tags and attributes
var attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
var dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
var ncname = "[a-zA-Z_][\\-\\.0-9_a-zA-Z" + (unicodeRegExp.source) + "]*";
var qnameCapture = "((?:" + ncname + "\\:)?" + ncname + ")";
var startTagOpen = new RegExp(("^<" + qnameCapture));
var startTagClose = /^\s*(\/?)>/;
var endTag = new RegExp(("^<\\/" + qnameCapture + "[^>]*>"));
var doctype = /^<!DOCTYPE [^>]+>/i;
// #7298: escape - to avoid being passed as HTML comment when inlined in page
var reCache = {};
var comment = /^<!\--/;
var conditionalComment = /^<!\[/;
这些正则都是源码里面粘贴过来的,包括匹配开始标签,结束标签,注释等等。
function parseHTML(html, options) {
var stack = [];
var expectHTML = options.expectHTML;
var isUnaryTag$$1 = options.isUnaryTag || no;
var canBeLeftOpenTag$$1 = options.canBeLeftOpenTag || no;
var index = 0;
var last, lastTag;
while (html) {
last = html;
// 确保不是 script 或者 style 者 textarea 标签
if (!lastTag || !isPlainTextElement(lastTag)) {
var textEnd = html.indexOf('<');
if (textEnd === 0) {
// End tag: 匹配结束标签
var endTagMatch = html.match(endTag);
if (endTagMatch) {
var curIndex = index;
advance(endTagMatch[0].length);
parseEndTag(endTagMatch[1], curIndex, index);
continue
}
// Start tag: 匹配开始标签
var startTagMatch = parseStartTag();
if (startTagMatch) {
handleStartTag(startTagMatch);
continue
}
}
var text = (void 0), rest = (void 0), next = (void 0);
if (textEnd >= 0) {
rest = html.slice(textEnd);
while (
!endTag.test(rest) &&
!startTagOpen.test(rest)
) {
// 把标签里面的文字提取出来 hello</div> 提取 hello
next = rest.indexOf('<', 1);
if (next < 0) { break }
textEnd += next;
rest = html.slice(textEnd);
}
text = html.substring(0, textEnd);
}
if (textEnd < 0) {
text = html;
}
if (text) {
advance(text.length); // 去除文字 hello 剩下 </div>
}
if (options.chars && text) {
options.chars(text, index - text.length, index);
}
} else {
var endTagLength = 0;
var stackedTag = lastTag.toLowerCase();
var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'));
var rest$1 = html.replace(reStackedTag, function (all, text, endTag) {
endTagLength = endTag.length;
return ''
});
index += html.length - rest$1.length;
html = rest$1;
parseEndTag(stackedTag, index - endTagLength, index);
}
}
// Clean up any remaining tags
parseEndTag();
function advance(n) {
index += n;
html = html.substring(n);
}
function parseStartTag() {
var start = html.match(startTagOpen);
// [ '<div', 'div', index: 0, input: '<div id="1" bg="2">hello</div>', groups: undefined ]
if (start) {
var match = {
tagName: start[1],
attrs: [],
start: index
};
advance(start[0].length); // 去除 <div,剩下 id="1" bg="2">hello</div>
var end, attr;
// 把属性都存储起来 id="1" bg="2" 去除属性之后剩下 >hello</div>
while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
attr.start = index;
advance(attr[0].length);
attr.end = index;
match.attrs.push(attr);
}
if (end) {
match.unarySlash = end[1];
advance(end[0].length); // 把>或者/>去除剩下 hello</div>
match.end = index;
return match
}
}
}
function handleStartTag(match) {
var tagName = match.tagName;
var unarySlash = match.unarySlash; // 自闭合标签为/ 双标签为‘’空
var unary = !!unarySlash;
var l = match.attrs.length;
var attrs = new Array(l);
for (var i = 0; i < l; i++) {
var args = match.attrs[i];
var value = args[3] || args[4] || args[5] || '';
attrs[i] = {
name: args[1],
value: value
};
// 按照{ name: value } 的形式保存起来,{ id: 1 }
if (options.outputSourceRange) {
attrs[i].start = args.start + args[0].match(/^\s*/).length;
attrs[i].end = args.end;
}
}
// 双标签
if (!unary) {
stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end });
lastTag = tagName;
}
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end);
}
}
// stackedTag, index - endTagLength, index
function parseEndTag(tagName, start, end) {
var pos, lowerCasedTagName;
if (start == null) { start = index; }
if (end == null) { end = index; }
// Find the closest opened tag of the same type
if (tagName) {
lowerCasedTagName = tagName.toLowerCase();
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].lowerCasedTag === lowerCasedTagName) {
break
}
}
} else {
// If no tag name is provided, clean shop
pos = 0;
}
if (pos >= 0) {
// Close all the open elements, up the stack
for (var i = stack.length - 1; i >= pos; i--) {
if (options.end) {
options.end(stack[i].tag, start, end);
}
}
// Remove the open elements from the stack
stack.length = pos;
lastTag = pos && stack[pos - 1].tag;
}
}
}
上面是把所有的标签进行处理,按照顺序遍历之后就可以生成 AST 了。
var currentParent;
var stack = [];
var root;
function end(tag, start) {
var element = stack[stack.length - 1];
// pop stack 出栈
stack.length -= 1;
currentParent = stack[stack.length - 1];
closeElement(element);
}
function start(tag, attrs, unary, start, end) {
var element = createASTElement(tag, attrs, currentParent);
if (!root) {
root = element;
}
if (!unary) {
currentParent = element;
// push stack 入栈
stack.push(element);
}
}
function chars(text, start, end) {
var children = currentParent.children;
if (text) {
var child;
child = {
type: 2,
expression: '',
tokens: '',
text: text
};
if (child) {
children.push(child);
}
}
}
function closeElement (element) {
if (currentParent && !element.forbidden) {
currentParent.children.push(element);
element.parent = currentParent;
}
element.children = element.children.filter(function (c) { return !(c).slotScope; });
}
function createASTElement (
tag,
attrs,
parent
) {
return {
type: 1,
tag: tag,
attrsList: attrs,
attrsMap: attrs,
rawAttrsMap: {},
parent: parent,
children: []
}
}
const content = '<template><div bg="1">hello<span>11</span></div></template>'
parseHTML(content, {
start: start,
end: end,
chars: chars
});
console.log(root) // ast
源码在 vue-template-compiler
评论
6 个火爆 GitHub 的后台管理模板,快来收藏!
将Python客栈设为“星标⭐”第一时间收到最新资讯今天来给大家介绍6个火爆 G 站的管理后台模板,有了它们,对于前端不是很熟悉的小伙伴来说,再也不用烦恼了,而且有一说一,即使是前端大牛,要想从零开发一套完整的管理模板,也不是一件容易的事情。1. vue-element-admin该项目是基于 Vu
Python客栈
0
【第124期】Vue 3 组合式 API 的瑞士军刀(VueUse)
概述 今天,我们要深入探索一个强大的工具集——VueUse。它为 Vue 3 的组合式 API(Composition API)提供了超过 200 个实用函数,让你的开发工作更加得心应手。官方网站:https://vueuse.org/VueUse 是什么?VueUse 的官方网站是 https:
前端微服务
0
每天骑的共享单车是什么通信原理,有人了解过吗?
转自:网络我们经常骑的共享单车到底是什么通信原理,有人了解过吗?现在就带大家了解下。一、智能车锁共享单车最核心的硬件是智能车锁,主要用于实现控制和定位功能。车锁内集成了嵌入式芯片(通信模块),GPS模块和物联网SIM卡。智能锁制造商通过在锁内集成带有独立号码的SIM卡,通过2G、3G、4G网络,与云
菜鸟学Python
1
面试官:谈谈前端路由的实现原理【hash&history】
哈喽,大家好我是考拉🐨。今天我们来聊一聊前端路由。当谈到前端路由时,指的是在前端应用中管理页面导航和URL的机制。前端路由使得单页应用(Single-Page Application,SPA)能够在用户与应用交互时动态地加载不同的视图,而无需每次都重新加载整个页面。在前端开发中,常用的前端路由库有很
程序员成长指北
10
扩散模型的原理及实现(Pytorch)
来源:机器学习算法那些事本文约6500字,建议阅读13分钟本文完整的介绍了有关扩散模型的必要知识,并且使用Pytorch进行了完整的实现。扩散模型的导火索,是始于2020 年所提出的DDPM(Denoising Diffusion Probabilis...
数据派THU
0
跨平台开发的实践与原理
? 这是第 404 篇 不掺水的原创 ,想要了解更多 ,请戳下方卡片 关注我们吧~ 引言 在如今不断增长的小程序市场中,小程序的数量迅速增多。这是因为小程序具有诸多优势,例如轻量化、便捷性和良好的用户体验,吸引...
前端迷
0
Kafka 时间轮(TimingWheel)原理,值得借鉴
在kafka中,有许多请求并不是立即返回,而且处理完一些异步操作或者等待某些条件达成后才返回,这些请求一般都会带有timeout参数,表示如果timeout时间后服务端还不满足返回的条件,就判定此次请求为超时,这时候kaf...
浪尖聊大数据
0
【模板】产品与业务如何有效对齐需求及目标
之前给百度的某产品业务团队分享:“产品与业务如何有效对齐需求”。整理了一个图: 那么针对于“业务BRD”、“用户分析”和“产品PRD”三者之间的关系,各节点需要整理哪些有效的信息呢? 1.业务BRD 梳理出有效的BRD,需要...
强少来了
0