- Vue3源码01 : 代码管理策略-monorepo
- Vue3源码02: 项目构建流程和源码调试方法
- Vue3源码03: Vue3响应式核心原理
- Vue3源码04: Vue3响应式系统源码实现1/2
- Vue3源码05 : Vue3响应式系统源码实现(2/2)
- Vue3源码06: reactive、ref相关api源码实现
- Vue3源码07: 故事要从createApp讲起
- Vue3源码08: 虚拟Node到真实Node的路其实很长
- Vue3源码09: 组件的渲染和更新流程
- Vue3源码10: 名动江湖的diff算法
- Vue3源码11: 编译优化之Block Tree 与 PatchFlags
本文先会分析Vue3
的编译过程主要包含哪些环节,理解了主体环节后,再带着大家深入分析AST
的生成过程。
编译过程
我们进入core/packages/compiler-dom/src/index.ts
,会发现有这样的代码:
// 代码片段1
export function compile(
template: string,
options: CompilerOptions = {}
): CodegenResult {
return baseCompile(
template,
extend({}, parserOptions, options, {
nodeTransforms: [
// ignore <script> and <tag>
// this is not put inside DOMNodeTransforms because that list is used
// by compiler-ssr to generate vnode fallback branches
ignoreSideEffectTags,
...DOMNodeTransforms,
...(options.nodeTransforms || [])
],
directiveTransforms: extend(
{},
DOMDirectiveTransforms,
options.directiveTransforms || {}
),
transformHoist: __BROWSER__ ? null : stringifyStatic
})
)
}
函数compile
的作用就是把模版字符串转化成一个render
函数。而函数compile
函数内部仅仅是调用了函数baseCompile
,baseCompile
函数是从compiler-core
导入的,也就是关于编译相关的功能主要是在compiler-core
中完成的。而compiler-dom
主要是向函数baseCompiler
传入了一系列的参数,这些参数代表什么含义我们在后文会在恰当的地方解释。
我们来看一下compiler-core
中函数baseCompile
的代码:
// 代码片段2
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
// 此处省略许多代码...
const ast = isString(template) ? baseParse(template, options) : template
// 此处省略许多代码...
transform(
ast,
extend({}, options, {
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: extend(
{},
directiveTransforms,
options.directiveTransforms || {} // user transforms
)
})
)
return generate(
ast,
extend({}, options, {
prefixIdentifiers
})
)
}
我们对函数baseCompile
进行精简了之后,做了三件重要的事情,调用baseParse
函数返回ast
,接着调用transform
对ast
进行处理,最后调用generate
函数返回结果,其实这三个函数完成了下面三项工作:
- 将模版字符串转化成
AST
; - 将
AST
转化成可以用来描述JavaScript
的AST
; - 根据第2步生成的可以描述
JavaScript
的AST
生成一个函数。
上面三项工作,每一项都涉及到大量代码,本文只分析Vue3是如何将模版字符串转化成AST
的。
AST的生成
什么是AST
为了直观的体会AST
是什么,我们在这个网址上https://vue-next-template-explorer.netlify.app/输入下面的代码:
<!--代码片段3-->
<div>
<div>yangyitao</div>
</div>
此时查看控制台输出的AST
,我们提取其中的部分内容显示在这里:
// 代码片段4
{
children: [{type: 2, content: 'yangyitao'}],
isSelfClosing: false,
loc: {start: {…}, end: {…}, source: '<div>yangyitao</div>'},
tag: "div",
tagType: 0,
type: 1
}
代码片段4中没有对每一项都展开,但是我们依然可以直观的看到,所谓的AST
其实就是一个对象,该对象可以用来描述我们传入的模版字符串。下面我们就进入baseParse
函数,分析模版字符串转化为render
函数的具体过程。
baseParse函数
我们来看函数baseParse
的代码实现:
// 代码片段5
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)
)
}
发现函数baseParse
内部主要通过createParserContext
、getCursor
、createRoot
、parseChildren
、getSelection
等5个函数来完成工作。接下来我们就进入到这5个函数中去一探究竟。
createParserContext
代码语言:javascript复制// 代码片段6
function createParserContext(
content: string,
rawOptions: ParserOptions
): ParserContext {
const options = extend({}, defaultParserOptions)
let key: keyof ParserOptions
for (key in rawOptions) {
// @ts-ignore
options[key] =
rawOptions[key] === undefined
? defaultParserOptions[key]
: rawOptions[key]
}
return {
options,
column: 1,
line: 1,
offset: 0,
originalSource: content,
source: content,
inPre: false,
inVPre: false,
onWarn: options.onWarn
}
}
对于函数createParserContext
,我们先忽略对rawOptions
参数的处理,会发现该函数返回了一个对象。该对象记录了很多信息,我们这里主要关注5个属性:column
、line
、offset
、originalSource
、source
。这5个属性具体代表什么含义呢?假设我们有这样的代码片段:
<!--代码片段7-->
<script src="./compiler-dom.global.js"></script>
<script>
const { compile } = VueCompilerDOM;
let ast = compile('<div>yangyitao</div>')
</script>
我们查看函数createParserContext
的首次调用的返回值,省略一些内容后有下面的信息:
// 代码片段8
{
column: 1,
line: 1,
offset: 0,
originalSource: "<div>yangyitao</div>",
source: "<div>yangyitao</div>"
}
接下来我们解释下面5个属性的含义。
column
:所谓列,是对模版字符串指解析到哪一列了,举个例子<div>yangyitao</div>
,如果程序处理完开始标签<div>
,那么此时相当于程序已经解析到第6列,需要注意的是这里的列是相对于行的位置,比如代码解析到了第二行的第一个字符,那列的值依然是1;line
:代码片段7中我们只有一行代码,所以这个值始终是1;offset
:与column
不同,偏移量offset
是相对于我们要解析的整个模版字符串的位置。举个例子,如果我们要解析下面的模版字符串:
<div>
yangyitao</div>
当解析完第一个标签<div>
,column
是1,但偏移量是5。需要注意的是偏移量是从0开始计数,而column
和line
是从1开始计数;
originalSource
:代表整个待解析的模版字符串,对应代码片段7的<div>yangyitao</div>
;source
:代表尚未解析模版字符串,比如代码片段7中的模版字符串<div>yangyitao</div>
如果将开始标签<div>
解析完毕,那么source
的值就应该是yangyitao</div>
;
我们想一想,为什么要有这样一个上下文对象呢?所谓上下文对象,实际上就是维护了一个对象,这个对象记录了当前对模版字符串进行解析的状态,比如解析到什么地方了,还剩多少内容没有处理,同时还记录了当前处理节点的类型等等。代码片段6中我们刚才忽略了参数rawOptions
,我们来看看这个rawOptions
可能包括哪些内容:
// 代码片段9
export const defaultParserOptions: MergedParserOptions = {
delimiters: [`{{`, `}}`],
getNamespace: () => Namespaces.HTML,
getTextMode: () => TextModes.DATA,
isVoidTag: NO,
isPreTag: NO,
isCustomElement: NO,
decodeEntities: (rawText: string): string =>
rawText.replace(decodeRE, (_, p1) => decodeMap[p1]),
onError: defaultOnError,
onWarn: defaultOnWarn,
comments: __DEV__
}
从代码片段9中可以看出,我们的上下文不仅维护了状态还具备一些能力,通过这些能力可以获取当前操作节点的类型等等,至于什么是TextModes
、Namespaces
我们在后续用到的地方再讲解。
getCursor
代码语言:javascript复制// 代码片段10
function getCursor(context: ParserContext): Position {
const { column, line, offset } = context
return { column, line, offset }
}
该函数逻辑非常简单,只是从上下文中获取了几个属性,这几个属性能反映出当前对模版字符串解析到什么位置了。
getSelection
代码语言:javascript复制// 代码片段11
function getSelection(
context: ParserContext,
start: Position,
end?: Position
): SourceLocation {
end = end || getCursor(context)
return {
start,
end,
source: context.originalSource.slice(start.offset, end.offset)
}
}
该函数返回的是一个对象,这个对象代表了一个完整节点的代码内容以及这些内容在整个模版字符串中的开始位置和结束位置。至于开始位置和结束位置是通过上文介绍过的列、行、偏移量来进行描述。
createRoot
代码语言:javascript复制// 代码片段12
export function createRoot(
children: TemplateChildNode[],
loc = locStub
): RootNode {
return {
type: NodeTypes.ROOT,
children,
helpers: [],
components: [],
directives: [],
hoists: [],
imports: [],
cached: 0,
temps: 0,
codegenNode: undefined,
loc
}
}
该函数返回的其实就是一个我们所说的AST
,AST
是什么,AST
就是一个对象,就是一个用来描述模版字符串的对象。需要注意的是,根节点有一个特殊的类型来标识NodeTypes.ROOT
。
parseChildren
函数parseChildren
可以说是整个解析模版字符串功能的灵魂,鉴于代码量太大,我们来看精简过后的代码:
// 代码片段13
function parseChildren(
context: ParserContext,
mode: TextModes,
ancestors: ElementNode[]
): TemplateChildNode[] {
const parent = last(ancestors)
const nodes: TemplateChildNode[] = []
while (!isEnd(context, mode, ancestors)) {
const s = context.source
if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
if (startsWith(s, context.options.delimiters[0])) {
node = parseInterpolation(context, mode)
} else if (mode === TextModes.DATA && s[0] === '<') {
if (s[1] === '!') {
if (startsWith(s, '<!--')) {
node = parseComment(context)
} else if (startsWith(s, '<!DOCTYPE')) {
node = parseBogusComment(context)
} else if (startsWith(s, '<![CDATA[')) {
if (ns !== Namespaces.HTML) {
node = parseCDATA(context, ancestors)
}
}
} else if (/[a-z]/i.test(s[1])) {
node = parseElement(context, ancestors)
}
}
}
if (!node) {
node = parseText(context, mode)
}
if (isArray(node)) {
for (let i = 0; i < node.length; i ) {
pushNode(nodes, node[i])
}
} else {
pushNode(nodes, node)
}
}
return nodes
}
该函数原本还有很多其他比较重要的逻辑,但核心的逻辑就如代码片段13所描述的那样。这段代码该如何去理解呢?里面有两个关键的变量,一个是parent
用来描述解析的子节点归属于哪个父节点,另一个变量是nodes
表示解析得到的子节点的数组。同时,代码片段13中还调用了几个关键的函数parseElement
、parseCDATA
、parseText
、parseComment
、parseBogusComment
、parseInterpolation
。
在解释这几个函数具体是如何实现的之前,我们需要搞清楚TextModes
具备什么作用,才能明白为什么需要这几个函数来分工协作。
TextModes
我们先来观察TextModes
的代码:
// 代码片段14
export const enum TextModes {
// | Elements | Entities | End sign | Inside of
DATA, // | ✔ | ✔ | End tags of ancestors |
RCDATA, // | ✘ | ✔ | End tag of the parent | <textarea>
RAWTEXT, // | ✘ | ✘ | End tag of the parent | <style>,<script>
CDATA,
ATTRIBUTE_VALUE
}
Vue3通过TextModes
来区分,当前处理的模版字符串内容属于什么类型。普通标签用DATA
来表示,textarea
中的文本用RCDATA
来表示,模版字符串原则上不会有<style>
、<script>
标签存在的,所以对这类型的字符串不做任何处理。RCDATA
相较于DATA
,有一个不同点,DATA
类型的处理方式会对元素标签进行正常解析,但是RCDATA
不会解析标签,但这二者都会解析实体字符串。而至于CDATA
也是对其中的内容不做解析。而ATTRIBUTE_VALUE
则主要用来标识标签的属性部分内容。
其实可以理解为,对于模版字符串的不同的内容类型,采取不同的策略来进行解析。而这些策略,就是我们上面提到过的parseElement
、parseCDATA
、parseText
、parseComment
、parseBogusComment
、parseInterpolation
等函数。虽然对不同类型的数据具体处理各不相同,但是解析的方式主要逻辑是很相似的。所以本文不打算对每一个函数进行详细分析,而只是对极具代表性的函数parseElement
进行讲解。
parseElement
代码语言:javascript复制// 代码片段14
function parseElement(
context: ParserContext,
ancestors: ElementNode[]
): ElementNode | undefined {
// 此处省略很多代码...
const parent = last(ancestors)
const element = parseTag(context, TagType.Start, parent)
// 此处省略很多代码...
ancestors.push(element)
const mode = context.options.getTextMode(element, parent)
const children = parseChildren(context, mode, ancestors)
ancestors.pop()
// 此处省略很多代码...
element.children = children
// 此处省略很多代码...
return element
}
省略大量代码留下核心逻辑,里面有有三个点值得关注:
- 利用
ancestors
维护当前节点的所有父节点; - 通过
parseChildren
获取子节点的内容; - 将获取的子节点内容赋值给当前节点
element.children = children
; - 利用
parseTag
解析节点本身的内容。
上面后三个点中的后两个点其实概括了parseElement
的核心工作。而变量accestors
目前来看主要和命名空间ns
相关,但是ns
在目前项目中主要是枚举值HTML
,所以其作用比较微弱,拿出来讲一讲是为了防止大家疑惑,唯一值得注意的一个点是accestors
是一个栈,从代码片段14中可以看出,在解析子节点之前先push
该节点,紧接着解析完子节点后再pop
出当前节点,这样就保证了解析的子节点都能获取到自己正确的父节点。
第2个关键点,相当于递归执行代码片段13。而第3个关键点则是直接将解析到的结果赋值给当前节点,第4个关键点是解析标签本身,下面开始分析parseTag
函数。
parseTag
请看parseTag
函数的代码实现:
// 代码片段15
function parseTag(
context: ParserContext,
type: TagType,
parent: ElementNode | undefined
): ElementNode | undefined {
// Tag open.
const start = getCursor(context)
const match = /^</?([a-z][^trnf />]*)/i.exec(context.source)!
const tag = match[1]
const ns = context.options.getNamespace(tag, parent)
advanceBy(context, match[0].length)
advanceSpaces(context)
// save current state in case we need to re-parse attributes with v-pre
const cursor = getCursor(context)
const currentSource = context.source
// Attributes.
let props = parseAttributes(context, type)
advanceBy(context, isSelfClosing ? 2 : 1
let tagType = ElementTypes.ELEMENT
if (!context.inVPre) {
if (tag === 'slot') {
tagType = ElementTypes.SLOT
} else if (tag === 'template') {
if (
props.some(
p =>
p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name)
)
) {
tagType = ElementTypes.TEMPLATE
}
} else if (isComponent(tag, props, context)) {
tagType = ElementTypes.COMPONENT
}
}
return {
type: NodeTypes.ELEMENT,
ns,
tag,
tagType,
props,
isSelfClosing,
children: [],
loc: getSelection(context, start),
codegenNode: undefined // to be created during transform phase
}
}
省略了很多代码后,我们能比较容易得出下面几个结论:
- 函数
parseTag
返回的是一个对象; - 函数
parseTag
返回的对象中包括一个重要的属性props
; - 函数
parseTag
返回的对象中包括一个重要的属性type
;
其实可以简单理解该函数返回了一个对象,该对象描述了一个html
标签,比如下面的模版代码:<div id='app'>yangyitao</div>
中<div id='app'>
就是parseTag
返回的对象所需要描述的东西。至于解析属性的函数parseAttributes
实现逻辑比较简单代码量也不多,大家可以自己进行分析。
小结
本文从compiler-dom
中的compile
函数讲起,分析了模版字符串解析成AST
的核心流程,希望大家能在阅读文章后多进行调试,深入掌握AST
的生成过程。