例如:<div id="foo”v-show=display”/>
上面这段模板中的 div 标签存在一个 id 属性和一个v-show 指令。为了处理属性和指令,我们需要在parseTag函数中增加 parseAttributes 解析函数,如下面的代码所示:
function parseTag(context, type='start'){
const {advanceBy, advanceSpaces} = context
const match = type === 'start'
// 匹配开始标签
? /^<([a-z][^\t\r\n\f />]*)/i.exec(context.source)
// 匹配结束标签
: /^<\/([a-z][^\t\r\n\f />]*)/i.exec(context.source)
const tag = match[1]
advanceBy(match[0].length)
advanceSpaces()
// 调用 parseAttributes 函数完成属性与指令的解析,并得到 props 数组,
// props 数组是由指令节点与属性节点共同组成的数组
const props = parseAttributes(context)
const isSelfClosing = context.source.startsWith('/>')
advanceBy(isSelfClosing? 2 : 1)
// 返回标签节点
return {
type: 'Element',
tag,
props, // 将 props 数组添加到标签节点上
children:[],
isSelfClosing
}
}
上面这段代码的关键点之一是,需要在消费标签的“开始部分”和无用的空白字符之后再调用 parseAttribute 函数。举个例子,假设标签的内容如下:
<div id="foo" v-show="display">
标签的“开始部分”指的是字符串 <div,所以当消耗标签的“开始部分”以及无用空白字符后,剩下的内容为:
id="foo" v-show="display">
上面这段内容才是 parseAttributes 函数要处理的内容。由于该函数只用来解析属性和指令因此会不断地消费上面这段模板内容,直到遇到标签的“结束部分”为止。其中,结束部分指的是字符>或者字符串 />。
因此parseAttributes函数的整体框架如下:
function parseAttributes(context){
const props = []
while(
!context.source.startsWith('>') &&
!context.source.startsWith('/>')
){
// 解析属性或指令
}
// 将解析结果返回
return props
}
实际上,parseAttributes 函数消费模板内容的过程,就是不断地解析属性名称、等于号、属性值的过程
parseAttributes 函数会按照从左到右的顺序不断地消费字符串。
下面是parseAttributes函数的具体实现:
function parseAttributes(context){
const { advanceBy, advanceSpaces } = context
const props = []
while(
!context.source.startsWith('>') &&
!context.source.startsWith('/>')
){
// 该正则用于匹配属性名称
const match = /^[^\t\r\n\f />][^t\r\n\f />=]*/.exec(context,source)
// 得到属性名称
const name = match[0]
// 消费属性名称
advanceBy(name.length)
// 消费属性名称与等于号之间的空白字符
advanceSpaces()
// 消费等于号
advanceBy(1)
// 消费等于号与属性值之间的空白字符
advanceSpaces()
// 属性值
let value = ''
// 获取当前模板内容的第一个字符
const quote = context.source[0]
// 判断属性值是否被引号引用
const isQuoted = quote === '"' || quote === "'"
if(isQuoted){
// 属性值被引号引用,消费引号
advanceBy(1)
// 获取下一个引号的索引
const endQuoteIndex = context.source.indexOf(quote)
if(endQuoteIndex>-1){
// 获取下一个引号之前的内容作为属性值
value = context.source.slice(0,endQuoteIndex)
// 消费属性值
advanceBy(value.length)
// 消费引号
advanceBy(1)
}else{
// 缺少引号错误
console.error('缺少引号')
}
}else{
// 代码运行到这里,说明属性值没有被引号引用
// 下一个空白字符之前的内容全部作为属性值
const match = /^[^\t\r\n\f >]+/.exec(context.source)
// 获取属性值
value = match[0]
// 消费属性值
advanceBy(value.length)
}
// 消费属性值后面的空白字符
advanceSpaces()
props.push({
type: 'Attribute',
name,
value
})
}
//返回
return props
}
在上面这段代码中,有两个重要的正则表达式:
用来匹配属性名称
/^[^\t\r\n\f />][^t\r\n\f />=]*/
用来匹配没有使用引号引用的属性值, 该正则表达式会一直对字符串进行匹配,直到遇到空白字符或字符>为止,
/^[^\t\r\n\f >]+/
配合parseAttributes 函数,假设给出如下模板:
<div id="foo"v-show="display"></div>
解析上面这段模板,将会得到如下AST:
const ast = {
type: 'Root',
children: [
{
type: 'Element'
tag: 'div',
props: [
{type: 'Attribute', name: 'id', value: 'foo'},
{type: 'Attribute', name: 'v-show', value: 'display'}
]
}
]
}
可以看到,在类型为 Attribute 的属性节点中,其 name 字段完整地保留着模板中编写的属性名称。我们可以对属性名称做进一步的分析,从而得到更具体的信息。例如,属性名称以字符@开头,则认为它是一个 v-on 指令绑定。我们甚至可以把以 v- 开头的属性看作指令绑定,从而为它赋予不同的节点类型