携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第21天,点击查看活动详情
这次讲compiler
模块的parse
,用例子和简单的代码实现来看看是怎么将模板转化成ast
节点树的。
parse
简单说下过程
生成根节点,传入的作为children解析
从左到右遍历探查,
<
开头则解析元素,{{
则解析插值节点,其他的归为文本节点,一直到</
或者字符结束
工具函数
advanceBy 吃掉一段字符
就是用这个方法,解析完的字符要吃掉,最后吃光光
function advanceBy(context, numberOfCharacters) {
const { source } = context;
context.source = source.slice(numberOfCharacters);
}
parseTextData 吃掉的文本
解析文本时,拿取文本尾部索引,吃掉文本并且返回文本内容
function parseTextData(context, length) {
const rawText = context.source.slice(0, length);
advanceBy(context, length);
return rawText;
}
吃掉空白区域
比如 id="foo" v-if
中间就有空格,吃掉,方便下个属性的解析
function advanceSpaces(context) {
const match = /^[\t\r\n\f ]+/.exec(context.source);
if (match) {
advanceBy(context, match[0].length);
}
}
辨别自闭合标签、元素和组件
const HTML_TAGS =
'html,body,base,head,link,meta,style,title,address,article,aside,footer,' +
'header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,div,dd,dl,dt,figcaption,' +
'figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,' +
'data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,s,samp,small,span,strong,sub,sup,' +
'time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,' +
'canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,' +
'th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,' +
'option,output,progress,select,textarea,details,dialog,menu,' +
'summary,template,blockquote,iframe,tfoot';
const VOID_TAGS =
'area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr';
function makeMap(str) {
const map = str
.split(',')
.reduce((map, item) => ((map[item] = true), map), Object.create(null));
return (val) => !!map[val];
}
export const isVoidTag = makeMap(VOID_TAGS);
export const isNativeTag = makeMap(HTML_TAGS);
横杆转驼峰
// my-class =》myClass
const camelizeRE = /-(\w)/g;
export function camelize(str) {
// _:-c,c:c
return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''));
}
// 源码:在vue3中,加了闭包缓存
const cacheStringFunction$1 = (fn) => {
const cache = Object.create(null);
return ((str) => {
const hit = cache[str];
return hit || (cache[str] = fn(str));
});
};
const camelize = cacheStringFunction$1((str) => {
return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''));
});
节点类型
export const NodeTypes = {
ROOT: 'ROOT',
ELEMENT: 'ELEMENT',
TEXT: 'TEXT',
SIMPLE_EXPRESSION: 'SIMPLE_EXPRESSION',
INTERPOLATION: 'INTERPOLATION',
ATTRIBUTE: 'ATTRIBUTE',
DIRECTIVE: 'DIRECTIVE',
};
export const ElementTypes = {
ELEMENT: 'ELEMENT',
COMPONENT: 'COMPONENT',
};
添加根节点
export function createRoot(children) {
return {
type: NodeTypes.ROOT,
children,
};
}
export function parse(content) {
// 添加配置
const context = createParserContext(content);
// 加根节点,开始解析
return createRoot(parseChildren(context));
}
function createParserContext(content) {
return {
options: {
delimiters: ['{{', '}}'], // 插值节点起始和终点
isVoidTag, // 放这是为了跨平台支持,作用是为了区分是否是自闭和标签元素
isNativeTag, // 区分是否是html标签,为了区别是元素还是组件
},
source: content,
};
}
解析
例子:<div id="foo" v-if="ok">hello {{name}}</div>
function parseChildren(context) {
const nodes = [];
// 没结束一直循环
while (!isEnd(context)) {
const s = context.source;
let node;
// 插值节点
if (s.startsWith(context.options.delimiters[0])) {
// '{{'
node = parseInterpolation(context);
} else if (s[0] === '<') {
// 元素节点
node = parseElement(context);
} else {
// 文本节点
node = parseText(context);
}
nodes.push(node);
}
return nodes
}
元素节点
// id="foo" v-if="ok">
function parseElement(context) {
// Start tag. 解析标签,解析到 > 或者 />
const element = parseTag(context);
// 只用 /> 判断是否是自闭合不够,虽然 hr 是自闭和 但是<hr/> 和 <hr> 两种写法都被允许
// 所以还用 isVoidTag 枚举判断 是否是 自闭和标签
if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
return element;
}
// Children. 其他的内容当children hello {{name}}</div>
element.children = parseChildren(context);
// End tag. 删闭合 '/>' 或者 >
parseTag(context);
return element;
}
function parseTag(context) {
// Tag open.
// <div > : < 开头、或者</
// 接小写字母,后面接上非空格等元素
// 匹配 <div ,match = [<,div] ,也匹配 </div>
const match = /^</?([a-z][^\t\r\n\f />]*)/i.exec(context.source);
const tag = match[1];
advanceBy(context, match[0].length); //删掉 <div
advanceSpaces(context); //去掉div后面的空格
// Attributes. 吃掉属性 删掉字符
const { props, directives } = parseAttributes(context);
// Tag close. 闭合标签,看是否是自闭和,自闭和删两个,正常闭删1个
const isSelfClosing = context.source.startsWith('/>');
advanceBy(context, isSelfClosing ? 2 : 1);
// 枚举标签名,得出是否是组件
const tagType = isComponent(tag, context)
? ElementTypes.COMPONENT
: ElementTypes.ELEMENT;
return {
type: NodeTypes.ELEMENT,
tag, //标签名 div
tagType, // 组件还是标签, element
props, // [id]
directives, // [v-if]
isSelfClosing, // 是否自闭和,false
children: [],
};
}
// 枚举标签名,得出是否是组件
function isComponent(tag, context) {
const { options } = context;
return !options.isNativeTag(tag);
}
// 解析属性
function parseAttributes(context) {
const props = [];
const directives = [];
// 当字符还有且没结束时,循环
while (
context.source.length &&
!context.source.startsWith('>') &&
!context.source.startsWith('/>')
) {
const attr = parseAttribute(context);
if (attr.type === NodeTypes.ATTRIBUTE) {
props.push(attr);
} else {
directives.push(attr);
}
}
return { props, directives };
}
function parseAttribute(context) {
// Name. v-bind:class='abc'
// name判断很宽除了下述几个字符外都支持 非空格开头,后接非等于号空格以外的字符
const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source);
const name = match[0];
// 取到名字 v-bind:class 后删掉
advanceBy(context, name.length);
advanceSpaces(context);
// Value
let value;
// 有可能是 chcked 若有等于号,删掉,去除等号两边空格,解析内容
if (context.source[0] === '=') {
advanceBy(context, 1);
advanceSpaces(context);
// 拿到 值
value = parseAttributeValue(context);
advanceSpaces(context);
}
// 如果是v-|:|@)开头就是指令节点,否则是属性节点
// Directive name : v-bind:class
if (/^(v-|:|@)/.test(name)) {
let dirName, argContent;
if (name[0] === ':') {
dirName = 'bind';
argContent = name.slice(1);
} else if (name[0] === '@') {
dirName = 'on';
argContent = name.slice(1);
} else if (name.startsWith('v-')) {
// v-bind:class =》 [bind,class]
[dirName, argContent] = name.slice(2).split(':');
}
return {
type: NodeTypes.DIRECTIVE,
name: dirName, // bind
exp: value && { // abc
type: NodeTypes.SIMPLE_EXPRESSION,
content: value.content,
isStatic: false,
},
arg: argContent && { // class ,v-if则是空值
type: NodeTypes.SIMPLE_EXPRESSION,
content: camelize(argContent),
isStatic: true,
}
};
}
// Attribute
return {
type: NodeTypes.ATTRIBUTE,
name,
value: value && {
type: NodeTypes.TEXT,
content: value.content,
},
};
}
function parseAttributeValue(context) {
// 不考虑没有引号的情况
// 取到 '
const quote = context.source[0];
advanceBy(context, 1);
// 找到后引号,拿出里面的值
const endIndex = context.source.indexOf(quote);
const content = parseTextData(context, endIndex);
advanceBy(context, 1);
return { content };
}
文本节点
到 hello{{}}
了
// 不支持文本节点中带有'<'符号 hello{{}}
function parseText(context) {
const endTokens = ['<', context.options.delimiters[0]];
// 找在 < 前面 的 {{ ,找不到就用 < 结尾
// 寻找text最近的endIndex。因为遇到'<'或'{{'都可能结束
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;
}
}
const content = parseTextData(context, endIndex);
return {
type: NodeTypes.TEXT,
content,
};
}
插值节点
到 {{name}}</div>
了
function parseInterpolation(context) {
const [open, close] = context.options.delimiters;
advanceBy(context, open.length);
const closeIndex = context.source.indexOf(close);
// trim() 去掉name两边可能存在的空格
const content = parseTextData(context, closeIndex).trim();
advanceBy(context, close.length);
return {
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
isStatic: false,
content,
},
};
}
删除空白
vue-next-template-explorer.netlify.app/#eyJzcmMiOi…
遍历生成的节点,删掉字符之间多余的空格,删掉两个之间有换行的元素中间的空白节点
# 例子1
<div>
a
b
</div>
把两个字符中间的换行和空格 换成单纯的一个空格
export function render(_ctx, _cache, $props, $setup, $data, $options) {
//return (_openBlock(), _createElementBlock("div", null, "\r\n a\r\n b\r\n"))
return (_openBlock(), _createElementBlock("div", null, " a b "))
}
# 例子2
<div>
<span>a</span>
<span>b</span> <span>c</span>
</div>
可以看到,两个节点中间有换行,则可以删掉中间的空白节点,如果不是换行,则要保留
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("span", null, "a"),
_createElementVNode("span", null, "b"),
_createTextVNode(),
_createElementVNode("span", null, "c")
]))
}
代码
function parseChildren(context) {
const nodes = [];
。。。
let removedWhitespace = false;
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)) {
const prev = nodes[i - 1];
const next = nodes[i + 1];
// 开头和结尾是空白,删掉空白节点
if (
!prev ||
!next ||
(prev.type === NodeTypes.ELEMENT &&
next.type === NodeTypes.ELEMENT &&
/[\r\n]/.test(node.content))
) {
// <span>b</span> 换行 <span>c</span>
// 不能在这里删,会影响节点遍历,打标志,说明有删除操作
removedWhitespace = true;
nodes[i] = null;
} else {
// <span>b</span> <span>c</span>
// Otherwise, the whitespace is condensed into a single space
node.content = ' ';
}
} else {
// a b
node.content = node.content.replace(/[\t\r\n\f ]+/g, ' ');
}
}
}
// 有删除操作 去掉删掉的节点
return removedWhitespace ? nodes.filter(Boolean) : nodes;
}