vue3源码学习10-编译one-模板解析为AST

首先找到编译的入口compile函数:

// 支持两个参数,第一个template是待编译的模板字符串,第二个options是编译的一些配置信息
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
    })
  )
}

可以发现内部执行的是baseCompile方法,而且在options的基础上又扩展了一些参数,下来看一下baseCompile的实现:packages/compiler-core/src/compile.ts

export function baseCompile(
  template: string | RootNode,
  options: CompilerOptions = {}
): CodegenResult {
  const onError = options.onError || defaultOnError
  const isModuleMode = options.mode === 'module'
  /* istanbul ignore if */
  if (__BROWSER__) {
    if (options.prefixIdentifiers === true) {
      onError(createCompilerError(ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED))
    } else if (isModuleMode) {
      onError(createCompilerError(ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED))
    }
  }

  const prefixIdentifiers =
    !__BROWSER__ && (options.prefixIdentifiers === true || isModuleMode)
  if (!prefixIdentifiers && options.cacheHandlers) {
    onError(createCompilerError(ErrorCodes.X_CACHE_HANDLER_NOT_SUPPORTED))
  }
  if (options.scopeId && !isModuleMode) {
    onError(createCompilerError(ErrorCodes.X_SCOPE_ID_NOT_SUPPORTED))
  }
  // 解析template生成AST
  const ast = isString(template) ? baseParse(template, options) : template
  const [nodeTransforms, directiveTransforms] =
    getBaseTransformPreset(prefixIdentifiers)

  if (!__BROWSER__ && options.isTS) {
    const { expressionPlugins } = options
    if (!expressionPlugins || !expressionPlugins.includes('typescript')) {
      options.expressionPlugins = [...(expressionPlugins || []), 'typescript']
    }
  }
  // AST 转换
  transform(
    ast,
    extend({}, options, {
      prefixIdentifiers,
      nodeTransforms: [
        ...nodeTransforms,
        ...(options.nodeTransforms || []) // user transforms
      ],
      directiveTransforms: extend(
        {},
        directiveTransforms,
        options.directiveTransforms || {} // user transforms
      )
    })
  )
  // 生成代码
  return generate(
    ast,
    extend({}, options, {
      prefixIdentifiers
    })
  )
}

AST(以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构),这里只要知道AST中的节点是可以完整描述她在模板中映射的节点信息,一颗必须有根节点,它的根节点是一个虚拟节点,并不会映射到一个具体节点,这里的虚拟节点就可以让我们在template中定义多个根节点,这个在vue2.x中是会报错的。现在看下解析template生成的实现:

packages/compiler-core/src/parse.ts

export function baseParse(
  content: string,
  options: ParserOptions = {}
): RootNode {
  // 创建解析上下文
  const context = createParserContext(content, options)
  const start = getCursor(context)
  // 解析子节点,并创建AST
  return createRoot(
    parseChildren(context, TextModes.DATA, []),
    getSelection(context, start)
  )
}

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

// 1.创建解析上下文
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表示解析的相关配置 
    options,
	// 当前代码列号
    column: 1,
	// 当前代码行号
    line: 1,
	// 当前代码相对于原始代码的偏移量
    offset: 0,
	// 最初的原始代码
    originalSource: content,
	// 当前代码
    source: content,
	// 当前代码是否在pre标签内
    inPre: false,
	// 当前代码是否在v-pre指令环境下
    inVPre: false,
    onWarn: options.onWarn
  }
}

// 2.解析子节点: 解析并创建AST节点数组,一是自顶向下分析代码,生成AST节点数组;二是空白字符管理,提高编译的效率。
function parseChildren(
  context: ParserContext,
  mode: TextModes,
  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) {
      if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {
        // '{{'
		// 处理 {{ 插值代码
        node = parseInterpolation(context, mode)
      } else if (mode === TextModes.DATA && s[0] === '<') {
        // https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
		// 处理 < 开头的代码
        if (s.length === 1) {
		  // s 长度为1, 说明代码结尾是< ,报错
          emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 1)
        } else if (s[1] === '!') {
		  // 处理 <! 开头的代码
          // https://html.spec.whatwg.org/multipage/parsing.html#markup-declaration-open-state
          if (startsWith(s, '<!--')) {
			// 处理注释节点
            node = parseComment(context)
          } else if (startsWith(s, '<!DOCTYPE')) {
            // Ignore DOCTYPE by a limitation.
			// 处理 <!DOCTYPE 节点
            node = parseBogusComment(context)
          } else if (startsWith(s, '<![CDATA[')) {
			// 处理 <![CDATA[ 节点
            if (ns !== Namespaces.HTML) {
              node = parseCDATA(context, ancestors)
            } else {
              emitError(context, ErrorCodes.CDATA_IN_HTML_CONTENT)
              node = parseBogusComment(context)
            }
          } else {
            emitError(context, ErrorCodes.INCORRECTLY_OPENED_COMMENT)
            node = parseBogusComment(context)
          }
        } else if (s[1] === '/') {
          // https://html.spec.whatwg.org/multipage/parsing.html#end-tag-open-state
		  // 处理 </ 结束标签
          if (s.length === 2) {
			// 长度为2 说明结尾是 </ ,报错
            emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 2)
          } else if (s[2] === '>') {
			// </> 缺少结束标签,报错
            emitError(context, ErrorCodes.MISSING_END_TAG_NAME, 2)
            advanceBy(context, 3)
            continue
          } else if (/[a-z]/i.test(s[2])) {
			// 多余的结束标签
            emitError(context, ErrorCodes.X_INVALID_END_TAG)
            parseTag(context, TagType.End, parent)
            continue
          } else {
            emitError(
              context,
              ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME,
              2
            )
            node = parseBogusComment(context)
          }
        } else if (/[a-z]/i.test(s[1])) {
		  // 解析标签元素节点
          node = parseElement(context, ancestors)
        } else if (s[1] === '?') {
          emitError(
            context,
            ErrorCodes.UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME,
            1
          )
          node = parseBogusComment(context)
        } else {
          emitError(context, ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME, 1)
        }
      }
    }
    if (!node) {
	  // 解析普通文本节点
      node = parseText(context, mode)
    }

    if (isArray(node)) {
	  // 如果 node 是数组,则遍历添加
      for (let i = 0; i < node.length; i++) {
        pushNode(nodes, node[i])
      }
    } else {
	  // 添加单个node
      pushNode(nodes, node)
    }
  }

  // Whitespace handling strategy like v2
  // 模板代码中经常会出现换行和空格,需要移除这些没有意义的节点,提高编译效率
  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) {
        if (!/[^\t\r\n\f ]/.test(node.content)) {
		  // 匹配空白字符
          const prev = nodes[i - 1]
          const next = nodes[i + 1]
          // Remove if:
          // - the whitespace is the first or last node, or:
          // - (condense mode) the whitespace is adjacent to a comment, or:
          // - (condense mode) the whitespace is between two elements AND contains newline
		  // 如果空白字符是开头或者结尾节点, 或者空白字符与注释节点相连,或者空白字符在两个元素之间并包含换行符,直接移除
          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 {
			// 否则压缩这些空白字符成一个空格
            // Otherwise, the whitespace is condensed into a single space
            node.content = ' '
          }
        } else if (shouldCondense) {
          // in condense mode, consecutive whitespaces in text are condensed
          // down to a single space.
          node.content = node.content.replace(/[\t\r\n\f ]+/g, ' ')
        }
      }
      // Remove comment nodes if desired by configuration.
      else if (node.type === NodeTypes.COMMENT && !context.options.comments) {
		// 非空文件
        removedWhitespace = true
        nodes[i] = null as any
      }
    }
    if (context.inPre && parent && context.options.isPreTag(parent.tag)) {
      // remove leading newline per html spec
      // https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
      const first = nodes[0]
      if (first && first.type === NodeTypes.TEXT) {
		// 根据HTML规范删除前导换行符
        first.content = first.content.replace(/^\r?\n/, '')
      }
    }
  }
  return removedWhitespace ? nodes.filter(Boolean) : nodes
}

其中有很多判断,我们只看四种情况解析:注释节点、插值、普通文本、元素节点:

1.注释节点解析

function parseComment(context: ParserContext): CommentNode {
  __TEST__ && assert(startsWith(context.source, '<!--'))

  const start = getCursor(context)
  let content: string

  // Regular comment.
  const match = /--(\!)?>/.exec(context.source)
  if (!match) {
	// 没有匹配的注释结束符
    content = context.source.slice(4)
    advanceBy(context, context.source.length)
    emitError(context, ErrorCodes.EOF_IN_COMMENT)
  } else {
    if (match.index <= 3) {
	  // 非法的注释符号
      emitError(context, ErrorCodes.ABRUPT_CLOSING_OF_EMPTY_COMMENT)
    }
    if (match[1]) {
	  // 注释结束符不正确
      emitError(context, ErrorCodes.INCORRECTLY_CLOSED_COMMENT)
    }
	// 获取注释的内容
    content = context.source.slice(4, match.index)

    // Advancing with reporting nested comments.
	// 截取到注释结尾之间的代码,用于后续判断嵌套注释
    const s = context.source.slice(0, match.index)
    let prevIndex = 1,
      nestedIndex = 0
	// 判断嵌套注释符的情况,存在即报错
    while ((nestedIndex = s.indexOf('<!--', prevIndex)) !== -1) {
      advanceBy(context, nestedIndex - prevIndex + 1)
      if (nestedIndex + 4 < s.length) {
        emitError(context, ErrorCodes.NESTED_COMMENT)
      }
      prevIndex = nestedIndex + 1
    }
	// 前进代码到注释结束符后
    advanceBy(context, match.index + match[0].length - prevIndex + 1)
  }

  return {
	// 表示是注释节点
    type: NodeTypes.COMMENT,
	// 注释的内容
    content,
	// 注释代码的开头和结束位置信息
    loc: getSelection(context, start)
  }
}

可以发现就是利用正则去匹配,截取注释内容,其他情况报错,最后调用了advanceBy去前进代码,更新content解析上下文:

function advanceBy(context: ParserContext, numberOfCharacters: number): void {
  const { source } = context
  __TEST__ && assert(numberOfCharacters <= source.length)
  // 更新context的offset、line、column
  advancePositionWithMutation(context, source, numberOfCharacters)
  // 更新context的source
  context.source = source.slice(numberOfCharacters)
}

// packages/compiler-core/src/utils.ts
export function advancePositionWithMutation(
  pos: Position,
  source: string,
  numberOfCharacters: number = source.length
): Position {
  let linesCount = 0
  let lastNewLinePos = -1
  for (let i = 0; i < numberOfCharacters; i++) {
    if (source.charCodeAt(i) === 10 /* newline char code */) {
      linesCount++
      lastNewLinePos = i
    }
  }

  pos.offset += numberOfCharacters
  pos.line += linesCount
  pos.column =
    lastNewLinePos === -1
      ? pos.column + numberOfCharacters
      : numberOfCharacters - lastNewLinePos

  return pos
}

这时注释部分代码就处理完了,接着看插值的解析:

2.**插值解析: ** {{ msg }}

function parseInterpolation(
  context: ParserContext,
  mode: TextModes
): InterpolationNode | undefined {
  // 从默认配置中获取插值开始和结束分隔符,默认是 {{ 和 }}
  const [open, close] = context.options.delimiters
  __TEST__ && assert(startsWith(context.source, open))

  // 跳过开始分隔符的位置,查找结束分割符
  const closeIndex = context.source.indexOf(close, open.length)
  if (closeIndex === -1) {
	// 不存在就报错
    emitError(context, ErrorCodes.X_MISSING_INTERPOLATION_END)
    return undefined
  }

  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,
    content: {
      type: NodeTypes.SIMPLE_EXPRESSION,
      isStatic: false,
      // Set `isConstant` to false by default and will decide in transformExpression
      constType: ConstantTypes.NOT_CONSTANT,
      content,
      loc: getSelection(context, innerStart, innerEnd)
    },
    loc: getSelection(context, start)
  }
}

3.普通文本解析:This is my app

function parseText(context: ParserContext, mode: TextModes): TextNode {
  __TEST__ && assert(context.source.length > 0)

  // 文本结束符:对于一段文本来说,都是在遇到 < 或者插值分隔符 {{ 结束
  const endTokens =
    mode === TextModes.CDATA ? [']]>'] : ['<', 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
    }
  }

  __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 {
  const rawText = context.source.slice(0, length)
  advanceBy(context, length)
  if (
    mode === TextModes.RAWTEXT ||
    mode === TextModes.CDATA ||
    !rawText.includes('&')
  ) {
    return rawText
  } else {
    // DATA or RCDATA containing "&"". Entity decoding required.
    return context.options.decodeEntities(
      rawText,
      mode === TextModes.ATTRIBUTE_VALUE
    )
  }
}

4.元素节点解析

<div class="app">
  <hello :msg="msg"></hello>
</div>

当前代码以 < 开头,并且后面跟着字母:

function parseElement(
  context: ParserContext,
  ancestors: ElementNode[]
): ElementNode | undefined {
  __TEST__ && assert(/^<[a-z]/i.test(context.source))

  // Start tag.
  // 是否在 pre 标签内
  const wasInPre = context.inPre
  // 是否在 v-pre 指令内
  const wasInVPre = context.inVPre
  // 获取当前元素的父标签节点
  const parent = last(ancestors)
  // 解析开始标签,生成一个标签节点,并前进代码到开始标签后
  const element = parseTag(context, TagType.Start, parent)
  // 是否 在 pre 标签的边界
  const isPreBoundary = context.inPre && !wasInPre
  // 是否在 v-pre 指令的边界
  const isVPreBoundary = context.inVPre && !wasInVPre

  if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
	// 如果是自闭合标签,直接返回标签节点
    // #4030 self-closing <pre> tag
    if (isPreBoundary) {
      context.inPre = false
    }
    if (isVPreBoundary) {
      context.inVPre = false
    }
    return element
  }

  // Children.
  // 下边是处理子节点
  // 先把标签节点添加到ancestors,进行入栈
  ancestors.push(element)
  const mode = context.options.getTextMode(element, parent)
  // 递归解析子节点,传入ancestors
  const children = parseChildren(context, mode, ancestors)
  // ancestors出栈
  ancestors.pop()

  // 添加到children属性中
  element.children = children

  // End tag.
  // 结束标签
  if (startsWithEndTagOpen(context.source, element.tag)) {
	// 解析结束标签,并前进代码到结束标签后
    parseTag(context, TagType.End, parent)
  } else {
	// 找不到结束标签,报错
    emitError(context, ErrorCodes.X_MISSING_END_TAG, 0, element.loc.start)
    if (context.source.length === 0 && element.tag.toLowerCase() === 'script') {
      const first = children[0]
      if (first && startsWith(first.loc.source, '<!--')) {
        emitError(context, ErrorCodes.EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT)
      }
    }
  }
  // 更新标签节点的代码位置,结束位置到标签节点后
  element.loc = getSelection(context, element.loc.start)

  if (isPreBoundary) {
    context.inPre = false
  }
  if (isVPreBoundary) {
    context.inVPre = false
  }
  return element
}

元素节点解析主要做了三件事:解析开始标签、解析子节点、解析闭合标签,首先来看开始标签的解析,是通过parseTag解析并创建一个标签节点:

// parseTag 最终返回的值就是一个描述标签节点的对象,其中 type 表示它是一个标签节点,tag 表示标签名,tagType 表示标签的类型,
// content 表示文本的内容,isSelfClosing 表示是否是一个闭合标签,loc 表示文本的代码开头和结束的位置信息,children 是标签的
// 子节点数组,会先初始化为空。

function parseTag(
  context: ParserContext,
  type: TagType,
  parent: ElementNode | undefined
): ElementNode | undefined {

  // Tag open.
  // 标签打开
  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)

  // save current state in case we need to re-parse attributes with v-pre
  // 保存当前状态以防后面需要用v-pre重新解析属性
  const cursor = getCursor(context)
  const currentSource = context.source

  // check <pre> tag
  // 检查是不是一个 pre 标签
  if (context.options.isPreTag(tag)) {
    context.inPre = true
  }

  // Attributes.
  // 解析标签中的属性, 并前进代码到属性后
  let props = parseAttributes(context, type)

  // check v-pre
  // 检查属性中有没有 v-pre 指令
  if (
    type === TagType.Start &&
    !context.inVPre &&
    props.some(p => p.type === NodeTypes.DIRECTIVE && p.name === 'pre')
  ) {
    context.inVPre = true
    // reset context
	// 重置context
    extend(context, cursor)
    context.source = currentSource
    // re-parse attrs and filter out v-pre itself
	// 重新解析属性,并把 v-pre 过滤了
    props = parseAttributes(context, type).filter(p => p.name !== 'v-pre')
  }

  // Tag close.
  // 闭合标签
  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)
  }

  if (type === TagType.End) {
    return
  }
  // 判断标签类型。是组件、插槽还是模板
  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)
        )
      ) {
		// 判断是否有is属性
        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
  }
}

子节点解析完成后,baseParse就剩使用createRoot创建AST根节点,来看createRoot实现:

// packages/compiler-core/src/ast.ts
export function createRoot(
  children: TemplateChildNode[],
  loc = locStub
): RootNode {
  // 返回一个js对象,作为AST根节点
  return {
	// 根节点类型
    type: NodeTypes.ROOT,
	// 之前解析的子节点数组
    children,
    helpers: [],
    components: [],
    directives: [],
    hoists: [],
    imports: [],
    cached: 0,
    temps: 0,
    codegenNode: undefined,
    loc
  }
}
posted @ 2022-09-17 13:54  菜菜123521  阅读(135)  评论(0编辑  收藏  举报