模板编译
模板编译的第一步就是将template编译成ast
这里面已经隐藏了*<![CDATA[]]> xml语法、<!-- -->注释*的解析
具体过程如下:
1、入口 baseParse
路径 vue-next-master/packages/compiler-core/src/parse.ts
export function baseParse(
content: string,
options: ParserOptions = {}
): RootNode {
const context = createParserContext(content, options)
const start = getCursor(context)
return createRoot(
parseChildren(context, TextModes.DATA, []),
getSelection(context, start)
)
}
- createParserContext 创建解析的上下文,里面包含模板source、列column、行line、偏移量offset
- getCursor 获取当前解析的开始位置 返回是列、行、偏移量
- parseChildren 参数是当前上下文、文本模式(默认是0)、解析栈(通过这个找父子关系) 返回的是解析出来的子节点
2、解析子节点 parseChildren
先看个简版的parseChildren
function parseChildren(
context,
mode,
ancestors
) {
const parent = last(ancestors)
// const ns = parent ? parent.ns : Namespace.HTML
const nodes = []
while (!isEnd(context, mode, ancestors)) {
const s = context.source
let node
if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {
// '{{'
node = parseInterpolation(context, mode)
} else if (mode === TextModes.DATA && s[0] === '<') {
if (s.length === 1) {
// 错误
console.error(`${context.originalSource} 错误`)
} else if (s[1] === '/') {
// 可能是结束标签
if (/[a-z]/i.test(s[2])) {
parseTag(context, TagType.End, parent)
continue
}
} else if (/[a-z]/i.test(s[1])) {
// 标签解析
node = parseElement(context, ancestors)
}
}
}
if (!node) {
// 纯文本解析
node = parseText(context, mode)
}
if (Array.isArray(node)) {
for (let i = 0; i < node.length; i++) {
pushNode(nodes, node)
}
} else {
pushNode(nodes, node)
}
}
return nodes
}
- last获取父级节点
- while循环不断解析,条件是:不是结束标签
- 解析{{}}
- 解析结束标签
- 解析一般元素
- 纯文本解析
2.1 先看文本解析 parseText
function parseText(context, mode) {
const endTokens = mode === TextModes.CDATA ? [']]>'] : ['<', context.options.delimiters[0]]
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 start = getCursor(context)
const content = parseTextData(context, endIndex, mode)
return {
type: 2,
content,
loc: getSelection(context, start)
}
}
根据解析类型拿取结束标记
拿到第一个是‘<’或者‘{{’开头的位置
parseTextData解析文本的内容,同时前进解析的位置,返回解析出来的文本
getSelection是获取当前解析文本的开始和结束位置的
function parseTextData(
context,
length,
mode) {
const rawText = context.source.slice(0, length)
advanceBy(context, length)
if (mode === TextModes.RAWTEXT || mode === TextModes.CDATA
|| rawText.indexOf('&') === -1) {
return rawText
} else {
// 替换文本中 &(gt|lt|amp|apos|quot) 为 > < & ' "
return context.options.decodeEntities(rawText, mode === TextModes.ATTRIBUTE_VALUE)
}
}
2.2 解析元素
function parseElement(
context,
ancestors
) {
const parent = last(ancestors)
const element = parseTag(context, TagType.Start, parent)
// 自闭合标签的判断
if (element.isSelClosing || isVoidTag(element.tag)) {
return element
}
ancestors.push(element)
const children = parseChildren(context, 0, ancestors)
ancestors.pop()
element.children = children
element.loc = getSelection(context, element.loc.start)
return element
}
- 获取父级元素
- 解析标签
- 判断是否是自闭合标签或者空标签
- 解析标签的子元素
- 返回解析的元素,包含tag、props、children
2.2.1 解析标签
function parseTag(
context,
type,
parent
) {
const start = getCursor(context)
const match = /^</?([a-z][^\t\r\n\f />]*)/i.exec(context.source)
const tag = match[1]
const ns = context.options.getNamespace(tag, parent)
advanceBy(context, match[0].length)
advanceSpaces(context)
// 解析attributes
let props = parseAttributes(context, type)
// 处理v-pre
// todo
// 标签自闭合
let isSelClosing = false
if (context.source.length === 0) {
console.error('标签闭合错误')
} else {
isSelClosing = startsWith(context.source, '/>')
if (type === TagType.End && isSelClosing) {
console.error(`END_TAG_WITH_TRAILING_SOLIDUS`)
}
advanceBy(context, isSelClosing ? 2 : 1)
}
if (type === TagType.End) return
let tagType = 0
if (!context.inVPre) {
if (tag === 'slot') {
tagType = 2
} else if (tag === 'template') {
// 模板的处理
} else if (isComponent(tag)) {
// 组件
tagType = 1
}
}
return {
type: 0,
ns,
tag,
tagType,
props,
isSelClosing,
children: [],
loc: getSelection(context, start),
codegenNode: undefined
}
}
Vue3的正则比Vue2简单了很多
剔除空格和换行符号,解析标签属性
判断是否是一个组件,直接是所有原生标签做了一个map来判断的
最终生成的AST比较重要的属性是tag,type,props,children
2.2.2 标签属性解析
简化后的代码
function parseAttribute(
context,
nameSet
) {
const start = getCursor(context)
const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)
const name = match[0]
if (nameSet.has(name)) {
console.error(`attribute 相同了${name}`)
}
nameSet.add(name)
// 检查name是否合法
const pattern = /["'<]/g
let m
while (m = pattern.exec(name)) {
console.error(`属性${name} 不合法`)
}
advanceBy(context, name.length)
// attribute的value解析
let value
if (/^[\t\r\n\f ]*=/.test(context.source)) {
advanceSpaces(context)
advanceBy(context, 1) // 去除=号
advanceSpaces(context)
value = parseAttributeValue(context)
if (!value) {
console.error(`${name} 没有属性`)
}
}
console.log(value)
const loc = getSelection(context, start)
if (/^(v-[A-Za-z0-9]|:|.|@|#)/.test(name)) {
const match = /(?:^v-([a-z0-9-]+))?(?:(?::|^.|^@|^#)([[^]]+]|[^.]+))?(.+)?$/i.exec(name)
let isPropShorthand = startsWith(name, '.')
let dirName =
match[1] ||
(isPropShorthand || startsWith(name, ':')
? 'bind'
: startsWith(name, '@')
? 'on'
: 'slot')
let arg
if (match[2]) {
const isSlot = dirName === 'slot'
const startOffset = name.lastIndexOf(match[2])
const loc = getSelection(
context,
getNewPosition(context, start, startOffset),
getNewPosition(context,
start,
startOffset + match[2].length + ((isSlot && match[3]) || '').length
)
)
let content = match[2]
let isStatic = true
if (content.startsWith('[')) {
isStatic = false
if (!content.endsWith(']')) {
console.error(`[没有闭合`)
content = content.slice(1)
} else {
content = content.slice(1, content.length - 1)
}
} else if (isSlot) {
// match[3] = '.sync.trim' || undefined
content += match[3] || ''
}
arg = {
type: 4,
content,
isStatic,
constType: isStatic ? 3 : 0,
loc
}
}
if (value && value.isQuote) {
const valueLoc = value.loc
valueLoc.start.offset ++
valueLoc.start.column ++
valueLoc.end = advancePositionWhiteClone(valueLoc.start, value.content)
valueLoc.source = valueLoc.source.slice(1, -1)
}
// 修饰符
const modifiers = match[3] ? match[3].slice(1).split('.') : []
if (isPropShorthand) modifiers.push('prop')
// v-bind:foo.sync -> v-model:foo
if (dirName === 'bind' && arg) {
if (
modifiers.includes('sync')
) {
dirName = 'model'
modifiers.splice(modifiers.indexOf('sync'), 1)
}
}
return {
type: 7,
name: dirName,
exp: value && {
type: 4,
content: value.content,
constType: 0,
loc: value.loc
},
arg,
modifiers,
loc
}
}
return {
type: 6,
name,
value: value && {
type: 2,
content: value.content,
loc: value.loc
},
loc
}
}
- 获取属性名称name,如果有相同的name抛出错误,同时前进name长度的字符串
- 解析attribute的value
- 正则判断是否是Vue的语法如:v-bind,:name.sync,@click,#default插槽这样的属性 具体例子如下:
- 如果是match[1]存在就会直接返回(bind,on,slot),没有匹配到,则判断是以点.、*:*开头或者匹配到了参数match[1]的都是bind属性, 否则判断是否@开头。从而确定bind,on,slot
- 判断绑定的属性是否是变量,如:v-bind:[src]这样的形式,拿到变量内容content。如果是插槽,会认为匹配到的修饰符和插槽名称是一起的(match[3]=".sync.trim")。最终生成arg变量
- modifiers修饰符集合
- v-bind:src.sync这种语法转换成v-model:src的语法,说明Vue3是支持:src.sync的语法的,内部模板编译时会拉平这个差异
constType 属性静态提升
export const enum ConstantTypes {
NOT_CONSTANT = 0, // 不能静态提升的
CAN_SKIP_PATCH = 1, // patch时可以跳过的
CAN_HOIST = 2, // 能静态提升
CAN_STRINGIFY = 3 // 能够静态提升的
}
2.2.3解析attribute的value
function parseAttributeValue(context) {
const start = getCursor(context)
let content
const quote = context.source[0]
const isQuote = quote === `"` || quote === `'`
if (isQuote) {
advanceBy(context, 1)
const endIndex = context.source.indexOf(quote)
if (endIndex === -1) {
// 没有找到引用,认为整个字符串都是value
content = parseTextData(context, context.source.length, TextModes.ATTRIBUTE_VALUE)
} else {
content = parseTextData(context, endIndex, TextModes.ATTRIBUTE_VALUE)
advanceBy(context, 1)
}
} else {
// 没有引号
const match = /^[^\t\r\n\f >]+/.exec(context.source)
if (!match) {
return undefined
}
const unexpectedChars = /["'<=`]/g
let m
while (m = unexpectedChars.exec(match[0])) {
console.error(`不能出现的符号, ${start}`)
}
content = parseTextData(context, match[0].length, TextModes.ATTRIBUTE_VALUE)
}
return { content, isQuote, loc: getSelection(context, start) }
}
- 判断属性的value是否带单引号或者双引号
- 如果带引号直接前进一个字符位
- parseTextData解析value
生成根节点createRoot
export function baseParse(
content,
options = {}
) {
const context = createParserContext(content, options)
const start = getCursor(context)
return createRoot(
parseChildren(context, TextModes.DATA, []),
getSelection(context, start)
)
}
export function createRoot(
children,
loc
) {
return {
type: 0,
children,
helpers: [],
components: [],
directives: [],
hoists: [],
imports: [],
cached: 0,
temps: 0,
codegenNode: undefined,
loc
}
}
举例
输入模板
<form id="search" v-model:value="bb">
{{search}} <input name="query" v-model="searchQuery">
</form>
输出ast
{
"type": 0,
"children": [
{
"type": 0,
"ns": 0,
"tag": "form",
"tagType": 1,
"props": [
{
"type": 6,
"name": "id",
"value": {
"type": 2,
"content": "search",
"loc": {
"start": {
"column": 10,
"line": 1,
"offset": 9
},
"end": {
"column": 18,
"line": 1,
"offset": 17
},
"source": "\"search\""
}
},
"loc": {
"start": {
"column": 7,
"line": 1,
"offset": 6
},
"end": {
"column": 18,
"line": 1,
"offset": 17
},
"source": "id=\"search\""
}
},
{
"type": 7,
"name": "model",
"exp": {
"type": 4,
"content": "bb",
"constType": 0,
"loc": {
"start": {
"column": 34,
"line": 1,
"offset": 33
},
"end": {
"column": 36,
"line": 1,
"offset": 35
},
"source": "bb"
}
},
"arg": {
"type": 4,
"content": "value",
"isStatic": true,
"constType": 3,
"loc": {
"start": {
"column": 27,
"line": 1,
"offset": 26
},
"end": {
"column": 32,
"line": 1,
"offset": 31
},
"source": "value"
}
},
"modifiers": [],
"loc": {
"start": {
"column": 19,
"line": 1,
"offset": 18
},
"end": {
"column": 37,
"line": 1,
"offset": 36
},
"source": "v-model:value=\"bb\""
}
}
],
"isSelClosing": false,
"children": [
{
"type": 2,
"content": "\n ",
"loc": {
"start": {
"column": 38,
"line": 1,
"offset": 37
},
"end": {
"column": 13,
"line": 2,
"offset": 50
},
"source": "\n "
}
},
{
"type": 5,
"content": {
"type": 4,
"isStatic": false,
"constType": 0,
"content": "search",
"loc": {
"start": {
"column": 15,
"line": 2,
"offset": 52
},
"end": {
"column": 20,
"line": 2,
"offset": 57
},
"source": "searc"
}
},
"loc": {
"start": {
"column": 13,
"line": 2,
"offset": 50
},
"end": {
"column": 23,
"line": 2,
"offset": 60
},
"source": "{{search}}"
}
},
{
"type": 2,
"content": " ",
"loc": {
"start": {
"column": 23,
"line": 2,
"offset": 60
},
"end": {
"column": 24,
"line": 2,
"offset": 61
},
"source": " "
}
},
{
"type": 0,
"ns": 0,
"tag": "input",
"tagType": 1,
"props": [
{
"type": 6,
"name": "name",
"value": {
"type": 2,
"content": "query",
"loc": {
"start": {
"column": 36,
"line": 2,
"offset": 73
},
"end": {
"column": 43,
"line": 2,
"offset": 80
},
"source": "\"query\""
}
},
"loc": {
"start": {
"column": 31,
"line": 2,
"offset": 68
},
"end": {
"column": 43,
"line": 2,
"offset": 80
},
"source": "name=\"query\""
}
},
{
"type": 7,
"name": "model",
"exp": {
"type": 4,
"content": "searchQuery",
"constType": 0,
"loc": {
"start": {
"column": 53,
"line": 2,
"offset": 90
},
"end": {
"column": 64,
"line": 2,
"offset": 101
},
"source": "searchQuery"
}
},
"modifiers": [],
"loc": {
"start": {
"column": 44,
"line": 2,
"offset": 81
},
"end": {
"column": 65,
"line": 2,
"offset": 102
},
"source": "v-model=\"searchQuery\""
}
}
],
"isSelClosing": false,
"children": [],
"loc": {
"start": {
"column": 24,
"line": 2,
"offset": 61
},
"end": {
"column": 66,
"line": 2,
"offset": 103
},
"source": "<input name=\"query\" v-model=\"searchQuery\">"
}
},
{
"type": 2,
"content": "\n ",
"loc": {
"start": {
"column": 66,
"line": 2,
"offset": 103
},
"end": {
"column": 9,
"line": 3,
"offset": 112
},
"source": "\n "
}
}
],
"loc": {
"start": {
"column": 1,
"line": 1,
"offset": 0
},
"end": {
"column": 9,
"line": 3,
"offset": 112
},
"source": "<form id=\"search\" v-model:value=\"bb\">\n {{search}} <input name=\"query\" v-model=\"searchQuery\">\n "
}
}
],
"helpers": [],
"components": [],
"directives": [],
"hoists": [],
"imports": [],
"cached": 0,
"temps": 0,
"loc": {
"start": {
"column": 1,
"line": 1,
"offset": 0
},
"end": {
"column": 16,
"line": 3,
"offset": 119
},
"source": "<form id=\"search\" v-model:value=\"bb\">\n {{search}} <input name=\"query\" v-model=\"searchQuery\">\n </form>"
}
}
总结
- advanceBy前进字符的长度,同时修改当前解析的位置
- getSelection是获取当前解析文本的开始和结束位置
- 最终返回的ast就是一个对象