Vue3追本溯源(四)template模版编译

接上篇双向数据绑定解析完setup方法并代理了数据,setupStatectx属性都设置了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.typeinstance.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赋值给originalSourcesource属性,这两个属性在后续解析模版时经常使用到,其次是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不是数组,则pushnodes数组中。

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, '@')判断dirnameon事件,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函数中,将解析完的属性对象attrpushprops数组中,继续调用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值,本例tagbutton,返回0(后续解析到别的标签时再做详述)。然后调用parseChildren方法解析子标签,这里就是复用parseChildren方法解析标签内部的子标签。本例中button标签内部的字元素为文本,最终调用parseText方法得到文本解析结果{ type: 2, content, loc: 位置信息 }。然后pushnodes数组中。之后回归到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、首先是type2的文本节点,通过正则表达式判断!/[^\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,本例中一个type5,一个type1,所以中间那个type2的对象的content属性赋值为' '(空格)
4、最后返回过滤后的nodes对象,nodes.filter(Boolean),那么为null的两个元素就被过滤了

至此parseChildren函数就解析结束了,以本例为模版解析到三个对象,分别是type5的动态数据解析结果、type2的换行符(content已替换为' ') 和 type1HTML标签元素。再回归到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函数就返回了一个对象type0childrenparseChildren方法返回的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属性数组中。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享