Vue3源码--模板解析成AST

88 阅读3分钟

模板编译

模板编译的第一步就是将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)
  )
}
  1. createParserContext 创建解析的上下文,里面包含模板source列column行line偏移量offset
  2. getCursor 获取当前解析的开始位置 返回是列、行、偏移量
  3. 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
}
  1. last获取父级节点
  2. while循环不断解析,条件是:不是结束标签
  3. 解析{{}}
  4. 解析结束标签
  5. 解析一般元素
  6. 纯文本解析

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

}
  1. 获取父级元素
  2. 解析标签
  3. 判断是否是自闭合标签或者空标签
  4. 解析标签的子元素
  5. 返回解析的元素,包含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
    }
}
  1. 获取属性名称name,如果有相同的name抛出错误,同时前进name长度的字符串
  2. 解析attribute的value
  3. 正则判断是否是Vue的语法如:v-bind,:name.sync,@click,#default插槽这样的属性 具体例子如下:
  4. 如果是match[1]存在就会直接返回(bind,on,slot),没有匹配到,则判断是以点.、*:*开头或者匹配到了参数match[1]的都是bind属性, 否则判断是否@开头。从而确定bind,on,slot
  5. 判断绑定的属性是否是变量,如:v-bind:[src]这样的形式,拿到变量内容content。如果是插槽,会认为匹配到的修饰符和插槽名称是一起的(match[3]=".sync.trim")。最终生成arg变量
  6. modifiers修饰符集合
  7. 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) }
}
  1. 判断属性的value是否带单引号或者双引号
  2. 如果带引号直接前进一个字符位
  3. 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>"
    }
}

总结

  1. advanceBy前进字符的长度,同时修改当前解析的位置
  2. getSelection是获取当前解析文本的开始和结束位置
  3. 最终返回的ast就是一个对象