接上篇双向数据绑定解析完setup
方法并代理了数据,setupState
和 ctx
属性都设置了get、set
钩子函数后开始进行template
模版编译(以本例为模版进行解析)
本例
<div id='app'>
{{message}}
<button @click="modifyMessage">修改数据</button>
</div>
复制代码
template模版编译入口
上篇解析到setup
函数执行完成以及一些代理钩子设置之后,执行了finishComponentSetup
方法,一起来看下此函数的内部实现
export function finishComponentSetup(
instance: ComponentInternalInstance,
isSSR: boolean,
skipOptions?: boolean
) {
const Component = instance.type as ComponentOptions
// ...
if (__NODE_JS__ && isSSR) {}
else if (!instance.render) {
if (compile && !Component.render) {
const template = (__COMPAT__ && instance.vnode.props && instance.vnode.props['inline-template']) || Component.template
if (template) {
// ...
const { isCustomElement, compilerOptions } = instance.appContext.config
const { delimiters, compilerOptions: componentCompilerOptions } = Component
const finalCompilerOptions: CompilerOptions = extend(
extend(
{
isCustomElement,
delimiters
},
compilerOptions), componentCompilerOptions)
// ...
Component.render = compile(template, finalCompilerOptions)
// ...
}
}
// ...
}
// ... support for 2.x options
}
复制代码
首先看到Component = instance.type
,instance.type
实际上是初始的vnode
对象的type
属性,就是调用createApp
传入的参数,并且template
属性为root
根节点的内部HTML
字符串(在调用新app.mount
方法时赋值的)。判断render
属性是否不存在并且template
是否存在之后,调用compile
方法,第一个参数是template
模版字符串,第二参数是一些属性(后续解析过程中使用到再详细分析)。接下来分析下compile
函数具体做了什么?
// registerRuntimeCompiler方法定义
export function registerRuntimeCompiler(_compile: any) {
compile = _compile
}
// registerRuntimeCompiler方法调用位置
registerRuntimeCompiler(compileToFunction)
复制代码
可以看到初始化调用registerRuntimeCompiler
函数,将compileToFunction
方法赋值给了compile
,所以实际是执行了compileToFunction
函数。
function compileToFunction(
template: string | HTMLElement,
options?: CompilerOptions
): RenderFunction {
if (!isString(template)) {
// ...
}
const key = template
const cached = compileCache[key]
if (cached) {
return cached
}
if (template[0] === '#') { /* ... */ }
const { code } = compile(
template,
extend(
{
hoistStatic: true,
onError: __DEV__ ? onError : undefined,
onWarn: __DEV__ ? e => onError(e, true) : NOOP
} as CompilerOptions,
options
)
)
}
复制代码
compileToFunction
函数内部判断全局缓存对象cached
中是否已经存在相同模版的解析结果了,存在则直接返回,不存在则调用compile
方法。
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
})
)
}
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
// ...
const ast = isString(template) ? baseParse(template, options) : template
// ...
}
export function baseParse(
content: string,
options: ParserOptions = {}
): RootNode {
const context = createParserContext(content, options)
const start = getCursor(context)
return createRoot(
parseChildren(context, TextModes.DATA/* 0 */, []),
getSelection(context, start)
)
}
// TextModes 定义的一个枚举类型
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
}
复制代码
实际上compile
函数最终调用了baseParse
方法,首先调用createParserContext
函数创建一个context
对象
// context对象
const options = extend({}, defaultParserOptions)
for (key in rawOptions) {
// @ts-ignore
options[key] = rawOptions[key] === undefined ? defaultParserOptions[key] : rawOptions[key]
}
// ...
{
options,
column: 1,
line: 1,
offset: 0,
originalSource: content,
source: content,
inPre: false,
inVPre: false,
onWarn: options.onWarn
}
复制代码
将template
赋值给originalSource
和source
属性,这两个属性在后续解析模版时经常使用到,其次是column、line、offset
(列、行、偏移)这些用来标注模版位置信息。
调用
getCursor
方法就是返回位置信息对象{ column, line, offset }
最后调用createRoot
方法。而调用createRoot
方法的第一个参数是parseChildren
方法的返回值,这个函数就开始解析模版了。我们以本例为基础了解此函数的内部实现。
parseChildren解析template模版
function parseChildren(
context: ParserContext,
mode: TextModes, // TextModes.DATA 0
ancestors: ElementNode[] // [] 空数组
): TemplateChildNode[] {
const parent = last(ancestors)
const ns = parent ? parent.ns : Namespaces.HTML
const nodes: TemplateChildNode[] = []
while (!isEnd(context, mode, ancestors)) {
const s = context.source
let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined
if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
// ...
}
// ...
}
// ...
}
// 判断模版是否结束的isEnd方法
function isEnd(
context: ParserContext,
mode: TextModes,
ancestors: ElementNode[]
): boolean {
const s = context.source
switch (mode) {
case TextModes.DATA:
if (startsWith(s, '</')) {
// TODO: probably bad performance
for (let i = ancestors.length - 1; i >= 0; --i) {
if (startsWithEndTagOpen(s, ancestors[i].tag)) {
return true
}
}
}
break
// ...
}
}
复制代码
可以看到parseChildren
函数内部是通过一个while
循环,循环解析template
模版的。while
循环的判断条件是isEnd
方法的返回值,判断模版是否解析到结束位置了。当mode
为0时,isEnd
方法的具体流程:
1、
template
字符串开头是否是”</” [startsWith(s, '</')
]
2、”</” 后面的标签是否与最近一次解析的元素的标签是一致的
3、并且template
模版字符串在tag
标签后面的字符串是否是 “>”
注:2和3 是在startsWithEndTagOpen
方法中校验的
接着我们回到while循环内部,判断mode是0|1执行if语句(本例mode为0)
// 创建context对象时inVPre属性为false
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 (!node) {
node = parseText(context, mode)
}
if (isArray(node)) {
for (let i = 0; i < node.length; i++) {
pushNode(nodes, node[i])
}
} else {
pushNode(nodes, node)
}
复制代码
在createParserContext
函数中,options
对象由传入的rawOptions
对象 和 defaultParserOptions
对象做了合并,因此delimiters
为[{{
, }}
],因此while
内部执行流程
1、
startsWith(s, context.options.delimiters[0])
判断是否是”{{“开头(“{{“是Vue中引用变量的标识,所以是解析变量的分支)
2、s[0] === '<'
第一个字符是否是”<” (“<“是HTML
标签元素的开头部分,所以是解析元素的分支)
3、parseText(context, mode)
上面两个都不满足条件的话执行parseText
方法(所以是解析换行符、固定文本或者动态数据的分支)
parseText解析文本、换行符或者动态数据
依本例解析首先是换行符,看下parseText
方法的内部实现
function parseTextData(
context: ParserContext,
length: number,
mode: TextModes
): string {
const rawText = context.source.slice(0, length)
advanceBy(context, length)
if (
mode === TextModes.RAWTEXT ||
mode === TextModes.CDATA ||
rawText.indexOf('&') === -1
) {
return rawText
} else {
// ...
}
}
function parseText(context: ParserContext, mode: TextModes): TextNode {
// ...
const endTokens = ['<', 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: NodeTypes.TEXT, // 2 text文本
content,
loc: getSelection(context, start)
}
}
复制代码
parseText
方法内部indexOf
找到template
中最近的的”<” 或者 “{{” 的位置,然后调用parseTextData
方法参数为context
对象、最近的”<“或者”{{“的下标 和 mode
(0),parseTextData
方法先通过slice
方法,获取0 到 最近的”<” 或者”{{“之间的内容(可能是固定文本或者换行符),再调用advanceBy(context, length)
方法
function advanceBy(context: ParserContext, numberOfCharacters: number): void {
const { source } = context
__TEST__ && assert(numberOfCharacters <= source.length)
advancePositionWithMutation(context, source, numberOfCharacters)
context.source = source.slice(numberOfCharacters)
}
复制代码
这个方法的主要作用是通过slice
方法,获取从已经解析的内容位置(numberOfCharacters
),到template
字符串最后,重新赋值给context.source
(其实就是把解析完的字符串裁剪掉),还会调用advancePositionWithMutation
方法修改位置参数(其实是修改offset、line、column
的值,offset
加上numberOfCharacters
已知内容的长度、 如果遇到换行符line++
等等,这里不逐行解析这个方法)。所以parseTextData
返回的content
就是具体的文本内容或者换行符,parseText
返回的是一个type
为2的文本对象{ type:2, content: text内容|\n, loc: 内容起始结束为止信息 }
。在while
循环的最后,判断node
不是数组,则push
到nodes
数组中。
parseInterpolation解析动态参数
之后继续循环while
,解析{{message}}
时调用node = parseInterpolation(context, mode)
方法
function parseInterpolation(
context: ParserContext,
mode: TextModes
): InterpolationNode | undefined {
// open 是 {{ , close 是 }}
const [open, close] = context.options.delimiters
// ...
const closeIndex = context.source.indexOf(close, open.length)
if (closeIndex === -1) {
// ...
}
const start = getCursor(context)
advanceBy(context, open.length)
const innerStart = getCursor(context)
const innerEnd = getCursor(context)
const rawContentLength = closeIndex - open.length
const rawContent = context.source.slice(0, rawContentLength)
const preTrimContent = parseTextData(context, rawContentLength, mode)
const content = preTrimContent.trim() // 去除字符串首尾的空格
const startOffset = preTrimContent.indexOf(content)
if (startOffset > 0) {
advancePositionWithMutation(innerStart, rawContent, startOffset)
}
const endOffset =
rawContentLength - (preTrimContent.length - content.length - startOffset)
advancePositionWithMutation(innerEnd, rawContent, endOffset)
advanceBy(context, close.length)
return {
type: NodeTypes.INTERPOLATION, // type = 5 动态数据类型
content: {
type: NodeTypes.SIMPLE_EXPRESSION, // 4 js表达式
isStatic: false,
constType: ConstantTypes.NOT_CONSTANT, // 0
content,
loc: getSelection(context, innerStart, innerEnd)
},
loc: getSelection(context, start)
}
}
复制代码
parseInterpolation
方法内部大致实现流程:
1、
context.source.indexOf(close, open.length)
找到 “}}” (花括号结束符)的位置closeIndex
2、advanceBy(context, open.length)
template模版裁剪”{{” (开始符)
3、动态参数的长度等于结束的位置减去开始符号的长度rawContentLength = closeIndex - open.length
4、rawContent = context.source.slice(0, rawContentLength)
动态参数的内容通过slice
方法获取,从0到动态参数的长度rawContentLength
(context.source
已经去掉了”{{“)
5、通过parseTextData
方法获取动态参数名称,本例为message
6、然后查看content
前后是否有数据空格,去除空格计算位置信息,template
移除 “}}” 结束符
7、最后返回动态数据的解析对象{ type: 5, content: { type: 4, isStatic: false, content, ... } }
parseElement解析元素
继续执行while
循环,本例中{{message}}
之后又是一个换行符,继续调用parseText
方法解析(和第一个换行符一致,这里不赘述了)。之后解析button
元素,会执行s[0] === '<'
这个分支,看下这个判断内部是如何实现的
if (s.length === 1) {//...}
else if (s[1] === '!') {// 解析HTML注释代码 <!-- --> }
else if (s[1] === '/') {}
else if (/[a-z]/i.test(s[1])) {
// 解析元素开头 本例执行这个分支 <button
node = parseElement(context, ancestors)
}
// parseElement方法定义
function parseElement(
context: ParserContext,
ancestors: ElementNode[]
): ElementNode | undefined {
// ...
const wasInPre = context.inPre
const wasInVPre = context.inVPre
const parent = last(ancestors)
const element = parseTag(context, TagType.Start, parent)
const isPreBoundary = context.inPre && !wasInPre
const isVPreBoundary = context.inVPre && !wasInVPre
// ...
}
复制代码
parseTag解析Tag标签
当解析元素时,调用parseElement
方法。此方法内部先调用parseTag
方法解析tag标签
function parseTag(
context: ParserContext,
type: TagType,
parent: ElementNode | undefined
): ElementNode | undefined {
// ...
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)
const cursor = getCursor(context)
const currentSource = context.source
// ...
let props = parseAttributes(context, type)
// ...
}
复制代码
parseTag
方法解析HTML标签的步骤
1、通过正则表达式
/^<\/?([a-z][^\t\r\n\f />]*)/i
解析标签名称(推荐个正则图形解析地址: jex.im/regulex/#!f…)
2、调用context.options.getNamespace
方法,此方法的作用大致是判断parent
父节点是否存在,不存在时tag
标签是否为svg
或者math
标签。本例为button
标签
3、之后tempalte
模版裁剪解析完的”<button”字符串,再调用parseAttributes
方法解析元素的内联属性
function parseAttributes(
context: ParserContext,
type: TagType
): (AttributeNode | DirectiveNode)[] {
const props = []
const attributeNames = new Set<string>()
while (
context.source.length > 0 &&
!startsWith(context.source, '>') &&
!startsWith(context.source, '/>')
) {
// ...
const attr = parseAttribute(context, attributeNames)
if (type === TagType.Start) {
props.push(attr)
}
// ...
advanceSpaces(context)
}
return props
}
复制代码
parseAttributes 循环解析元素中的属性
parseAttributes
方法内部也是通过while
循环解析属性,判断条件是template
模版字符串是否是以”>” 或者 “/>”开头(标签是否解析到结束符了),while
循环内部通过parseAttribute
方法解析元素的属性
parseAttribute 解析单个属性的名称和值
function parseAttribute(
context: ParserContext,
nameSet: Set<string>
): AttributeNode | DirectiveNode {
// ...
const start = getCursor(context)
const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)!
const name = match[0]
// ...
advanceBy(context, name.length)
if (/^[\t\r\n\f ]*=/.test(context.source)) {
advanceSpaces(context)
advanceBy(context, 1)
advanceSpaces(context)
value = parseAttributeValue(context)
// ...
}
const loc = getSelection(context, start)
if (!context.inVPre && /^(v-|:|\.|@|#)/.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: ExpressionNode | undefined
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('[')) { /* ... */ }
else if (isSlot) { // 本例插槽指令,省略代码 }
arg = {
type: NodeTypes.SIMPLE_EXPRESSION, // 4 简单表达式
content,
isStatic,
constType: isStatic
? ConstantTypes.CAN_STRINGIFY // 3
: ConstantTypes.NOT_CONSTANT, // 0
loc
}
}
if (value && value.isQuoted) {
const valueLoc = value.loc
valueLoc.start.offset++
valueLoc.start.column++
valueLoc.end = advancePositionWithClone(valueLoc.start, value.content)
valueLoc.source = valueLoc.source.slice(1, -1)
}
const modifiers = match[3] ? match[3].substr(1).split('.') : []
if (isPropShorthand) modifiers.push('prop')
// ...
return {
type: NodeTypes.DIRECTIVE, // 7
name: dirName,
exp: value && {
type: NodeTypes.SIMPLE_EXPRESSION,
content: value.content,
isStatic: false,
// Treat as non-constant by default. This can be potentially set to
// other values by `transformExpression` to make it eligible for hoisting.
constType: ConstantTypes.NOT_CONSTANT,
loc: value.loc
},
arg,
modifiers,
loc
}
}
}
复制代码
parseAttribute
方法解析属性流程
1、通过正则表达式
/^[^\t\r\n\f />][^\t\r\n\f />=]*/
解析出key=value
的结构,本例为@click="modifyMessage"
,所以属性名称为@click
2、通过正则表达式/^[\t\r\n\f ]*=/
判断剩余template字符串是否是”=value”的结构,本例为=”modifyMessage”,调用parseAttributeValue
方法解析属性值,后续我们再看这个方法内部具体是如何解析属性值的
3、通过正则表达式/^(v-|:|\.|@|#)/
属性名称是v-(指令)、:(数据)、@(事件)中的一种,本例为@click,然后再通过正则表达式/(?:^v-([a-z0-9-]+))??:(?::|^\.|^@|^#)(\[[^\]]+\]|[^\.]+))?(.+)?$/i
解析出具体的指令、数据或者事件的名称。再根据startsWith(name, '@')
判断dirname
是on
事件,content=match[2]
为click
4、最后生成arg
对象用来表示属性名称{ type:4, content: 属性名称, isStatic: true, ... }
parseAttributeValue 解析属性值
下面先回头来看下parseAttributeValue
方法中具体是如何解析属性值的
function parseAttributeValue(context: ParserContext): AttributeValue {
const start = getCursor(context)
let content: string
const quote = context.source[0]
const isQuoted = quote === `"` || quote === `'`
if (isQuoted) {
advanceBy(context, 1)
const endIndex = context.source.indexOf(quote)
if (endIndex === -1) {}
else {
content = parseTextData(context, endIndex, TextModes.ATTRIBUTE_VALUE)
advanceBy(context, 1)
}
} else { /* ... */ }
return { content, isQuoted, loc: getSelection(context, start) }
}
复制代码
1、
parseAttributeValue
方法中首先判断第一个字符是否是" | '
(双引号或者单引号),本例中解析的template
字符串应该是"modifyMessage"
,然后去掉第一个" | '
,indexOf找到第二个的位置并记录下标endIndex
2、调用parseTextData
函数解析具体的属性值名称,这个方法上面已经解析过了,利用slice
获取到endIndex
长度的内容,本例为modifyMessage
,换言之click
的方法名为modifyMessage
3、最后返回一个对象表示属性值{ content, isQuoted, loc: 位置信息 }
之后再回归到parseAttribute
方法中,属性名和属性值都解析完之后,返回一个属性的解析结果对象
{ type: 7, name: 'on', exp: 属性值的相关信息, arg: 属性名的相关信息, ... }
。再回归到parseAttributes
函数中,将解析完的属性对象attr
,push
到props
数组中,继续调用while
循环解析下一个属性直到元素结束符为止。最后返回props
属性数组。(本例只有一个属性click
)。再回归到parseTag方法中,看解析完属性之后又实现了什么
// parseTag 方法后续
let props = parseAttributes(context, type)
// 检查是否有v-pre指令 ...
let isSelfClosing = false
if (context.source.length === 0) {
emitError(context, ErrorCodes.EOF_IN_TAG)
} else {
isSelfClosing = startsWith(context.source, '/>')
if (type === TagType.End && isSelfClosing) {
emitError(context, ErrorCodes.END_TAG_WITH_TRAILING_SOLIDUS)
}
advanceBy(context, isSelfClosing ? 2 : 1)
}
// ...
// 判断标签是否是slot或者template
return {
type: NodeTypes.ELEMENT, // type:1 解析的元素对象
ns,
tag,
tagType,
props,
isSelfClosing,
children: [],
loc: getSelection(context, start),
codegenNode: undefined // to be created during transform phase
}
复制代码
可以看到parseTag
方法再解析完属性之后,去除了元素的结束标签(">"
或者 "/>"
),最后返回了一个对象表示元素的解析结果 { type: 1, tag: button, props:[{...}], children: [], loc: 位置信息, codegenNode: undefined, isSelfClosing: false(是否是自闭合标签) }
。之后回归到parseElement
方法中,看解析完标签前半部分后又实现了什么
// parseElement方法后续实现
if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
// 判断是否是自闭合标签或者空标签(类似于br、hr标签,本例不符合暂不解析)
}
// 开始解析标签内部的子元素
ancestors.push(element)
const mode = context.options.getTextMode(element, parent)
const children = parseChildren(context, mode, ancestors)
ancestors.pop()
// ...
复制代码
parseElement
方法后续先判断是否是自闭合标签或者空标签,否则开始解析标签内部的子元素。先调用
context.options.getTextMode
方法获取mode
值,本例tag
为button
,返回0(后续解析到别的标签时再做详述)。然后调用parseChildren
方法解析子标签,这里就是复用parseChildren
方法解析标签内部的子标签。本例中button
标签内部的字元素为文本,最终调用parseText
方法得到文本解析结果{ type: 2, content, loc: 位置信息 }
。然后push
到nodes
数组中。之后回归到parseElement方法中
在
parseChildren
函数的while
循环解析结束后,会执行一段代码,主要是过滤一下生成的node
对象(因为本例中子节点比较简单,所以这部分的解析放到后面)。
// parseElement解析完子标签的后续
element.children = children
if (startsWithEndTagOpen(context.source, element.tag)) {
parseTag(context, TagType.End, parent)
} else { /*...*/ }
element.loc = getSelection(context, element.loc.start)
return element
复制代码
当解析完标签内部的子元素时,将它赋值给element
对象的children
属性,然后处理结束标签</button>
,计算整个标签元素的位置信息,返回element
对象。此时parseChildren
方法的while
循环已经结束了,因为所有的标签都解析完成了。来看下nodes
数组中有哪些对象:
1、
type=2, content=\n
(换行符)的文本标签
2、type=5, content={ type:4, content: message }
的简单js
表达式对象(动态数据)
3、type=2, content=\n
(换行符)的文本标签
4、type=1
的元素标签对象{ type:1, props: [{...}], children: [...], ... }
5、type=2, content=\n
(换行符)的文本标签\
模版解析的while
循环结束后,继续解析parseChildren
方法中的后续代码,是如何过滤并修改nodes
数组中的元素对象
// parseChildren中的后续代码
let removedWhitespace = false
if (mode !== TextModes.RAWTEXT && mode !== TextModes.RCDATA) {
const shouldCondense = context.options.whitespace !== 'preserve'
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (!context.inPre && node.type === NodeTypes.TEXT/* 2 */) {
if (!/[^\t\r\n\f ]/.test(node.content)) {
const prev = nodes[i - 1]
const next = nodes[i + 1]
if (
!prev ||
!next ||
(shouldCondense &&
(prev.type === NodeTypes.COMMENT ||
next.type === NodeTypes.COMMENT ||
(prev.type === NodeTypes.ELEMENT &&
next.type === NodeTypes.ELEMENT &&
/[\r\n]/.test(node.content))))
) {
removedWhitespace = true
nodes[i] = null as any
} else {
node.content = ' '
}
}
else if (shouldCondense) {/* ... */}
}
else if (node.type === NodeTypes.COMMENT && !context.options.comments) {}
}
if (context.inPre && parent && context.options.isPreTag(parent.tag)) {}
}
return removedWhitespace ? nodes.filter(Boolean) : nodes
复制代码
1、首先是
type
为2
的文本节点,通过正则表达式判断!/[^\t\r\n\f ]/.test(node.content)
是这些特殊符号中的一个
2、然后获取这个node
相邻的上下两个对象,如果它相邻的上一个对象 后者 下一个对象不存在,则nodes[i] = null as any
,这个对象直接赋值null
,所以nodes
中的第一个 和 最后一个 元素变成了null
3、如果相邻的两个元素存在,判断相邻的上下两个对象的type
值是不是有一个为NodeTypes.COMMENT(3)
或者 都为NodeTypes.ELEMEN(1)
并且content
属性值包含\r\n
,本例中一个type
为5
,一个type
为1
,所以中间那个type
为2
的对象的content
属性赋值为' '
(空格)
4、最后返回过滤后的nodes
对象,nodes.filter(Boolean)
,那么为null
的两个元素就被过滤了
至此parseChildren
函数就解析结束了,以本例为模版解析到三个对象,分别是type
为5
的动态数据解析结果、type
为2
的换行符(content
已替换为' '
) 和 type
为1
的HTML
标签元素。再回归到baseParse
方法中,执行createRoot
方法,参数是解析完的模版对象数组 和 位置信息
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
}
}
复制代码
最终createRoot
函数就返回了一个对象type
为0
,children
为parseChildren
方法返回的nodes
数组,这个对象就作为根节点对象。baseParse
方法返回的就是根节点对象。再回归到baseCompile
方法中,当ast
对象生成完成之后,后续会调用transform
方法对ast
对象进行一些转换,再调用generate
方法生成render
函数。
总结
上篇结尾解析到的finishComponentSetup
函数内部,调用compile
方法进行模版编译,实际是调用baseCompile
方法进行模版的解析
、转化
、最终生成render函数
。本文主要是从解析开始,也就是createRoot
方法生成根节点,此方法的参数调用parseChildren
方法真正解析模版字符串。
1、parseChildren方法主要是通过
while
循环边解析边裁剪,直到全部解析完成
2、通过正则表达式对解析的字符串进行分类,主要是HTML标签、动态数据、静态文本三类
3、HTML标签中的子元素会继续调用parseChildren
方法进行深度解析,之后赋值给children
属性
4、HTML标签中属性也会通过while循环(正则表达式判断)解析所有的属性(指令、数据、事件),最后生成属性对象,再push到props属性数组中。