Vue3 的模板解析器

1,191 阅读8分钟

源码版本:3.2.20

文件位置:compiler-core/src/parse.ts

贯穿整个解析流程的对象

解析上下文对象,保存着当前的解析进度、解析配置项和源码字符串等信息,下文使用 context 表示。类型定义如下:

export interface ParserContext {
  options: MergedParserOptions /* 解析配置对象 */
  readonly originalSource: string /* 模板字符串源码 */
  source: string /* 剩余待解析的模板字符串 */

  /* cursor */
  offset: number /* 当前解析位置,相对于源码字符串 */
  line: number /* 当前解析位置, 映射源码的行数 */
  column: number /* 当前解析位置,映射源码的列数 */

  inPre: boolean /* 当前处于 <pre> 元素内 */
  inVPre: boolean /* 当前元素使用 v-pre 指令 */
  onWarn: NonNullable<ErrorHandlingOptions['onWarn']>
}

创建 context

export const NO = () => false

// 默认解析配置对象
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__
}

// 创建解析上下文对象
function createParserContext(
  content: string,
  rawOptions: ParserOptions
): ParserContext {
  const options = extend({}, defaultParserOptions)

  let key: keyof ParserOptions
  // 自定义配置覆盖默认配置
  for (key in rawOptions) {
    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
  }
}

Vue 提供了默认的解析配置对象,使用者也可以根据自己的需求自定义解析时的配置。返回的 context 对象中的 originalSource 为可读属性,用于解析的是 source 属性

解析的入口函数

parse.ts 导出的函数对象只有一个,baseParse 接收源码字符串和自定义的解析配置对象,返回解析生成的抽象语法树:

export function baseParse(
  content: string,
  options: ParserOptions = {}
): RootNode {
  // 创建解析上下文对象,合并解析配置
  const context = createParserContext(content, options)

  // 返回根节点
  return createRoot(/* 创建根节点 */
    parseChildren(context, TextModes.DATA, []),/* 解析子节点 */
    getSelection(context, start)
  )
}

parseChildren 生成的子节点赋值于根节点的 children 属性

解析子节点

子节点会以深度优先遍历的形式进行解析,详情可查看源码:

function parseChildren(
  context: ParserContext,
  mode: TextModes,
  ancestors: ElementNode[]
): TemplateChildNode[] {
  // 当前等解析节点的父节点
  const parent = last(ancestors)

  // 返回的子节点
  const nodes: TemplateChildNode[] = []

  // isEnd 判断 `context.source` 的起始字符串是否是结束字符串
  // 例如 '</',还会匹配 ancestors 的标签名
  while (!isEnd(context, mode, ancestors)) {
    // 待解析字符串
    const s = context.source

    // 当前解析的节点值
    let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined

      // 判断当前起始字符是否是插值的开始字符
      if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {
        // 插值节点
        node = parseInterpolation(context, mode)
      } else if (mode === TextModes.DATA && s[0] === '<') {
        // 根绝 HTML 规范处理:https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
        if (s.length === 1) {
          /* error */
        }
        /* 忽略其他情况 */
        else if (/[a-z]/i.test(s[1])) {
          // 元素标签,解析元素
          node = parseElement(context, ancestors)
        }/* 错误校验 */
      }

    // 当前节点既不是元素,也不是插值
    if (!node) {/* 普通文本 */
      node = parseText(context, mode)
    }

    /* 将 node 添加到 nodes 数组。如果 node 是数组的情况会下,会遍历进行添加 */
  }

  /* Whitespace handling strategy like v2 */

  return removedWhitespace ? nodes.filter(Boolean) : nodes
}

parseChildren 函数会解析起始字符串的 tag 是否与父节点的标签名(TextModes.DATA 模式下)相同,用于判断当前的所有子节点解析完成;解析完后将节点添加到 nodes;最后根据策略是否过滤空的节点

解析文本信息

parseText 用于解析普通文本节点:

function parseText(context: ParserContext, mode: TextModes): TextNode {
  // 表示文本结束的 token
  const endTokens =
    mode === TextModes.CDATA ? [']]>'] : ['<', context.options.delimiters[0]]

  // 假设待解析字符串都是文本,不存在标签,所以索引为整个字符串长度
  let endIndex = context.source.length

  // 例如:someText{{...}}</tag> 或者 someText</tag>{{val}}
  for (let i = 0; i < endTokens.length; i++) {
    // 查找最先遇到的结束 token 的位置
    const index = context.source.indexOf(endTokens[i], 1)

    // 当前文本正确的结束下标
    if (index !== -1 && endIndex > index) {
      endIndex = index
    }
  }

  // 解析文本内容
  const content = parseTextData(context, endIndex, mode)

  return {
    type: NodeTypes.TEXT,
    content,
    loc: getSelection(context, start)
  }
}

// 不仅用于 parseText,涉及文本都涉及此函数
// 解析文本的时候同时步进 context.source
function parseTextData(
  context: ParserContext,
  length: number,
  mode: TextModes
): string {
  // 获取文本
  const rawText = context.source.slice(0, length)

  /* 步进 context.source,移除文本内容 */

  if (
    mode === TextModes.RAWTEXT ||
    mode === TextModes.CDATA ||
    rawText.indexOf('&') === -1
  ) {
    return rawText
  } else {
    // DATA or RCDATA containing "&"". Entity decoding required.
    // 例如 %gt 转译为 >
    return context.options.decodeEntities(
      rawText,
      mode === TextModes.ATTRIBUTE_VALUE
    )
  }
}

parseText 会根据 "分割字符" 提取纯文本作为单独的一个节点

解析插值

当起始字符串与 context.options.delimiters[0] 匹配时,说明后续内容可能是插值类型的节点,会调用此函数生成节点对象:

function parseInterpolation(
  context: ParserContext,
  mode: TextModes
): InterpolationNode | undefined {
  // 获取插值的 开始 和 结束 的字符串。默认 ‘{{’ 和 ‘}}’
  const [open, close] = context.options.delimiters

  // 结束字符串最近的索引
  const closeIndex = context.source.indexOf(close, open.length)

  /* 步进 context.source,移除 open 字符 */

  // 插值内容节点的 开始 与 结束 位置
  const innerStart = getCursor(context)
  const innerEnd = getCursor(context)

  // 插值内容节点的长度
  const rawContentLength = closeIndex - open.length

  // 解析文本内容
  const preTrimContent = parseTextData(context, rawContentLength, mode)

  /* 步进 context.source,移除 close 字符 */

  // 返回插值节点
  return {
    type: NodeTypes.INTERPOLATION,
    content: {
      type: NodeTypes.SIMPLE_EXPRESSION,
      isStatic: false,
      constType: ConstantTypes.NOT_CONSTANT,
      content,
      loc: getSelection(context, innerStart, innerEnd)
    },
    loc: getSelection(context, start)
  }
}

//demo
baseParse('{{  x  }}')
/* 生成的节点对象(省略其他节点)
{
  type: 5, // NodeTypes.INTERPOLATION
  content: {
    type: 4, // NodeTypes.SIMPLE_EXPRESSION
    isStatic: false,
    constType: 0, // ConstantTypes.NOT_CONSTANT
    content: "x",
    loc: {},
  },
  loc: {},
}
*/

函数所作的事情是对 {{/* ... */}} 字符串进行解析生成相应的节点对象(假设开始符号是 '{{',结束符号是 '}}'

函数首先通过 context.options.delimiters 获取开始符号与结束符号;然后从源字符串中提取 开始结束 中的字符串进行处理;最后生成相应的节点对象

解析元素

parseElement 函数会解析整个元素,包括子元素:

function parseElement(
  context: ParserContext,
  ancestors: ElementNode[]
): ElementNode | undefined {

  // 父节点
  const parent = last(ancestors)

  // 解析元素标签及属性,生成元素节点
  const element = parseTag(context, TagType.Start, parent)

  // 没有子节点的元素直接返回
  if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
    /* ... */

    return element
  }

  // 当前元素作为子元素的父元素
  ancestors.push(element)

  // 解析子元素生成节点集合
  const children = parseChildren(context, mode, ancestors)

  // 恢复
  ancestors.pop()

  // 赋值
  element.children = children

  if (startsWithEndTagOpen(context.source, element.tag)) {
    // 目的是步进 context.source
    parseTag(context, TagType.End, parent)
  } else {
    /* 提示 X_MISSING_END_TAG */
  }

  return element
}

解析标签

// 解析标签,既可用于开始标签,亦可用于结束标签
function parseTag(
  context: ParserContext,
  type: TagType.Start,
  parent: ElementNode | undefined
): ElementNode
function parseTag(
  context: ParserContext,
  type: TagType.End,
  parent: ElementNode | undefined
): void
function parseTag(
  context: ParserContext,
  type: TagType,
  parent: ElementNode | undefined
): ElementNode | undefined {
  /**
   * 获取标签名
   *
   * 效果:
   *  对于 <tag a b c></tag> 结果为 ['<tag', 'tag']
   *  对于 </tag> 结果为 ['</tag', 'tag']
   */
  const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!
  const tag = match[1]

  const ns = context.options.getNamespace(tag, parent)

  /* 步进 context.source,移除match[0] */
  /* 步进 context.source,移除多余空白字符 */

  // 当前标签是否是 <pre> 标签,可自定义
  if (context.options.isPreTag(tag)) {
    context.inPre = true
  }

  // 解析当前标签的所有属性
  let props = parseAttributes(context, type)

  // 指令属性是否存在 v-pre
  if (
    type === TagType.Start &&
    !context.inVPre &&
    props.some(p => p.type === NodeTypes.DIRECTIVE && p.name === 'pre')
  ) {
    context.inVPre = true

    /* context.source 重置 */

    // 重新解析属性,而此时会将指令属性节点都转换为普通属性节点
    props = parseAttributes(context, type).filter(p => p.name !== 'v-pre')
  }

  // Tag close.
  let isSelfClosing = false
  if (context.source.length === 0) {
    /* 提示 EOF_IN_TAG */
  } else {
    isSelfClosing = startsWith(context.source, '/>')

    /* 步进 context.source,根据 isSelfClosing 移除 /> 或者 > */
  }

  // 结束标签做完步进 context.source 就可以了
  if (type === TagType.End) {
    return
  }

  // 标签类型: ELEMENT/SLOT/TEMPLATE/COMPONENT
  let tagType = ElementTypes.ELEMENT

  // v-pre 将按照原始元素生成
  if (!context.inVPre) {
    if (tag === 'slot') {
      tagType = ElementTypes.SLOT
    } else if (tag === 'template') {
      if (
        props.some(
          p =>
            p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name)
        )
      ) {
        // isSpecialTemplateDirective: if,else,else-if,for,slot
        tagType = ElementTypes.TEMPLATE
      }
    } else if (isComponent(tag, props, context)) {
      /**
       * isComponent 函数将会尝试调用以下函数
       *  context.options.isCustomElement
       *  context.options.isBuiltInComponent
       *  context.options.isNativeTag
       * 还会查找 is 属性, is="vue:" / v-is
       */
      tagType = ElementTypes.COMPONENT
    }
  }

  return {
    type: NodeTypes.ELEMENT,
    ns,
    tag,
    tagType,
    props,
    isSelfClosing,
    children: [],
    loc: getSelection(context, start),
    codegenNode: undefined
  }
}

解析元素属性

元素属性存在零个及以上的情况,元素属性也可以分成 指令属性普通的属性。在未到达标签结束符号的时候,循环解析每一个属性:

function parseAttributes(
  context: ParserContext,
  type: TagType
): (AttributeNode | DirectiveNode)[] {
  // 属性节点集合
  const props = []

  // 属性名集合
  const attributeNames = new Set<string>()

  // 如果字符串不为空且起始位置没有匹配到 "闭合" 符号,此时 <tag  "当前位置"  >
  while (
    context.source.length > 0 &&
    !startsWith(context.source, '>') &&
    !startsWith(context.source, '/>') /* 自闭合 */
  ) {
    if (startsWith(context.source, '/')) {
      /* 提示 UNEXPECTED_SOLIDUS_IN_TAG */

      /* 步进 context.source,移除 '/' 及余下空白字符 */
      continue
    }

    // 解析单个属性
    const attr = parseAttribute(context, attributeNames)

    // Trim whitespace between class
    // https://github.com/vuejs/vue-next/issues/4251
    /* 处理 class 属性 */

    // 只有开始的标签属性才会录入
    if (type === TagType.Start) {
      props.push(attr)
    }

    /* 步进 context.source,移除多余空白字符 */
  }

  return props
}

parseAttributes 函数只会在 type === TagType.Start 即开始标签时返回当前节点的属性集合,是因为该函数也通用适用于结束标签,用于步进 context.source

解析单个元素属性

元素属性节点分为 指令属性节点普通属性节点,区别就在于属性的名字是否以特定字符开始:

function parseAttribute(
  context: ParserContext,
  nameSet: Set<string>
): AttributeNode | DirectiveNode {
  // 获取属性名,例如 a="b" 得到 a
  const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)!
  const name = match[0]

  /* 如果属性名已经存在时会提示 DUPLICATE_ATTRIBUTE */

  nameSet.add(name)

  /* 步进 context.source,移除属性名 */

  // 属性值
  let value: AttributeValue = undefined

  // 如果是有值属性(a="b")
  if (/^[\t\r\n\f ]*=/.test(context.source)) {
    // '     =      "value"  ' -> '"value"  '
    /* 步进 context.source,移除多余空白字符 */
    /* 步进 context.source,移除=*/
    /* 步进 context.source,移除多余空白字符 */
    value = parseAttributeValue(context)
  }

  // 所处环境不是 v-pre 且当前属性名与指令正则匹配,则此节点属于指令属性节点
  if (!context.inVPre && /^(v-[A-Za-z0-9-]|:|\.|@|#)/.test(name)) {
    /**
     * 效果:
     *  'v-a'     -> ['v-a', 'a']
     *  'v-a:b'   -> ['v-a:b', 'a', 'b']
     *  'v-a:b.c' -> ['v-a:b', 'a', 'b', '.c']
     *
     *  '@a'      -> ['@a', undefined, 'a']
     *  '@a.b'      -> ['@a.b', undefined, 'a', '.b']
     *
     *  ':a'      -> [':a', undefined, 'a']
     *  ':a.b'      -> [':a.b', undefined, 'a', '.b']
     *
     *  '#a'      -> ['#a', undefined, 'a']
     *  '#a.b'      -> ['#a.b', undefined, 'a', '.b']
     *
     *  '.a'      -> ['.a', undefined, 'a']
     *  '.a.b'      -> ['.a.b', undefined, 'a', '.b']
     */
    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'

      // 参数值
      let content = match[2]

      // 表示当前获取的引用是否时静态的
      let isStatic = true

      // v-on:[event]="method" -> content === 'event'
      // v-on:event="method" -> content === 'event'
      if (content.startsWith('[')) {
        isStatic = false

        // 更新 content 值
        if (!content.endsWith(']')) {
          /* 提示 X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END */
          content = content.slice(1)
        } else {
          content = content.slice(1, content.length - 1)
        }
      } else if (isSlot) {
        // #1241 special case for v-slot: vuetify relies extensively on slot
        // names containing dots. v-slot doesn't have any modifiers and Vue 2.x
        // supports such usage so we are keeping it consistent with 2.x.
        content += match[3] || ''
      }

      arg = {
        type: NodeTypes.SIMPLE_EXPRESSION,
        content,
        isStatic,
        constType: isStatic
          ? ConstantTypes.CAN_STRINGIFY
          : ConstantTypes.NOT_CONSTANT,
        loc
      }
    }

    /* 如果属性值存在引号,更新属性值的 loc,生成代码时避免转换为字符串值 */

    // [,,, '.a.b.c'] -> ['a', 'b', 'c']
    const modifiers = match[3] ? match[3].slice(1).split('.') : []

    if (isPropShorthand) modifiers.push('prop')

    // 返回指令属性节点
    return {
      type: NodeTypes.DIRECTIVE,
      name: dirName,
      exp: value && {
        type: NodeTypes.SIMPLE_EXPRESSION,
        content: value.content,
        isStatic: false,
        constType: ConstantTypes.NOT_CONSTANT,
        loc: value.loc
      },
      arg,
      modifiers,
      loc
    }
  }

  // 返回普通的属性节点
  return {
    type: NodeTypes.ATTRIBUTE,
    name,
    value: value && {
      type: NodeTypes.TEXT,
      content: value.content,
      loc: value.loc
    },
    loc
  }
}

//demo
baseParse('<tag id="app"></tag>')
/* 生成的节点对象(省略其他节点)
{
  type: 6, // NodeTypes.ATTRIBUTE
  name: "id",
  value: {
    type: 2, // NodeTypes.TEXT
    content: "app",
    loc: {},
  },
  loc: {},
}
*/

baseParse('<tag @click="method"></tag>')
/* 生成的节点对象(省略其他节点)
{
  type: 7, // NodeTypes.DIRECTIVE
  name: "on",
  exp: {
    type: 4, // NodeTypes.SIMPLE_EXPRESSION
    content: "method",
    isStatic: false,
    constType: 0, // ConstantTypes.NOT_CONSTANT
    loc: {},
  },
  arg: {
    type: 4, // NodeTypes.SIMPLE_EXPRESSION
    content: "click",
    isStatic: true,
    constType: 3, // ConstantTypes.CAN_STRINGIFY
    loc: {},
  },
  modifiers: [],
  loc: {},
}
*/
解析元素属性值

对于元素属性值的解析非常简单,就是获取引号里的文本内容(支持无引号):

function parseAttributeValue(context: ParserContext): AttributeValue {
  // 属性值
  let content: string

  // "value" 或者 'value' 表示存在引号
  const quote = context.source[0]

    // isQuoted 用于指令属性节点,判断是否需要更新属性值节点的 loc
  const isQuoted = quote === `"` || quote === `'`

  // 获取属性值内容
  if (isQuoted) {
  /* 步进 context.source,移除一个引号 */

    const endIndex = context.source.indexOf(quote)
    if (endIndex === -1) {/* 解析剩余字符串,获得属性值 */
      content = parseTextData(
        context,
        context.source.length,
        TextModes.ATTRIBUTE_VALUE
      )
    } else {/* 解析特定长度的字符串,获取属性值 */
      content = parseTextData(context, endIndex, TextModes.ATTRIBUTE_VALUE)

      /* 步进 context.source,移除一个引号 */
    }
  } else {
    /* 匹配获取属性值 /^[^\t\r\n\f >]+/.exec(context.source) */
  }

  return { content, isQuoted, loc: getSelection(context, start) }
}

总结

baseParse 函数使用 parseChildren 函数解析模板字符串生成子节点集合,传递给根节点,最后返回抽象语法树

生成的 ast 包含了源码的所有信息;之后经过 transform 对 ast 进行转换处理,对特定节点进行特殊转换;最终生成渲染函数