22. 模板转化 AST 语法树
22. 模板转化 AST 语法树
模板编译 Vue 中对 template 属性会编译成 render 方法。在线模板编译器
1.创建 compiler-core
增添新的包compiler-core/package.json
{
"name": "@vue/compiler-core",
"version": "1.0.0",
"description": "@vue/compiler-core",
"main": "index.js",
"module": "dist/compiler-core.esm-bundler.js",
"buildOptions": {
"name": "VueCompilerCore",
"compat": true,
"formats": ["esm-bundler", "cjs"]
}
}
我们将开发环境下的打包入口改为 compile-core,这里我们先提供所需要 ast 的节点类型
export function compile(template) {
// 1.将模板转化成ast语法树
const ast = parse(template);
// 2.对ast语法树进行转化
transform(ast);
// 3.生成代码
return generate(ast);
}
2.生成 ast 语法树
准备语法树相关类型ast.ts
export const enum NodeTypes {
ROOT, // 根节点
ELEMENT, // 元素
TEXT, // 文本
COMMENT, // 注释
SIMPLE_EXPRESSION, // 简单表达式
INTERPOLATION, // 模板表达式
ATTRIBUTE,
DIRECTIVE,
// containers
COMPOUND_EXPRESSION, // 复合表达式
IF,
IF_BRANCH,
FOR,
TEXT_CALL, // 文本调用
// codegen
VNODE_CALL, // 元素调用
JS_CALL_EXPRESSION // js调用表达式
}
创建解析上下文
创建解析上下文,并且根据类型做不同的处理解析。 ast 转化
function createParserContext(content) {
return {
line: 1,
column: 1,
offset: 0,
source: content, // source会不停的被截取
originalSource: content // 原始内容
};
}
function isEnd(context) {
const source = context.source;
return !source;
}
function parseChildren(context) {
const nodes = [];
while (!isEnd(context)) {
const s = context.source;
let node;
if (s.startsWith('{{')) {
// 处理表达式类型
} else if (s[0] === '<') {
// 标签的开头
if (/[a-z]/i.test(s[1])) {
} // 开始标签
}
if (!node) {
// 文本的处理
}
nodes.push(node);
}
return nodes;
}
function parse(template) {
const context = createParserContext(template);
return parseChildren(context);
}
处理文本节点
采用假设法获取文本结束位置
function parseText(context) {
// 123123{{name}}</div>
const endTokens = ['<', '{{'];
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;
}
}
}
处理文本内容,删除匹配到的结果,计算最新上下文位置信息
function parseText(context) {
// ...
let start = getCursor(context); // 1.获取文本开始位置
const content = parseTextData(context, endIndex); // 2.处理文本数据
return {
type: NodeTypes.TEXT,
content,
loc: getSelection(context, start) // 3.获取全部信息
};
}
function getCursor(context) {
// 获取当前位置
let { line, column, offset } = context;
return { line, column, offset };
}
function parseTextData(context, endIndex) {
const rawText = context.source.slice(0, endIndex);
advanceBy(context, endIndex); // 截取内容
return rawText;
}
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) {
// 遇到\n就增加一行
linesCount++;
linePos = i; // 记录换行后的字节位置
}
}
context.offset += endIndex; // 累加偏移量
context.line += linesCount; // 累加行数
// 计算列数,如果无换行,则直接在原列基础 + 文本末尾位置,否则 总位置减去换行后的字节位置
context.column =
linePos == -1 ? context.column + endIndex : endIndex - linePos;
}
function getSelection(context, start) {
const end = getCursor(context);
return {
start,
end,
source: context.originalSource.slice(start.offset, end.offset)
};
}
转化成最终ast
节点结果,标记ast
节点类型
处理表达式节点
获取表达式中的变量,计算表达式的位置信息
function parseInterpolation(context) {
const start = getCursor(context); // 获取表达式的开头位置
const closeIndex = context.source.indexOf('}}', 2); // 找到结束位置
advanceBy(context, 2); // 去掉 {{
const innerStart = getCursor(context); // 计算里面开始和结束
const innerEnd = getCursor(context);
const rawContentLength = closeIndex - 2; // 拿到内容
const preTrimContent = parseTextData(context, rawContentLength);
const content = preTrimContent.trim();
const startOffest = preTrimContent.indexOf(content);
if (startOffest > 0) {
// 有空格
advancePositionWithMutation(innerStart, preTrimContent, startOffest); // 计算表达式开始位置
}
const endOffset = content.length + startOffest;
advancePositionWithMutation(innerEnd, preTrimContent, endOffset);
advanceBy(context, 2);
return {
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
isStatic: false,
content,
loc: getSelection(context, innerStart, innerEnd) // 需要修改getSelection方法
},
loc: getSelection(context, start)
};
}
处理元素节点
处理标签
获取标签名称,更新标签位置信息
function advanceSpaces(context) {
const match = /^[ \t\r\n]+/.exec(context.source);
if (match) {
advanceBy(context, match[0].length);
}
}
function parseTag(context) {
const start = getCursor(context); // 获取开始位置
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 parseElement(context) {
// 1.解析标签名
let ele = parseTag(context);
if (context.source.startsWith('</')) {
parseTag(context); // 解析标签,标签没有儿子,则直接更新标签信息的结束位置
}
ele.loc = getSelection(context, ele.loc.start); // 更新最终位置
return ele;
}
处理子节点
递归处理子节点元素
function isEnd(context) {
const source = context.source;
if (context.source.startsWith('</')) {
// 如果遇到结束标签说明没有子节点
return true;
}
return !source;
}
function parseElement(context) {
let ele = parseTag(context);
const children = parseChildren(context); // 因为结尾标签, 会再次触发parseElement,这里如果是结尾需要停止
if (context.source.startsWith('</')) {
parseTag(context);
}
ele.loc = getSelection(context, ele.loc.start); // 更新最终位置
(ele as any).children = children; // 添加children
return ele;
}
处理属性
在处理标签后处理属性
function parseTag(context) {
const start = getCursor(context);
const match = /^<\/?([a-z][^ \t\r\n/>]*)/.exec(context.source);
const tag = match[1];
advanceBy(context, match[0].length);
advanceBySpaces(context);
let props = parseAttributes(context); // 处理属性
// ......
return {
type: NodeTypes.ELEMENT,
tag,
isSelfClosing,
loc: getSelection(context, start),
props
};
}
function parseAttributes(context) {
const props: any = [];
while (context.source.length > 0 && !context.source.startsWith('>')) {
const attr = parseAttribute(context);
props.push(attr);
advanceSpaces(context); // 解析一个去空格一个
}
return props;
}
function parseAttribute(context) {
const start = getCursor(context);
const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)!;
const name = match[0]; // 捕获到属性名
advanceBy(context, name.length); // 删除属性名
let value;
if (/^[\t\r\n\f ]*=/.test(context.source)) {
// 删除空格 等号
advanceSpaces(context);
advanceBy(context, 1);
advanceSpaces(context);
value = parseAttributeValue(context); // 解析属性值
}
const loc = getSelection(context, start);
return {
type: NodeTypes.ATTRIBUTE,
name,
value: {
type: NodeTypes.TEXT,
content: value.content,
loc: value.loc
},
loc
};
}
function parseAttributeValue(context) {
const start = getCursor(context);
const quote = context.source[0];
let content;
const isQuoteed = quote === '"' || quote === "'";
if (isQuoteed) {
advanceBy(context, 1);
const endIndex = context.source.indexOf(quote);
content = parseTextData(context, endIndex); // 解析引号中间的值
advanceBy(context, 1);
}
return { content, loc: getSelection(context, start) };
}
处理空节点
function parseChildren(context) {
const nodes: any = [];
while (!isEnd(context)) {
//....
}
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (node.type == NodeTypes.TEXT) {
// 如果是文本 删除空白文本,其他的空格变为一个
if (!/[^\t\r\n\f ]/.test(node.content)) {
nodes[i] = null;
} else {
node.content = node.content.replace(/[\t\r\n\f ]+/g, ' ');
}
}
}
return nodes.filter(Boolean);
}
创建根节点
将解析出的节点,再次进行包裹,这样可以支持模板下多个根节点的情况, 也是我们常说的 Fragment
export function createRoot(children, loc) {
return {
type: NodeTypes.ROOT,
children,
loc
};
}
function parse(template) {
// 标识节点的信息 行 列 偏移量
const context = createParserContext(template);
const start = getCursor(context);
return createRoot(parseChildren(context), getSelection(context, start));
}