Template的编译过程
我们都知道,在写vue描述dom结构的时候,可以写template,也可以写render函数,而template也最终会被编译成render函数用于渲染页面。template编译时会分为3个阶段:解析(parsing)、转换(transform)、生成(generate),如下图所示,在parse阶段,vue-compiler模板转换为AST,在transform阶段,对代码进行各种转换和标记,在generator阶段,再用new Function和with的组合,将AST转换成最终render函数。
-- 图片源自网络,侵删
什么是AST
AST的官方定义
在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。
通俗的讲就是AST用于描述语法的数据结构,有了这些描述信息,就可以用来进行一些我们想要的操作,例如:
- 优化变更代码,改变代码结构使达到想要的结构
- 代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全等等
- 代码混淆压缩
我们可以通过AST Explorer查看代码的抽象语法树结构,例如我们编译器选择@vue/compiler-dom,在左边编辑区域输入代码,右边就会实时生成对应的AST树。
Vue3和Vue2模板编译对比
vue3的模板编译相对于vue2是重写了的,在vue2里面主要通过正则解析模板字符串,然后提取相应的信息组成语法树。但是在vue3里面,主要是递归解析字符串中的词法,少量的用到了正则来匹配目标字符串,进行词法分析,组装成语法树。 vue3一个重大更新就是template里面可以包含多个根节点了,也就是官方所说的Fragment概念。ast里面类似,无论是一个根节点还是多个根节点,都会被包在一个根对象里面,children就是它所有的子节点。
Template编译成AST代码实现
在上面template转成ast结果里根节点 我们看到有3个属性:
type: 节点类型children:子节点loc:位置信息和当前源码
children是一个数组对象,包含着标签语法的信息描述,通常有以下属性:
type: 节点类型children:子节点loc:位置信息tag:标签名isSelfClosing:是否自闭和标签props:属性集合isStatic:是否是静态属性content:内容- 等...
我们就根据AST Explorer生成的结果,和对源码的分析,写一个简易版vue3的template转ast语法树的功能,目前只包含标签,插值表达式,文本,有兴趣的同学可以自己扩展。
下面我们会写一个
baseCompile()用于编译模板成ast;用baseParse()解析模板字符串,标识节点的信息 行、列、偏移量等,并且每解析一段 就移除一部分,为空就解析完成了;用createParserContext()生成解析上下文,用于标识当前词法的信息;用getCursor()记录开始位置;用createRoot()返回一个根节点,它传入子节点和位置信息。代码如下:
// vue3源码里拷贝的节点类型
const NodeTypes = {
ROOT: 1, // 根节点 fragment 解决多个根元素的问题
ElEMENT: 2, // 元素 div, p
TEXT: 3, // 文本
SIMPLE_EXPRESSION: 4, // 简单表达式 <div>{{name}}</div>
INTERPOLATION: 5, // 插值表达式
ATTRIBUTE: 6, // 属性
DIRECTIVE: 7, // 指令
COMPOUND_EXPRESSION: 8, // 组合表达式 {{name}} hello world
TEXT_CALL: 12, // createTextVnode()
VNODE_CALL: 13, // createVnode()
}
// 编译器,用于将template转化ast,和对ast的优化
function baseCompile(template) {
// 讲模板转换成ast语法树
const ast = baseParse(template);
// todo 这里会进行ast的transfer,用于对ast树标记和优化,后面处理
// ...
return ast;
}
// 词法解析函数,用于转ast
function baseParse(content) {
// 标识节点的信息 行、列、偏移量...
// 每解析一段 就移除一部分
const context = createParserContext(content);
const start = getCursor(context); // 记录开始位置
return createRoot(parseChildren(context), getSelection(context, start));
}
// 创建根节点,把parseChildren的果包一层返回出去,产生上面{type,children,loc}那样的结构
function createRoot(children, loc) {
return {
type: NodeTypes.ROOT,
children,
loc,
};
}
// 创建解析上下文, 用于描述代码结构的属性和解析时提供数据
function createParserContext(content) {
return {
line: 1, // 行
column: 1, // 列
offset: 0, // 偏移量
source: content, // 这个source会被不停的移除,等待source为空的时候解析完毕
originalSource: content, // 这个值是不会变的 记录你传入的内容
};
}
// 游标 获取起始位置 行,列,偏移位置
function getCursor(context) {
let { line, column, offset } = context;
return { line, column, offset };
}
// 获取信息对应的开始、结束和当前内容
function getSelection(context, start, end) {
end = end || getCursor(context);
return {
start,
end,
source: context.originalSource.slice(start.offset, end.offset),
};
}
// 是不是解析完毕 :碰到结束标签 || context.source = '' 都会判定解析完成
function isEnd(context) {
const source = context.source;
if(source.startsWith('</')){
return true
}
return !source
}
// 核心方法:解析词法生成ast的节点,这里只写一个简单的版本,只解析文本,插值表达式,标签,要解析其他的类型可以在这里面加逻辑
function parseChildren(context) {
// 根据内容做不同的处理
const nodes = [];
while (!isEnd(context)) {
const s = context.source; // 当前上下文中的内容 < abc {{}}
let node;
if (s[0] == "<") {
// 处理标签
} else if (s.startsWith("{{")) {
// 处理表达式
} else {
// 处理文本
}
nodes.push(node);
// 演示逐字解析过程,后面用advance进位函数代替
context.source = context.source.slice(1)
console.log(s); // 打印剩余的字符串
}
return nodes.filter(Boolean); // 过滤null值
}
下面我们来执行baseCompile()看一下结果
baseCompile(`<div>{{ greeting }}World!</div>`)
// 输出结果 =>
// div>{{ greeting }}World!</div>
// iv>{{ greeting }}World!</div>
// v>{{ greeting }}World!</div>
// >{{ greeting }}World!</div>
// {{ greeting }}World!</div>
// { greeting }}World!</div>
// greeting }}World!</div>
// greeting }}World!</div>
// reeting }}World!</div>
// eeting }}World!</div>
// eting }}World!</div>
// ting }}World!</div>
// ing }}World!</div>
// ng }}World!</div>
// g }}World!</div>
// }}World!</div>
// }}World!</div>
// }World!</div>
// World!</div>
// orld!</div>
// rld!</div>
// ld!</div>
// d!</div>
// !</div>
// </div>
上面的步骤没有问题,那么下面就继续写解析标签的方法:parseElement(),解析表达式的方法:parseInterpolation(),解析文本的方法:parseText(),
// 通过当前上下文解析标签元素
function parseElement(context) {
// 1.解析标签名
let ele = parseTag(context); // <div></div>
// 2.TODO 处理指令,动/静态属性,事件等,这里不做展开了
// 例如 ele = parseDirective(context) ...
// 有子节点就递归处理子节点
const children = parseChildren(context); // 如果遇到结束标签就直接跳出
if (context.source.startsWith('</')) {
parseTag(context); // 解析关闭标签时 同时会移除关闭信息并且更新偏移量
}
ele.children = children;
ele.loc = getSelection(context, ele.loc.start)
return ele;
}
// 解析插值表达式
function parseInterpolation(context) { // }} {{name}}
const start = getCursor(context); // 获取表达式的start位置
const closeIndex = context.source.indexOf('}}', '{{') // 找到'}}'位置索引,从'{{'开始找
advanceBy(context, 2); // 前进2个字符
const innerStart = getCursor(context);
const innerEnd = getCursor(context); // 这个end稍后我们会改
const rawContentLength = closeIndex - 2;// 拿到{{ 内容 }} 包含空格的
const preTrimContent = parseTextData(context, rawContentLength)
const content = preTrimContent.trim(); // 去掉前后空格 " name " => "name"
const startOffset = preTrimContent.indexOf(content); // {{ name }}
if (startOffset > 0) { // 有前面空格
advancePositionWithMutation(innerStart, preTrimContent, startOffset)
}
// 更新innerEnd
const endOffset = content.length + startOffset
advancePositionWithMutation(innerEnd, preTrimContent, endOffset)
advanceBy(context, 2);
return {
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
isStatic: false,
loc: getSelection(context, innerStart, innerEnd),
content
},
loc: getSelection(context, start)
}
}
// 解析文本
function parseText(context) {
// 1.先做文本处理
const endTokens = ["<", "{{"]; // 2种情况:hello</div> 或者 hello {{name}} 就说明文本区结束了
let endIndex = context.source.length; // 文本的整个长度
// 假设法 需要先假设 遇到 < 是结尾 在拿到遇到{{ 去比较那个 在前 就是到哪
for (let i = 0; i < endTokens.length; i++) {
const index = context.source.indexOf(endTokens[i], 1);
// 如果找到了索引并且 小于总长度
if (index !== -1 && endIndex > index) {
endIndex = index;
}
}
// 有了文本的结束位置 我就可以更新行列信息
let start = getCursor(context);
const content = parseTextData(context, endIndex);
return {
type: NodeTypes.TEXT,
content,
loc: getSelection(context, start),
};
}
// 解析标签
function parseTag(context) {
// 获取开始位置信息
const start = getCursor(context); //<div/>
// 最基本的元字符匹配标签元素
const match = /^<\/?([a-z][^ \t\r\n/>]*)/.exec(context.source);
const tag = match[1];
//
advanceBy(context, match[0].length);
advanceSpaces(context);
const isSelfClosing = context.source.startsWith('/>');
advanceBy(context, isSelfClosing ? 2 : 1);
return {
type: NodeTypes.ElEMENT,
tag,
isSelfClosing,
loc: getSelection(context, start)
}
}
// 解析文本节点,并且返回目标文本
function parseTextData(context, endIndex) {
const rawText = context.source.slice(0, endIndex);
advanceBy(context, endIndex); // 在context.source中把文本内容删除掉
return rawText;
}
// 前进,删除context.source中已解析的字符串
function advanceBy(context, endIndex) {
let s = context.source;// 原内容
// 计算出一个新的结束位置
advancePositionWithMutation(context, s, endIndex); // 根据内容和结束索引来修改上下文的信息
context.source = s.slice(endIndex); // 截取内容
}
// 通过传上下文,内容,结束位置信息,处理更新后的上下文中的行,列,和偏移量信息
function advancePositionWithMutation(context, s, endIndex) {
let linesCount = 0; // 行数
let linePos = -1; // 行的左偏移量
for (let i = 0; i < endIndex; i++) {
if (s.charCodeAt(i) == 10) { // 遇到换行就加一行
linesCount++;
linePos = i; // 换行后第一个字符的位置
}
}
context.offset += endIndex; // 更新偏移量
context.line += linesCount; // 更新行数
context.column = linePos == -1 ? context.column + endIndex : endIndex - linePos // 更新列数
}
// 前进空格
function advanceSpaces(context) {
const match = /^[ \t\r\n]+/.exec(context.source);
if (match) {
advanceBy(context, match[0].length)
}
}
验证:
const ast = baseCompile(
`<div>
<p>{{ greeting }} World!</p>
</div>`
)
console.log(ast);
解析结果:
{
"type": 1,
"children": [
{
"type": 2,
"tag": "div",
"isSelfClosing": false,
"loc": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 3,
"column": 8,
"offset": 36
},
"source": "<div>\n <p>{{ name }} 文本</p>\n </div>"
},
"children": [
{
"type": 2,
"tag": "p",
"isSelfClosing": false,
"loc": {
"start": {
"line": 2,
"column": 3,
"offset": 8
},
"end": {
"line": 2,
"column": 23,
"offset": 28
},
"source": "<p>{{ name }} 文本</p>"
},
"children": [
{
"type": 5,
"content": {
"type": 4,
"isStatic": false,
"loc": {
"start": {
"line": 2,
"column": 9,
"offset": 14
},
"end": {
"line": 2,
"column": 13,
"offset": 18
},
"source": "name"
}
},
"loc": {
"start": {
"line": 2,
"column": 6,
"offset": 11
},
"end": {
"line": 2,
"column": 16,
"offset": 21
},
"source": "{{ name }}"
}
},
{
"type": 3,
"content": " 文本",
"loc": {
"start": {
"line": 2,
"column": 16,
"offset": 21
},
"end": {
"line": 2,
"column": 19,
"offset": 24
},
"source": " 文本"
}
}
]
}
]
}
],
"loc": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 3,
"column": 8,
"offset": 36
},
"source": "<div>\n <p>{{ name }} 文本</p>\n </div>"
}
}
源码地址:gitHub
下期预告:手写vue3-complier-dom —— Transform篇