16.5 解析属性
parseAttributes 的消费过程就是不断地解析「属性名称 → 空白 → 等于号 → 空白 → 属性值」,直到遇到标签的结束部分
>或/>。
一、消费顺序
parseAttributes 会按照从左到右的顺序不断消费字符串:
- 消费属性名称,同时消费名称后面可能存在的空白字符;
- 属性名称解析完毕后,模板剩余内容理论上应以等于号开头,因此消费
=; - 等于号和属性值之间也可能存在空白字符,需要再消费一次空白;
- 解析属性值(三种写法,见下);
- 消费属性值后面的空白字符,然后循环处理下一个属性。
易错点:若属性名称之后不是等于号,说明模板内容不合法(例如无值属性
disabled),简化实现可以选择性地抛出错误,不能盲目地advanceBy(1)把字符吃掉。
二、属性值的三种写法
| 写法 | 示例 | 取值规则 |
|---|---|---|
| 双引号包裹 | id="foo" | 下一个相同引号之前的内容 |
| 单引号包裹 | id='foo' | 下一个相同引号之前的内容 |
| 无引号 | id=foo | 下一个空白字符之前的所有字符 |
三、实现
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
}
四、接入 parseTag
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()
// 解析属性与指令,得到 props 数组
const props = parseAttributes(context)
const isSelfClosing = context.source.startsWith('/>')
advanceBy(isSelfClosing ? 2 : 1)
return { type: 'Element', tag, props, children: [], isSelfClosing }
}
props数组是由属性节点与指令节点共同组成的数组(v-show这类指令同样先按名称/值解析出来);- 解析结果示例:
parse('<div id="foo" v-show="display"></div>')。
易错点:属性值的引号是成对的,取到闭引号后必须把闭引号也消费掉;无引号写法遇到
>就应停止,不能把标签的结束符吞进属性值。
相关
- 16.4 解析标签节点:
parseTag如何调用parseAttributes并返回 Element 节点 - 16.6.2 解码命名字符引用:属性值同样需要按 HTML 规范解码实体