Vue3 源码头脑风暴之 2 ☞compiler-core - ast parser
文章目录
stb-vue-next 完全拷贝于 vue-next ,主要目的学习及尝试应用于机顶盒环境。
本文依据 commit 进程进行记录,只要跟着下面的进程走,你将能完整实 现 vue ast parser 哦 💃🏼💃🏼💃🏼
声明:该篇为 ts 源码(commit)版本,之前做过一遍完整的 js 版本,更详细,也可参考
脑图
compiler-core parser 初始化
Vue3.0 源码系列(二)编译器核心 - Compiler core 1: parse.ts
c0a03af add baseParse declaration
feat(add): baseParse declaration · gcclll/stb-vue-next@c0a03af
添加
baseParse()
函数声明:1 2 3
export function baseParse(content: string, options: ParserOptions): RootNode { return {} as RootNode }
cb2d452 init baseParse function
feat: baseParse function · gcclll/stb-vue-next@cb2d452
增加
baseParse
函数实现,和涉及到的一些函数和类型声明。1 2 3 4 5 6 7 8
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) ) }
870343c add parseChildren function
4c6009d add pure text parser(parseText, parseTextData)
feat(add): parseText, parseTextData · gcclll/stb-vue-next@4c6009d
fix lint errors: 005c261
fix: lint errors · gcclll/stb-vue-next@005c261
新增了三个函数:
pushNode(nodes: TemplateChildNode[], node: TemplateChildNode): void
遍历 while 解析后的 ast ,合并相邻的文本节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
function pushNode(nodes: TemplateChildNode[], node: TemplateChildNode): void { if (node.type === NodeTypes.TEXT) { // 合并两个相邻的文本内容 const prev = last(nodes) // Merge if both this and the previous node are text and those are // consecutive. This happens for cases like "a < b". if ( prev && prev.type === NodeTypes.TEXT && prev.loc.end.offset === node.loc.start.offset ) { prev.content += node.content prev.loc.end = node.loc.end prev.loc.source += node.loc.source return } } nodes.push(node) }
parseText(context: ParserContext, mode: TextModes): TextNode
解析文本节点,文本节点结束标识:
<
和{{
,分别代表标签和插值开始符号。如:
some text<div>....
,some text{{ ... }}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
function parseText(context: ParserContext, mode: TextModes): TextNode { __TEST__ && assert(context.source.length > 0) const endTokens = ['<', context.options.delimiters[0]] if (mode === TextModes.CDATA) { endTokens.push(']]>') } 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 } } __TEST__ && assert(endIndex > 0) const start = getCursor(context) const content = parseTextData(context, endIndex, mode) return { type: NodeTypes.TEXT, content, loc: getSelection(context, start) } }
function parseTextData(context: ParserContext, length: number, mode: TextModes): string
处理 HTML 一些特殊符号,比如:
a > b
=>a < b
1 2 3 4 5 6 7 8
const decodeRE = /&(gt|lt|amp|apos|quot);/g const decodeMap: Record<string, string> = { gt: '>', lt: '<', amp: '&', apos: "'", quot: '"' }
测试:
|
|
+RESULTS: 如结果显示 <
, >
等符号会被转成语义化符号。
>>> 普通文本 "some text" { type: 0, children: [ { type: 2, content: 'some text', loc: [Object] } ], } >>> 带 html 语义符号的文本 "a < b" { type: 0, children: [ { type: 2, content: 'a < b', loc: [Object] } ], } undefined
d7dbc28 add comment parser(parseComment)
feat(add): comment parser · gcclll/stb-vue-next@d7dbc28
修改 parseChildren()
:
else if s[0] === '<'
作为开始,可能是标签、html 注释等等。
代码:
|
|
通过
/--(\!)?>/
匹配注释的结束如果无法匹配到,说明是非法注释,如:
<!-- xxx ->
匹配到之后的非法情况(
match.index <= 3
):<!-->
或<!--->
捕获组(
(\!)
)也匹配到了,非法结束:<!-- --!>
嵌套注释也视为非法
测试:
|
|
+RESULTS:
>>> 非法注释:"<!-- xxx ->" Unexpected EOF in comment. >>> 非法注释:"<!--->" Illegal comment. >>> 非法注释:"<!-- xx --!>" Incorrectly closed comment. >>> 嵌套注释:"<!-- <!-- -->" Unexpected '<!--' in comment. >>> 有效注释 { type: 0, children: [ { type: 3, content: ' xx ', loc: [Object] } ], // ... }
7d5f9c4 add bogus comment parser(parseBogusComment)
feat(add): bogus comment parser · gcclll/stb-vue-next@7d5f9c4
匹配正则: /^<(?:[\!\?]|\/[^a-z>])/i
<!DOCTYPE
注释<![[CDATA>
类型
|
|
测试:
|
|
+RESULTS:
CDATA section is allowed only in XML context. { type: 0, children: [ { type: 3, content: 'DOCTYPE xxx ', loc: [Object] } ], }
cef8485 add more error element situations
feat(add): more error element situations · gcclll/stb-vue-next@cef8485
更多错误标签情况,以 </
开头的情况处理。
|
|
+RESULTS:
Unexpected EOF in tag. End tag name was expected. Invalid end tag. '<?' is allowed only in XML context. Illegal tag name. Use '<' to print '<'.
b8cb825 add interpolation parser
feat(add): interpolation parser · gcclll/stb-vue-next@b8cb825
插值解析。
|
|
执行操作:
根据
{{
,}}
取出插值起始索引截取插值内容,替换 html 语义字符,且去掉前后空格
组装插值结构
|
|
+RESULTS:
{ type: 0, children: [ { type: 5, content: [Object], loc: [Object] } ], } >>> 插值节点 { type: 5, content: { type: 4, isStatic: false, isConstant: false, content: 'foo.value', loc: { start: [Object], end: [Object], source: 'foo.value' } }, loc: { start: { column: 1, line: 1, offset: 0 }, end: { column: 16, line: 1, offset: 15 }, source: '{{ foo.value }}' } } undefined
397da38 add element parser
feat(add): parse element function · gcclll/stb-vue-next@397da38
解析元素标签的入口函数,实际详细解析在 parseTag()
函数中,所以这里需要结合
parseTag
的实现才能测试。
代码:
|
|
源码分析:
通过调用
parseTag()
解析出标签元素结构判断是不是自闭合标签(
<div/>
),或者外部定义的空标签(不需要结束标签的,如:<my-tag>
,为合法标签)调用
parseChildren()
递归解析该节点下子孙节点结束标签解析
<pre>
和v-pre
检测
3b96a74 add tag parser
feat(add): tag parser · gcclll/stb-vue-next@3b96a74
|
|
开始标签匹配正则:
/^<\/?([a-z][^\t\r\n\f />]*)/i
<pre>
标签处理v-pre
指令处理自闭合标签处理
组装元素结构
NodeTypes.ELEMENT
测试:
|
|
+RESULTS: 省略部分输出
>>> 普通标签 { type: 1, ns: 0, tag: 'div', tagType: 0, props: [], isSelfClosing: false, children: [], } >>> 自闭合标签 { type: 1, ns: 0, tag: 'img', tagType: 0, props: [], isSelfClosing: true, children: [], } >>> 自定义空标签 <mydiv> { type: 1, ns: 0, tag: 'mydiv', tagType: 0, props: [], isSelfClosing: false, children: [], }
bf28a36 add tag parser of tag type
feat(add): parse tag for tag type · gcclll/stb-vue-next@bf28a36
解析出标签的标签名(component
? template
? slot
? …)。
if (options.isNativeTag && !hasVIs)
!options.isNativeTag(tag)
如果不是原生标签,则视为COMPONENT
第二种为
COMPONENT
情况1 2 3 4 5 6 7
else if ( hasVIs || isCoreComponent(tag) || (options.isBuiltInComponent && options.isBuiltInComponent(tag)) || /^[A-Z].test(tag)/ || tag === 'component' )
有
v-is
指令isCoreComponent()
vue 内置标签(Teleport
,Suspense
,KeepAlive
,BaseTransition
)选项中自定义的
标签名首字母大写的也视为
component
标签名直接是
component
的
if (tag === 'slot')
插槽标签<template>
标签,且带有指令1 2 3 4 5 6
tag === 'template' && props.some(p => { return ( p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name) ) })
特殊的模板指令:
1 2 3
const isSpecialTemplateDirective = /*#__PURE__*/ makeMap( `if,else,else-if,for,slot` )
这个由于需要用到属性,所以需要结合 parseAttributes
实现才能进行测试。
73fd01f add attribute name and value parser
feat(add): attribute name and value parser · gcclll/stb-vue-next@73fd01f
这里新增了三个函数(代码较多,需要查看源码直接点击上面 commit 链接)
parseAttributes(context, type)
属性解析入口,通过 while 循环解析出所有属性parseAttribute(context, nameSet)
解析单个属性,属性名用 nameSet 集合存储避 免重复parseAttributeValue(context)
解析属性值
测试:
|
|
+RESULTS: 元素结构
{ type: 1, ns: 0, tag: 'div', tagType: 1, props: [...], // 如下 isSelfClosing: false, children: [], }
+RESULTS: 属性列表, 省略 loc 位置数据
>>> 静态属性:class { type: 6, name: 'class', value: { type: 2, content: 'app', } } >>> 动态属性静态属性名:staticPropName { type: 7, name: 'bind', exp: { type: 4, content: 'bar', isStatic: false, isConstant: false, }, arg: { type: 4, content: 'staticPropName', isStatic: true, isConstant: true, }, modifiers: [], } >>> 带修饰符的属性:press.enter { type: 7, name: 'on', exp: { type: 4, content: 'pressKey', isStatic: false, isConstant: false, }, arg: { type: 4, content: 'press', isStatic: true, isConstant: true, }, modifiers: [ 'enter' ], } >>> 动态属性名:dynamicPropName { type: 7, name: 'bind', exp: { type: 4, content: 'foo', isStatic: false, isConstant: false, }, arg: { type: 4, content: 'dynamicPropName', isStatic: false, isConstant: false, }, modifiers: [], }
e32401e add combine whitespace nodes
feat(add): combine whitespace node · gcclll/stb-vue-next@e32401e
合并删除空行或空字符串节点。
|
|
+RESULTS: 正确结果
{ type: 2, content: ' some text other text ', loc: { start: { column: 6, line: 2, offset: 6 }, end: { column: 1, line: 5, offset: 28 }, source: '\nsome text\nother text\n' } }
+RESULTS: 'sometextothertext' 空格都被删了? fix: all whitespce removed · gcclll/stb-vue-next@bb31509
{ type: 1, ns: 0, tag: 'div', tagType: 1, props: [], isSelfClosing: false, children: [ { type: 2, content: 'sometextothertext', loc: [Object] } ], loc: { start: { column: 1, line: 2, offset: 1 }, end: { column: 7, line: 5, offset: 34 }, source: '<div>\nsome text\nother text\n</div>' }, codegenNode: undefined } undefined
+RESULTS: children = []
? fix: no children · gcclll/stb-vue-next@66936f3
{ type: 1, ns: 0, tag: 'div', tagType: 1, props: [], isSelfClosing: false, children: [], loc: { start: { column: 1, line: 2, offset: 1 }, end: { column: 7, line: 5, offset: 34 }, source: '<div>\nsome text\nother text\n</div>' }, codegenNode: undefined } undefined
用例测试
<f12>
打开控制台有惊喜哦╰(°▽°)╯ 👀👀👀👀👀👀👀👀。
下面章节所有测试都是根据官方测试用例进行的:parse.spec.ts
|
|
更多测试内容和输出(由于篇幅问题)请查看 <F12>
打开控制台查看。
+RESULTS:
{ type: 2, content: 'some text', loc: { start: { column: 1, line: 1, offset: 0 }, end: { column: 10, line: 1, offset: 9 }, source: 'some text' } } undefined