Vue3.0源码解读 - 编译系统

一、baseCompile

baseCompile:
编译器的总入口,是编译器的一个基础骨架(概念上可以理解为基类),然后不同平台的编译系统都是基于baseCompile来进行扩展的,如dom编译、服务端渲染编译、sfc,都是基于baseCompile进行了对应平台下处理场景的扩展。

function baseCompile(
  template: string | RootNode,
  options: CompilerOptions = {}
): CodegenResult {
  // 省略无关代码...

  // 生成AST节点树
  const ast = isString(template) ? baseParse(template, options) : template
  // 获取节点转换工具集、指令转换工具集
  const [nodeTransforms, directiveTransforms] = getBaseTransformPreset(
    prefixIdentifiers
  )
  // 遍历AST节点树,对上面生成的AST进行指令转换,生成可用节点,同时根据compiler
  // 传入的配置(如是否做静态节点提升等)对AST节点树进行优化处理,为rootNode及
  // 下属每个节点挂载codegenNode
  transform(
    ast,
    extend({}, options, {
      prefixIdentifiers,
      nodeTransforms: [
        ...nodeTransforms,
        ...(options.nodeTransforms || []) // user transforms
      ],
      directiveTransforms: extend(
        {},
        directiveTransforms,
        options.directiveTransforms || {} // user transforms
      )
    })
  )
  
  // 对转换及优化后的AST进行代码生成
  return generate(
    ast,
    extend({}, options, {
      prefixIdentifiers
    })
  )
}

二、parser模版解析器

parser的作用就是将我们传入的template string转化为AST节点树,供后面的transform使用。
rootNode:
根节点是一个临时容器,真正在运行时映射成具体内容的是rootNode下的children,说白了rootNode只是个用来存放实际节点的空壳子,假如parse AST节点时template string中是多根节点,那么没有一个抽象出来的根节点就无法表述完整的树结构,这也是为什么vue3.0能够允许多根模版的原因所在。
Position:
包含offset、line、column三个属性,offset记录parser解析到的位置相对原始template string开头的位置,line记录parser解析到的行数,column为列数,因为parse过程中会遇到\n\t\f之类的转义字符。
baseParse:
将template string解析成AST,AST是vue对节点的一种表述形式,和平时JS生成的抽象语法树是两码事。

export function baseParse(
  content: string, // 原始的模版字符串
  options: ParserOptions = {}
): RootNode {
  // 获得parser上下文,相当于class实例化的产物,用来存储parser的一些信息
  const context = createParserContext(content, options)
  // 获取parser开始位置
  const start = getCursor(context)
  // 生成AST节点
  return createRoot(
    parseChildren(context, TextModes.DATA, []), // 生成AST子节点
    getSelection(context, start) // 获取根节点位置信息、对应string信息:loc
  )
}

createParserContext创建出上下文的结构:

{
    options: extend({}, defaultParserOptions, options), // parser配置项
    // column、line、offset均是相对template string的全局位置信息
    column: 1, // parser解析到的列数
    line: 1, // 解析到的行数
    offset: 0, // 解析到相对于template string开始的位置
    originalSource: content, // 初始template string,即用户定义的完整模版字符串
    source: content, // parser处理后的最新template string
    inPre: false,
    inVPre: false
}

createRoot:
创建一个虚拟根节点容器,根据模版解析出children和对应的位置信息,透传给根节点对象

function createRoot(
  children: TemplateChildNode[],
  loc = locStub
): RootNode {
  // 生成AST根节点的结构
  return {
    type: NodeTypes.ROOT, // 节点类型
    children, // 子节点
    helpers: [],
    components: [], // 组件节点
    directives: [], // 指令节点
    hoists: [], 
    imports: [],
    cached: 0,
    temps: 0,
    codegenNode: undefined, // 用于后续generate阶段生成vnode创建物料
    // 节点在template string中所处的位置,结构{ source, start, end }
    // source对应节点在模版中对应的string部分,start、end对应节点起始标签
    // 对应template string中的位置
    loc 
  }
}

createChildren:
创建AST根节点容器的所属子节点,即模版中实际的节点。该方法为parser核心处理逻辑,用于解析一段“完整”的模版串,比如<div><p>test</p></div><div>...</div><p>...</p>都是“完整”的,因此会递归的执行parseChildren解析子节点,将解析出的子节点插入父节点中。
过程中两个比较重要的解析方法是parseInterpolation(处理插值)、parseElement(解析dom节点),将重点介绍。

function parseChildren(
  context: ParserContext,
  mode: TextModes,
  // 祖先节点,是一个栈结构,用于维护节点嵌套关系,越靠后的节点在dom树中的层级越深
  ancestors: ElementNode[] 
): TemplateChildNode[] {
  // 父节点
  const parent = last(ancestors)
  const ns = parent ? parent.ns : Namespaces.HTML
  // 存储解析出来的AST子节点
  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])) {
        // '{{'
        // 解析以‘{{’开头的模版,parseInterpolation为核心方法,下面重点讲解
        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) {
          // 错误处理
          emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 1)
        } else if (s[1] === '!') {
          if (startsWith(s, '<!--')) {
            // 解析注释节点
            node = parseComment(context)
          } else if (startsWith(s, '<!DOCTYPE')) {
            // Ignore DOCTYPE by a limitation.
            node = parseBogusComment(context)
          } else if (startsWith(s, '<![CDATA[')) {
            if (ns !== Namespaces.HTML) {
              node = parseCDATA(context, ancestors)
            } else {
              // 错误处理,省略
            }
          } else {
            // 错误处理省略
          }
        } else if (s[1] === '/') {
          // 解析结束标签错误的逻辑,此处省略
        } else if (/[a-z]/i.test(s[1])) {
          // 解析正常的html开始标签,获得解析到的AST节点
          // parseElement是核心方法,下面重点讲解
          node = parseElement(context, ancestors)
        } else if (s[1] === '?') {
          // 解析错误处理,省略
        } else {
          // 解析错误处理,省略
        }
      }
    }
    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)
    }
  }

  // Whitespace management for more efficient output
  // (same as v2 whitespace: 'condense')
  let removedWhitespace = false
  if (mode !== TextModes.RAWTEXT) {
    if (!context.inPre) {
      for (let i = 0; i < nodes.length; i++) {
        const node = nodes[i]
        if (node.type === NodeTypes.TEXT) {
          if (!/[^\t\r\n\f ]/.test(node.content)) {
            const prev = nodes[i - 1]
            const next = nodes[i + 1]
            // If:
            // - the whitespace is the first or last node, or:
            // - the whitespace is adjacent to a comment, or:
            // - the whitespace is between two elements AND contains newline
            // Then the whitespace is ignored.
            if (
              !prev ||
              !next ||
              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, condensed consecutive whitespace inside the text
              // down to a single space
              node.content = ' '
            }
          } else {
            node.content = node.content.replace(/[\t\r\n\f ]+/g, ' ')
          }
        } else if (!__DEV__ && node.type === NodeTypes.COMMENT) {
          // remove comment nodes in prod
          removedWhitespace = true
          nodes[i] = null as any
        }
      }
    } else if (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) {
        first.content = first.content.replace(/^\r?\n/, '')
      }
    }
  }

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

那么入参里的这个mode是什么呢,其实就是vue对节点进行了一个分类,大概看一下:

getTextMode({ tag, ns }: ElementNode): TextModes {
  // 节点命名空间是html的情况
  if (ns === DOMNamespaces.HTML) {
    if (tag === 'textarea' || tag === 'title') {
      // textarea和title标签属于RCDATA类型
      return TextModes.RCDATA
    }
    if (isRawTextContainer(tag)) {
      // 文本容器,包括style、script、noscript、iframe,归类到RAWTEXT
      return TextModes.RAWTEXT
    }
  }
  // 非html命名空间的节点归类到DATA,如svg
  return TextModes.DATA
}

parseElement:
解析dom元素生成AST节点

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

  // Start tag.
  const wasInPre = context.inPre
  const wasInVPre = context.inVPre
  const parent = last(ancestors) // 父节点
  // 解析开始标签生成AST节点
  const element = parseTag(context, TagType.Start, parent)
  const isPreBoundary = context.inPre && !wasInPre
  const isVPreBoundary = context.inVPre && !wasInVPre

  // 如果是自我闭合节点或者空标签,直接返回解析出的AST节点
  if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
    return element
  }

  // 根据开始标签解析出的节点入栈,并解析它的子节点,子节点解析完毕后,父节点出栈
  ancestors.push(element)
  const mode = context.options.getTextMode(element, parent)
  // 递归解析子节点,子节点解析过程中遇到父节点的结束标签,即解析完成并返回解析结果
  const children = parseChildren(context, mode, 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:

function parseTag(
  context: ParserContext,
  type: TagType,
  parent: ElementNode | undefined
): ElementNode {
  // 标签开始<.
  const start = getCursor(context)
  // 正则匹配开始 / 结束标签,\/?表示‘/’可有可无,因为此处匹配的是开始或者结束标签
  // 所以有的有‘/’有的没有
  const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!
  // exec返回的数组第一个是正则匹配文本,后面是子表达式匹配的内容,也就是括号里匹配到的
  // 内容,此处子表达式匹配到的是除\t\r\n\f />这些字符外的内容,也就是节点的标签名、
  // 各种属性集
  const tag = match[1]
  const ns = context.options.getNamespace(tag, parent)

  // parser解析完开始标签,推进模版字符串,位数为整个开始标签的长度
  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
  // 解析属性props,parseAttributes内部主要调用了parseAttribute,下面会讲到
  let props = parseAttributes(context, type)
  // <pre>标签和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)
    }
    // 根据是否是自闭合标签向前推进对应的位数,普通标签>:1位,自闭合标签/>:2位
    advanceBy(context, isSelfClosing ? 2 : 1)
  }

  let tagType = ElementTypes.ELEMENT
  const options = context.options
  if (!context.inVPre && !options.isCustomElement(tag)) {
    // 是否有由v-is属性(动态组件)
    const hasVIs = props.some(
      p => p.type === NodeTypes.DIRECTIVE && p.name === 'is'
    )
    if (options.isNativeTag && !hasVIs) {
      // 判断是否是html原生标签,如果不是则标记为组件类型的标签
      if (!options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT
    } else if (
      hasVIs ||
      isCoreComponent(tag) ||
      (options.isBuiltInComponent && options.isBuiltInComponent(tag)) ||
      /^[A-Z]/.test(tag) ||
      tag === 'component'
    ) {
      // 被解析为组件标签的case
      tagType = ElementTypes.COMPONENT
    }

    // 插槽标签处理
    if (tag === 'slot') {
      tagType = ElementTypes.SLOT
    } else if (
      tag === 'template' &&
      props.some(p => {
        return (
          p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name)
        )
      })
    ) {
      tagType = ElementTypes.TEMPLATE
    }
  }

  // 由tag解析出的AST节点结构
  return {
    type: NodeTypes.ELEMENT,
    ns,
    tag, // 标签名
    tagType, // 标签类型,表明是原生还是某种组件类型
    props, // 属性,解析后的表达式类型(包含属性的类型、名称、值表达式),具体看parseAttribute
    isSelfClosing,
    children: [], // 子节点,由外部的parseChildren生成并注入
    loc: getSelection(context, start), // 对应模版字符串中的位置信息
    codegenNode: undefined // to be created during transform phase
  }
}

parseAttribute:

function parseAttribute(
  context: ParserContext,
  nameSet: Set<string>
): AttributeNode | DirectiveNode {
  // Name.
  const start = getCursor(context)
  // 匹配属性名称
  const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)!
  const name = match[0]

  // 保证属性名称唯一性
  if (nameSet.has(name)) {
    emitError(context, ErrorCodes.DUPLICATE_ATTRIBUTE)
  }
  nameSet.add(name)

  if (name[0] === '=') {
    emitError(context, ErrorCodes.UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME)
  }
  {
    const pattern = /["'<]/g
    let m: RegExpExecArray | null
    while ((m = pattern.exec(name))) {
      emitError(
        context,
        ErrorCodes.UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME,
        m.index
      )
    }
  }

  // 推进属性名称
  advanceBy(context, name.length)

  // 解析属性值
  let value:
    | {
        content: string // 引号间(不一定在引号里)的原生文本内容
        isQuoted: boolean // 值是否包含在引号里
        loc: SourceLocation // 对应位置
      }
    | undefined = undefined

  if (/^[\t\r\n\f ]*=/.test(context.source)) {
    // 推进‘=’
    advanceSpaces(context)
    advanceBy(context, 1)
    advanceSpaces(context)
    // 解析出属性值,结构见value声明处
    value = parseAttributeValue(context)
    if (!value) {
      emitError(context, ErrorCodes.MISSING_ATTRIBUTE_VALUE)
    }
  }
  const loc = getSelection(context, start)

  // 指令属性,包括v-、:、@...开头的属性
  if (!context.inVPre && /^(v-|:|@|#)/.test(name)) {
    // 示例:指令v-bind:test用下面正则匹配后结果为:match = [
	//  'v-bind:test',
	//  'bind', // match[1]
	//  'test', // match[2]
	//  undefined,
	//  index: 0,
	//  input: 'v-bind:test',
	//  groups: undefined
	// ]
    const match = /(?:^v-([a-z0-9-]+))?(?:(?::|^@|^#)(\[[^\]]+\]|[^\.]+))?(.+)?$/i.exec(
      name
    )!

    // 确定指令所属类型:bind(e.g. :test)、on(e.g. @click)、slot
    // 对于全名的指令,如v-{dirName}:test,他的类型是{}里的内容,即on或bind
    // 简写命令如@click、:test对应的也是on、bind,指令的使用vue用户应该
    // 再熟悉不过了吧
    const dirName =
      match[1] ||
      (startsWith(name, ':') ? 'bind' : startsWith(name, '@') ? 'on' : 'slot')

    // arg代表属性真名
    let arg: ExpressionNode | undefined
    
    // match[2]对应的就是指令的真名了,e.g. v-bind:{realName}
    if (match[2]) {
      const isSlot = dirName === 'slot'
      const startOffset = name.indexOf(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('[')) {
        isStatic = false

        if (!content.endsWith(']')) {
          emitError(
            context,
            ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END
          )
        }

        content = content.substr(1, content.length - 2)
      } 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, // 是否是静态
        isConstant: isStatic, // 是否常量
        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)
    }
 
    // 指令属性的结构(v-if、v-for、v-bind、v-once...)
    return {
      type: NodeTypes.DIRECTIVE, // 表明是指令型属性
      name: dirName, // 指令类型
      exp: value && {
        type: NodeTypes.SIMPLE_EXPRESSION,
        content: value.content,
        isStatic: false,
        // transformExpression时可能会将isContant置为true,因为有些表达式是不会变化的
        // 所以有必要让其参与到静态提升中
        isConstant: false,
        loc: value.loc
      },
      arg, // 指令真名表达式
      modifiers: match[3] ? match[3].substr(1).split('.') : [],
      loc // 位置信息
    }
  }
   
  // 普通dom属性的结构,值一定是静态的,不会发生变化,如test="test"
  return {
    type: NodeTypes.ATTRIBUTE, // 非指令型指令类型
    name, // 属性名称
    value: value && {
      type: NodeTypes.TEXT,
      content: value.content,
      loc: value.loc
    }, // 属性值信息
    loc
  }
}

isEnd:
根据节点栈(ancestors)中的最后一个入栈节点和匹配到的结束标签做比较,如果判断为同一标签,表明节点是合法闭合的

function isEnd(
  context: ParserContext,
  mode: TextModes,
  ancestors: ElementNode[]
): boolean {
  const s = context.source

  switch (mode) {
    // 非html命名空间节点
    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

    // 直接装载文本的节点:一定是无层级的,直接判断入栈的最后一个节点即可
    case TextModes.RCDATA: // title、textarea
    case TextModes.RAWTEXT: { // script、noscript、iframe、style
      const parent = last(ancestors)
      if (parent && startsWithEndTagOpen(s, parent.tag)) {
        return true
      }
      break
    }

    case TextModes.CDATA:
      if (startsWith(s, ']]>')) {
        return true
      }
      break
  }

  return !s
}

advanceBy:
parser推进template string后,裁切template string为推进后的位置至尾部之间的内容,将其作为最新template string

function advanceBy(context: ParserContext, numberOfCharacters: number): void {
  const { source } = context
  advancePositionWithMutation(context, source, numberOfCharacters)
  context.source = source.slice(numberOfCharacters)
}

advancePositionWithMutation:
根据传入长度对模版字符串向前推进,同时推进后的最新位置信息。函数在parser中会频繁调用,考虑到拷贝新的位置信息耗费性能,因此直接修改源位置信息以节省开销。

export function advancePositionWithMutation(
  pos: Position, // 解析器当前位置信息
  source: string, // 当前待分析模版字符串
  numberOfCharacters: number = source.length // 模版字符串推进长度
): Position {
  let linesCount = 0 // 换行后的总行数
  let lastNewLinePos = -1 // 换行后上一行最后一个字符在template string中的位置
  // 遍历推进的内容,遇到换行符累加行数linesCount,同时记录上一行最后一个字符的
  // 位置lastNewLinePos
  for (let i = 0; i < numberOfCharacters; i++) {
    if (source.charCodeAt(i) === 10 /* newline char code */) {
      linesCount++
      lastNewLinePos = i
    }
  }

  // 累加之前的position结果,计算出parser最新的位置信息
  pos.offset += numberOfCharacters
  pos.line += linesCount
  pos.column =
    lastNewLinePos === -1
      ? pos.column + numberOfCharacters
      : numberOfCharacters - lastNewLinePos

  return pos
}

三、transform AST转换器

transform阶段主要是对parse阶段生成的AST节点树进行转化处理,产出可以在generate阶段生成运行时代码的信息(gencodeNode),转化过程中大量用表达式对象来描述信息(gencodeNode本身也可以是一个表达式对象),下面的代码就能体会到了。
表达式对象有很多种,以最简单的简单表达式(SimpleExpression)为例(比如一个变量引用):

{
  type: NodeTypes.SIMPLE_EXPRESSION, // 表达式类型标示
  loc, // 位置信息
  isConstant, // 是否是常量
  content, // 表达式内容
  // 是否是静态的,
  // e.g. v-bind:attr="value",value如果是动态变化的变量
  // v-bind:attr="true",true是常量不会变化,因此是静态的
  isStatic 
}
// 比如v-bind:attr="true",true转换为简单表达式对象就是
// { isContant: true, content: 'true', isStatic: true ... }

transform:

function transform(root: RootNode, options: TransformOptions) {
  // 创建transform上下文
  const context = createTransformContext(root, options)
  // 深度遍历AST,根据节点中的指令(v-if、v-for...转换为相应节点)
  // 遍历过程中转换完成后每个节点的codegenNode会挂载转换后的节点内容
  traverseNode(root, context)
  if (options.hoistStatic) {
    // 如果编译器配置了静态节点提升,对静态节点进行提升
    hoistStatic(root, context)
  }
  if (!options.ssr) {
    // 根据根节点下的children生成rootNode上的codegenNode
    createRootCodegen(root, context)
  }
  // 将transform上下文上的信息挂载到根节点
  root.helpers = [...context.helpers]
  root.components = [...context.components]
  root.directives = [...context.directives]
  root.imports = [...context.imports]
  root.hoists = context.hoists
  root.temps = context.temps
  root.cached = context.cached
}

traverseNode:

遍历AST节点树过程中,通过node转换器(nodeTransforms)对当前节点进行node转换,子节点全部遍历完成后执行对应指令的onExit回调退出转换。对v-if、v-for等指令的转换生成对应节点,都是由nodeTransforms中对应的指令转换工具完成的。
经nodeTransforms处理过的AST节点会被挂载codeGenNode属性(其实就是调用vnode创建的interface),该属性包含patchFlag等在AST解析阶段无法获得的信息,其作用就是为了在后面的generate阶段生成vnode的创建调用。
本质上codegenNode是一个表达式对象。

function traverseNode(
  node: RootNode | TemplateChildNode,
  context: TransformContext
) {
  // 上下文记录当前正在遍历的节点
  context.currentNode = node
  // 转换器:transformElement、transformExpression、transformText、 
  // transformSlotOutlet...
  // transformElement负责整个节点层面的转换,transformExpression负责
  // 节点中表达式的转化,transformText负责节点中文本的转换,转换后会增加
  // 一堆表达式表述对象
  const { nodeTransforms } = context
  const exitFns = []
  // 依次调用指令转换工具
  for (let i = 0; i < nodeTransforms.length; i++) {
    // 转换器只负责生成onExit回调(具体可以看下面的transformElement),
    // onExit函数才是执行转换主逻辑的地方,为什么要推到栈中先不执行呢?
    // 因为要等到子节点都转换完成挂载gencodeNode后,也就是深度遍历完成后
    // 再执行当前节点栈中的onExit,这样保证了子节点的表达式全部生成完毕
    const onExit = nodeTransforms[i](node, context)
    
    if (onExit) {
      if (isArray(onExit)) {
        // v-if、v-for为结构化指令,其onExit是数组形式
        exitFns.push(...onExit)
      } else {
        exitFns.push(onExit)
      }
    }
    if (!context.currentNode) {
      // node was removed
      return
    } else {
      // node may have been replaced
      node = context.currentNode
    }
  }

  switch (node.type) {
    case NodeTypes.COMMENT:
      if (!context.ssr) {
        // inject import for the Comment symbol, which is needed for creating
        // comment nodes with `createVNode`
        context.helper(CREATE_COMMENT)
      }
      break
    case NodeTypes.INTERPOLATION:
      // no need to traverse, but we need to inject toString helper
      if (!context.ssr) {
        context.helper(TO_DISPLAY_STRING)
      }
      break

    // for container types, further traverse downwards
    case NodeTypes.IF:
      // 对v-if生成的节点束进行遍历
      for (let i = 0; i < node.branches.length; i++) {
        traverseNode(node.branches[i], context)
      }
      break
    case NodeTypes.IF_BRANCH:
    case NodeTypes.FOR:
    case NodeTypes.ELEMENT:
    case NodeTypes.ROOT:
      // 遍历子节点
      traverseChildren(node, context)
      break
  }

  // 当前节点树遍历完成,依次执行栈中的指令退出回调onExit
  let i = exitFns.length
  while (i--) {
    exitFns[i]()
  }
}

transformElement:
nodeTransform有很多种,如文本、表达式相关转换,此处仅对element进行讲解,其他类型的有兴趣可查阅源码。
transformElement对原生dom元素和组件类型的AST生成对应的VNODE_CALL接口,即gencodeNode,用于后续generate阶段中进行创建vnode调用。

const transformElement: NodeTransform = (node, context) => {
  // 组件和原生dom的type都属于element,tagType标示了具体的类型所属
  if (
    !(
      node.type === NodeTypes.ELEMENT &&
      (node.tagType === ElementTypes.ELEMENT ||
        node.tagType === ElementTypes.COMPONENT)
    )
  ) {
    return
  }
  // 返回闭包,也是真正执行transform逻辑生成gencodeNode的地方,返回闭包是为了
  // 透出到外部,由外部控制调用时机,等待子代表达式转换生成完毕后再执行当前AST节点
  // 转换
  return function postTransformElement() {
    const { tag, props /* parsed props */ } = node
    // 是否为组件
    const isComponent = node.tagType === ElementTypes.COMPONENT
    // 决定创建组件vnode时的tag值
    const vnodeTag = isComponent
      ? resolveComponentType(node as ComponentNode, context)
      : `"${tag}"`
    // 是否是动态组件,resolveComponentType生成的tag值为call表达式
    const isDynamicComponent =
      isObject(vnodeTag) && vnodeTag.callee === RESOLVE_DYNAMIC_COMPONENT

    let vnodeProps: VNodeCall['props']
    let vnodeChildren: VNodeCall['children']
    let vnodePatchFlag: VNodeCall['patchFlag']
    let patchFlag: number = 0
    let vnodeDynamicProps: VNodeCall['dynamicProps']
    let dynamicPropNames: string[] | undefined
    let vnodeDirectives: VNodeCall['directives']

    // 需要创建block的情况:动态组件(v-bind:is)、svg、动态key...
    let shouldUseBlock =
      // dynamic component may resolve to plain elements
      isDynamicComponent ||
      (!isComponent &&
        // <svg> and <foreignObject> must be forced into blocks so that block
        // updates inside get proper isSVG flag at runtime. (#639, #643)
        // This is technically web-specific, but splitting the logic out of core
        // leads to too much unnecessary complexity.
        (tag === 'svg' ||
          tag === 'foreignObject' ||
          // #938: elements with dynamic keys should be forced into blocks
          findProp(node, 'key', true)))

    // props转化
    if (props.length > 0) {
      // buildProps会在处理props过程中解析出vnode创建所需的表达式对象
      // 包括属性、patchflag、动态属性名称集合、指令集合
      // buildProps下面会重点介绍
      const propsBuildResult = buildProps(node, context)
      vnodeProps = propsBuildResult.props // ObjectExpressions属性集
      patchFlag = propsBuildResult.patchFlag // patchFlag
      dynamicPropNames = propsBuildResult.dynamicPropNames // 动态属性名称集合
      const directives = propsBuildResult.directives
      // 运行时指令,创建运行时指令的ArrayExpression
      vnodeDirectives =
        directives && directives.length
          ? (createArrayExpression(
              directives.map(dir => buildDirectiveArgs(dir, context))
            ) as DirectiveArguments)
          : undefined
    }

    // 对子节点children进行转化处理
    if (node.children.length > 0) {
      // keep-alive处理逻辑省略...

      const shouldBuildAsSlots =
        isComponent &&
        // Teleport is not a real component and has dedicated runtime handling
        vnodeTag !== TELEPORT &&
        // explained above.
        vnodeTag !== KEEP_ALIVE

      if (shouldBuildAsSlots) {
        const { slots, hasDynamicSlots } = buildSlots(node, context)
        vnodeChildren = slots
        if (hasDynamicSlots) {
          patchFlag |= PatchFlags.DYNAMIC_SLOTS
        }
      } else if (node.children.length === 1 && vnodeTag !== TELEPORT) {
        // 仅有一个子节点且不是teleport类型的情况
        const child = node.children[0]
        const type = child.type
        // 检查是否包含动态文本节点,即插值、复合表达式
        const hasDynamicTextChild =
          type === NodeTypes.INTERPOLATION ||
          type === NodeTypes.COMPOUND_EXPRESSION
        if (hasDynamicTextChild && !getStaticType(child)) {
          patchFlag |= PatchFlags.TEXT // 标记为动态文本
        }
        // 如果当前唯一的子节点是文本节点(插值类型、复合表达式类型、原生文本节点)
        // 直接讲该文本节点作为vnodeChildren
        if (hasDynamicTextChild || type === NodeTypes.TEXT) {
          vnodeChildren = child as TemplateTextChildNode
        } else {
          vnodeChildren = node.children
        }
      } else {
        // 多子节点情况直接拷贝其子节点作为vnodeChildren
        vnodeChildren = node.children
      }
    }

    // patchFlag & dynamicPropNames
    if (patchFlag !== 0) {
      if (__DEV__) {
        // 省略无关代码...
      } else {
        vnodePatchFlag = String(patchFlag)
      }
      if (dynamicPropNames && dynamicPropNames.length) {
		// 动态属性字符串化
        vnodeDynamicProps = stringifyDynamicPropNames(dynamicPropNames)
      }
    }

    // 最终生成的VNodeCall结构,和createVnode入参一样的名称,相信大家都熟悉
    node.codegenNode = createVNodeCall(
      context,
      vnodeTag,
      vnodeProps,
      vnodeChildren,
      vnodePatchFlag,
      vnodeDynamicProps,
      vnodeDirectives,
      !!shouldUseBlock,
      false /* disableTracking */,
      node.loc
    )
  }
}

buildProps:
分析属性得到对应的patchFlag信息、动态属性名称、运行时的指令、属性表达式,结构如下:

{
  props: PropsExpression | undefined // 属性表达式
  directives: DirectiveNode[] 运行时指令
  patchFlag: number
  dynamicPropNames: string[] // 动态属性集
}
function buildProps(
  node: ElementNode,
  context: TransformContext,
  props: ElementNode['props'] = node.props,
  ssr = false
): {
  props: PropsExpression | undefined
  directives: DirectiveNode[]
  patchFlag: number
  dynamicPropNames: string[]
} {
  const { tag, loc: elementLoc } = node
  // 节点是否是组件
  const isComponent = node.tagType === ElementTypes.COMPONENT
  // 存储属性表达式(key-value expression)
  let properties: ObjectExpression['properties'] = []
  const mergeArgs: PropsExpression[] = []
  // 运行时指令,如自定义指令(v-custom)
  const runtimeDirectives: DirectiveNode[] = []

  // patchFlag analysis
  let patchFlag = 0 // 解析生成的patchFlag
  let hasRef = false // 是否包含ref属性(ref节点不能静态提升)
  let hasClassBinding = false // 含有动态class属性(:class="")
  let hasStyleBinding = false // 含有动态样式(:style="")
  let hasHydrationEventBinding = false // ssr
  let hasDynamicKeys = false // 含有除ref、class、style外的动态属性
  const dynamicPropNames: string[] = []

  // 解析生成patchFlag
  const analyzePatchFlag = ({ key/* 属性真名表达式 */, value /* 属性值表达式 */ }: Property) => {
    if (key.type === NodeTypes.SIMPLE_EXPRESSION && key.isStatic) {
      // 值属性名称是静态的
      const name = key.content
      
      // 服务端渲染逻辑,代码省略...

      if (
        value.type === NodeTypes.JS_CACHE_EXPRESSION ||
        ((value.type === NodeTypes.SIMPLE_EXPRESSION ||
          value.type === NodeTypes.COMPOUND_EXPRESSION) &&
          getStaticType(value) > 0)
      ) {
        // 属性包含缓存handler或为常量,不做patchFlag分类
        return
      }

      // 走到此处署名属性值为动态(或ref),根据属性名称分类
      if (name === 'ref') {
        // 属性为ref
        hasRef = true
      } else if (name === 'class' && !isComponent) {
        // 属性为动态class
        hasClassBinding = true
      } else if (name === 'style' && !isComponent) {
        // 属性为动态style
        hasStyleBinding = true
      } else if (name !== 'key' && !dynamicPropNames.includes(name)) {
        // 除key属性外的动态属性,收集到dynamicPropNames数组中
        dynamicPropNames.push(name)
      }
    } else {
      // 属性真名为动态的,标示含有动态指令名
      hasDynamicKeys = true
    }
  }

  // 对props进行遍历,依次处理
  for (let i = 0; i < props.length; i++) {
    const prop = props[i]
    if (prop.type === NodeTypes.ATTRIBUTE) {
      // 静态属性处理
      const { loc, name /* 属性名 */, value /* 属性值 */ } = prop
      if (name === 'ref') {
        hasRef = true
      }
      // 跳过动态is属性,代码省略...
      
      // 收集key-value属性表达式
      properties.push(
        // 生成key-value形式的属性表达式,只不过key、value均是表达式类型
        createObjectProperty(
          // key表达式
          createSimpleExpression(
            name,
            true,
            getInnerRange(loc, 0, name.length)
          ),
          // value表达式
          createSimpleExpression(
            value ? value.content : '',
            true,
            value ? value.loc : loc
          )
        )
      )
    } else {
      // 指令属性处理
      const { 
      	name /* 类型 */, 
      	arg /* 属性真名表达式 */, 
      	exp /* 属性值表达式 */, 
      	loc 
      } = prop
      const isBind = name === 'bind' // :
      const isOn = name === 'on' // @

      // 跳过v-slot、v-once、v-is、ssr下的v-on处理,代码省略...

      // 特殊case,没有真名的指令,如v-bind="test"、v-on="test"
      if (!arg && (isBind || isOn)) {
        // 算包含动态属性的场景
        hasDynamicKeys = true
        if (exp) {
          if (properties.length) {
            mergeArgs.push(
              createObjectExpression(dedupeProperties(properties), elementLoc)
            )
            properties = []
          }
          if (isBind) {
            mergeArgs.push(exp)
          } else {
            // v-on="obj" -> toHandlers(obj)
            mergeArgs.push({
              type: NodeTypes.JS_CALL_EXPRESSION,
              loc,
              callee: context.helper(TO_HANDLERS),
              arguments: [exp]
            })
          }
        } else {
          // 错误处理,省略...
        }
        continue
      }

      // 获取指令对应的转换器函数
      const directiveTransform = context.directiveTransforms[name]
      if (directiveTransform) {
        // vue内置指令(v-if、v-for...)转换,后面会以v-bind为例
        // 讲一下对应的directiveTransform(transformBind)
        const { 
          // key-value属性表达式数组,通常数组中只有指令对应的一个key-value表达式
          props, 
          // 属性是否为运行时指令
          needRuntime
        } = directiveTransform(prop, node, context)
        // 分析指令prop对应的patchFlag
        !ssr && props.forEach(analyzePatchFlag)
        properties.push(...props)
        // 当前指令为运行时指令
        if (needRuntime) {
          runtimeDirectives.push(prop)
          if (isSymbol(needRuntime)) {
            directiveImportMap.set(prop, needRuntime)
          }
        }
      } else {
        // 指令找不到对应的转换器函数,说明是自定义指令,推到runtimeDirectives中
        runtimeDirectives.push(prop)
      }
    }
  }

  let propsExpression: PropsExpression | undefined = undefined

  if (mergeArgs.length) {
    //v-bind="object"  v-on="object"边界场景处理
    if (properties.length) {
      mergeArgs.push(
        createObjectExpression(dedupeProperties(properties), elementLoc)
      )
    }
    if (mergeArgs.length > 1) {
      propsExpression = createCallExpression(
        context.helper(MERGE_PROPS),
        mergeArgs,
        elementLoc
      )
    } else {
      // single v-bind with nothing else - no need for a mergeProps call
      propsExpression = mergeArgs[0]
    }
  } else if (properties.length) {
    // 正常props场景处理,生成对应的Object表达式对象,
    propsExpression = createObjectExpression(
      // dedupeProperties会对重复属性的值进行值的合并,变为一个属性
      // 合并后属性value变为ArrayExpression,比如:
      // 节点上写了多个class,dedupeProperties会把这个class的值
      // 合并到一个ArrayExpression里,作为一个属性来处理
      dedupeProperties(properties),
      elementLoc
    )
  }

  // 根据具体情况生成对应的patchFlag
  if (hasDynamicKeys) {
    // 含有动态属性名称
    patchFlag |= PatchFlags.FULL_PROPS
  } else {
    if (hasClassBinding) {
      // 有动态class
      patchFlag |= PatchFlags.CLASS
    }
    if (hasStyleBinding) {
      // 有动态style
      patchFlag |= PatchFlags.STYLE
    }
    if (dynamicPropNames.length) {
      patchFlag |= PatchFlags.PROPS
    }
    if (hasHydrationEventBinding) {
      patchFlag |= PatchFlags.HYDRATE_EVENTS
    }
  }
  if (
    (patchFlag === 0 || patchFlag === PatchFlags.HYDRATE_EVENTS) &&
    (hasRef || runtimeDirectives.length > 0)
  ) {
    patchFlag |= PatchFlags.NEED_PATCH
  }

  return {
    // parse阶段属性集转化后的属性集,是ObjectExpression类型,该表达式由key-value
    // 属性表达式构成
    props: propsExpression, 
    // 运行时的指令,编译阶段不做转换,仍然是parse阶段的值
    directives: runtimeDirectives, 
    patchFlag,
    dynamicPropNames // 动态属性名集合
  }
}

transformBind:
绑定类型的指令属性转换,输入parse props,输出key-value属性表达式数组,key、value分别对应属性的名称与值

const transformBind: DirectiveTransform = (dir/* parse生成的单个prop表达式对象 */, node, context) => {
  const { exp /* 属性值表达式 */, modifiers /* 修饰符 */, loc } = dir
  const arg = dir.arg! // 属性真名表达式
  if (!exp || (exp.type === NodeTypes.SIMPLE_EXPRESSION && !exp.content)) {
    context.onError(createCompilerError(ErrorCodes.X_V_BIND_NO_EXPRESSION, loc))
  }
  
  // 修饰符处理逻辑,省略...

  return {
    props: [
      // 创建key-value表达式对象,key是属性真名表达式,value是属性值表达式
      createObjectProperty(arg!, exp || createSimpleExpression('', true, loc))
    ]
  }
}

traverChildren:

function traverseChildren(
  parent: ParentNode,
  context: TransformContext
) {
  let i = 0
  const nodeRemoved = () => {
    i--
  }
  for (; i < parent.children.length; i++) {
    const child = parent.children[i]
    if (isString(child)) continue
    // 上下文记录节点间亲子关系
    context.parent = parent
    // 记录当前父节点正在访问的子节点index
    context.childIndex = i
    context.onNodeRemoved = nodeRemoved
    // 深度递归遍历
    traverseNode(child, context)
  }
}

hoistStatic:
hoistStatic内部实际调用的是walk

// transformContext上挂载的hoist方法,exp接受的是AST节点上的codegenNode属性,
// codegenNode其实本质上就是一个表达式对象
// hoist方法将原始gencodeNode推入上下文,并生成新的简单表达式对象gencodeNode,
// 该对象刻画提升后的变量标识符,并由外部将新gencodeNode挂载到原节点上
fucntion hoist(exp) {
  // 将codegenNode(exp)推入hoists数组,生成render时会添加到闭包外
  context.hoists.push(exp)
  // 生成简单表达式类型的标识符(vue的表达式系统包括很多种:简单表达式、符合表达式、
  // 函数表达式、call等等)
  // 该标识符其实就是在render函数里声明的变量标识符,如:
  // const _hoisted_1 = createVNode('div', {})
  const identifier = createSimpleExpression(
    `_hoisted_${context.hoists.length}`, // content:表达式内容,通常为变量名称
    false, // isStatic:是否是静态内容,动态会受外界变化的影响
    exp.loc, // loc:位置信息
    true // isConstant:标示是否是常量
  )
  identifier.hoisted = exp
  return identifier
}


// 遍历AST节点树查找需要做静态提升的内容
function walk(
  node: ParentNode,
  context: TransformContext,
  resultCache: Map<TemplateChildNode, StaticType>,
  // 这里需要注意下,如果模版中根节点的children只有一个节点,是不需要进行静态提升的
  doNotHoistNode: boolean = false
) {
  // 标记是否含有提升节点
  let hasHoistedNode = false
  // Some transforms, e.g. trasnformAssetUrls from @vue/compiler-sfc, replaces
  // static bindings with expressions. These expressions are guaranteed to be
  // constant so they are still eligible for hoisting, but they are only
  // available at runtime and therefore cannot be evaluated ahead of time.
  // This is only a concern for pre-stringification (via transformHoist by
  // @vue/compiler-dom), but doing it here allows us to perform only one full
  // walk of the AST and allow `stringifyStatic` to stop walking as soon as its
  // stringficiation threshold is met.
  // 标记是否含有运行时常量,比如这种case:
  // <div :class="`myClass`">{{ `test` }}</div>
  // class属性和插值虽然看似是动态的,但是其内容一直都不变
  // 和<div class="myClass">test</div>完全等效,这就是
  // 运行时常量,同样参与静态提升
  let hasRuntimeConstant = false

  const { children } = node
  for (let i = 0; i < children.length; i++) {
    const child = children[i]
    // 只有原生dom节点才会做静态提升,比如一些指令类型的节点(if、for)就不会做提升,
    // 而是作为block
    if (
      child.type === NodeTypes.ELEMENT &&
      child.tagType === ElementTypes.ELEMENT
    ) {
      // 处理静态节点提升的逻辑
      let staticType
      if (
        !doNotHoistNode &&
        // staticType为0表示不是静态节点,getStaticType会递归判断子树
        // 中是否有非静态内容,如果子树中全部节点均为静态,则整颗子树提升
        (staticType = getStaticType(child, resultCache)) > 0
      ) {
        if (staticType === StaticType.HAS_RUNTIME_CONSTANT) {
          hasRuntimeConstant = true
        }
        // 在codegenNode打上patchFlag,标示当前节点树为静态提升,
        // 运行时diff遇到静态节点将直接跳过
        ;(child.codegenNode as VNodeCall).patchFlag =
          PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
        // 把codegenNode推入context.hoists中,供生成render函数时使用,
        // 对应关系:静态节点提升 <-> 当前模版字符串相对应的context
        // 并将codegenNode替换成新生成的codegenNode
        // 注意:这里context.hoists中存储的是原始的codegenNode,
        // 因为存储时的指针是旧指针
        child.codegenNode = context.hoist(child.codegenNode!)
        // 此时child.codegenNode已指向新生成的identifier对象,
        // child.codegenNode的指针发生了变化
        hasHoistedNode = true
        continue
      } else {
        // 非静态节点,staticType为0,但是属性可能存在静态值,也需要做提升处理
        const codegenNode = child.codegenNode!
        if (codegenNode.type === NodeTypes.VNODE_CALL) {
          const flag = getPatchFlag(codegenNode)
          if (
            (!flag ||
              flag === PatchFlags.NEED_PATCH ||
              flag === PatchFlags.TEXT) &&
            // 这里需要注意一点,属性包含动态变化的key,或者含有ref属性时,节点不会被
            // 提升。原因如下:
            // 1⃣️动态key:动态变化的key表明该节点在diff时可能会被完全替换,顺便提下diff
            // 的一个点,vue3.0里节点的tag和key有一个不同就会被当作不同节点,从而
            // 完全替换掉。
            // 2⃣️ref:ref属性是因为即使写了一个形如`<div ref="test"></div>`的
            // 节点,但是假如你在setup里声明了有效的同名响应式数据,比如也叫test
            // 那么静态ref也会和这个响应数据关联起来
            !hasDynamicKeyOrRef(child) &&
            // 不能有缓存的属性
            !hasCachedProps(child)
          ) {
            // 上面的判断表示节点本身不包含children的情况下是静态的,即属性全部为静态
            const props = getNodeProps(child)
            if (props) {
              // 将节点的props提升到context中
              codegenNode.props = context.hoist(props)
            }
          }
        }
      }
    } else if (child.type === NodeTypes.TEXT_CALL) {
      const staticType = getStaticType(child.content, resultCache)
      if (staticType > 0) {
        if (staticType === StaticType.HAS_RUNTIME_CONSTANT) {
          hasRuntimeConstant = true
        }
        child.codegenNode = context.hoist(child.codegenNode)
        hasHoistedNode = true
      }
    }

    // 对子节点递归执行walk解析出静态提升内容
    if (child.type === NodeTypes.ELEMENT) {
      walk(child, context, resultCache)
    } else if (child.type === NodeTypes.FOR) {
      // 不提升v-for生成的单个节点,因为要把它作为一个block
      walk(child, context, resultCache, child.children.length === 1)
    } else if (child.type === NodeTypes.IF) {
      for (let i = 0; i < child.branches.length; i++) {
        // 不提升v-if生成的单个分支节点,因为要把它作为一个block
        walk(
          child.branches[i],
          context,
          resultCache,
          child.branches[i].children.length === 1
        )
      }
    }
  }

  if (!hasRuntimeConstant && hasHoistedNode && context.transformHoist) {
    context.transformHoist(children, context, node)
  }
}

createRootCodegen:
根据子节点的情况创建根节点的codegenNode,对于子节点为单节点的情况,需要创建block,多节点创建fragment block。
为什么根节点要创建block呢,原因是需要在根级block上挂载动态子代节点(dynamicChildren),在patch阶段做diff操作时忽略dom树层级,减少不必要的遍历成本,而rootNode是与组件template强相关的,这样就保证了diff一个组件时,会直接比较组件根block下的dynamicChildren,有效减少了组件这颗子树的遍历层级,最理想的情况下可以完全忽略根节点以下子树的遍历,想象是不是就酸爽了。

function createRootCodegen(root: RootNode, context: TransformContext) {
  const { helper } = context
  const { children } = root
  const child = children[0]
  if (children.length === 1) {
    // 如果children为单节点,将它转化为block
    if (isSingleElementRoot(root, child) && child.codegenNode) {
      // single element root is never hoisted so codegenNode will never be
      // SimpleExpressionNode
      // 单根节点不会被提升,而是作为block
      const codegenNode = child.codegenNode
      // VNODE_CALL表示codegen是要生成vnode的
      if (codegenNode.type === NodeTypes.VNODE_CALL) {
        codegenNode.isBlock = true
        // 推入openBlock、createBlock方法,后面generate生成创建代码要用到
        helper(OPEN_BLOCK)
        helper(CREATE_BLOCK)
      }
      root.codegenNode = codegenNode
    } else {
      // - single <slot/>, IfNode, ForNode: already blocks.
      // - single text node: always patched.
      // root codegen falls through via genNode()
      root.codegenNode = child
    }
  } else if (children.length > 1) {
    // 多根节点创建稳定fragment的block,并打上PATCH_FLAG
    // 多根节点无容器,因此需要创建fragment VNodeCall
    root.codegenNode = createVNodeCall(
      context,
      helper(FRAGMENT),
      undefined,
      root.children,
      `${PatchFlags.STABLE_FRAGMENT} /* ${
        PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
      } */`,
      undefined,
      undefined,
      true
    )
  } else {
    // no children = noop. codegen will return null.
  }
}

四、generate

codegenNode:
用于生成vnode,启用各种创建vnode的方法调用(createVnode、createBlock等),也就是我们最终在导入组件对象时里面的render函数,文末有关于compiler生成的render函数样例。
CodegenNode是一个复合类型,可以是TemplateChildNode也可以是JSChildNode,前者其实就是我们前面生成的AST节点,后者是表达式对象(如simpleExpression等)。
generate:

function generate(
  ast: RootNode,
  options: CodegenOptions = {}
): CodegenResult {
  // 创建生成器上下文
  const context = createCodegenContext(ast, options)
  const {
    mode,
    push,
    prefixIdentifiers,
    indent,
    deindent,
    newline,
    scopeId,
    ssr
  } = context
  const hasHelpers = ast.helpers.length > 0
  const useWithBlock = !prefixIdentifiers && mode !== 'module'
  const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module'

  // 生成执行函数的前序部分,主要是方法(如createVnode、createBlock等)的引入,
  // 和静态提升节点的声明
  if (!__BROWSER__ && mode === 'module') {
    genModulePreamble(ast, context, genScopeId)
  } else {
    // 浏览器环境生成函数的前序代码
    genFunctionPreamble(ast, context)
  }

  // enter render function
  if (!ssr) {
    // 非浏览器环境推入render函数
    if (genScopeId) {
      push(`const render = ${PURE_ANNOTATION}_withId(`)
    }
    push(`function render(_ctx, _cache) {`)
  } else {
    if (genScopeId) {
      push(`const ssrRender = ${PURE_ANNOTATION}_withId(`)
    }
    push(`function ssrRender(_ctx, _push, _parent, _attrs) {`)
  }
  indent()

  if (useWithBlock) {
    // 如果判断需要使用with函数,会将_ctx声明为with函数体内的上下文,这样
    // 在调用属性时会方便很多,比如不用with调用_ctx.test,启用with后直接
    // 调用test就可以了,不过with函数因为传入未知作用域context,导致js
    // 预编译阶段不会提前确定好各个相应声明的所属位置,因此运行时会慢一些,
    // 因为要花一些时间查找声明所属位置
    push(`with (_ctx) {`)
    indent()
    // 在with函数体内导入节点创建方法,并重命名,防止和用户自定义属性冲突
    if (hasHelpers) {
      push(
        `const { ${ast.helpers
          .map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
          .join(', ')} } = _Vue`
      )
      push(`\n`)
      newline()
    }
  }

  // generate asset resolution statements
  if (ast.components.length) {
    genAssets(ast.components, 'component', context)
    if (ast.directives.length || ast.temps > 0) {
      newline()
    }
  }
  if (ast.directives.length) {
    genAssets(ast.directives, 'directive', context)
    if (ast.temps > 0) {
      newline()
    }
  }
  if (ast.temps > 0) {
    push(`let `)
    for (let i = 0; i < ast.temps; i++) {
      push(`${i > 0 ? `, ` : ``}_temp${i}`)
    }
  }
  if (ast.components.length || ast.directives.length || ast.temps) {
    push(`\n`)
    newline()
  }

  // generate the VNode tree expression
  if (!ssr) {
    push(`return `)
  }
  if (ast.codegenNode) {
    // 根据rootNode.codegenNode生成创建Vnode的函数调用
    genNode(ast.codegenNode, context)
  } else {
    push(`null`)
  }

  if (useWithBlock) {
    deindent()
    push(`}`)
  }

  deindent()
  push(`}`)

  if (genScopeId) {
    push(`)`)
  }

  return {
    ast,
    code: context.code,
    // SourceMapGenerator does have toJSON() method but it's not in the types
    map: context.map ? (context.map as any).toJSON() : undefined
  }
}

genFunctionPreamble:
生成函数的前序部分,主要包括createVnode、createBlock等节点创建方法的引入,和静态节点提升的声明

function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
  const {
    ssr,
    prefixIdentifiers,
    push,
    newline,
    runtimeModuleName,
    runtimeGlobalName
  } = context
  // vue模块导入
  const VueBinding =
    !__BROWSER__ && ssr
      ? `require(${JSON.stringify(runtimeModuleName)})`
      : runtimeGlobalName
  // 解构重命名alias
  const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}`
  // Generate const declaration for helpers
  // In prefix mode, we place the const declaration at top so it's done
  // only once; But if we not prefixing, we place the declaration inside the
  // with block so it doesn't incur the `in` check cost for every helper access.
  // AST节点的helpers主要包含createVnode等节点创建方法名,用于下面的解构批量导入对应方法
  if (ast.helpers.length > 0) {
    if (!__BROWSER__ && prefixIdentifiers) {
      push(
        `const { ${ast.helpers.map(aliasHelper).join(', ')} } = ${VueBinding}\n`
      )
    } else {
      // "with" mode.
      // save Vue in a separate variable to avoid collision
      push(`const _Vue = ${VueBinding}\n`)
      // in "with" mode, helpers are declared inside the with block to avoid
      // has check cost, but hoists are lifted out of the function - we need
      // to provide the helper here.
      // AST中存在提升的静态节点时,导入静态节点创建的方法们
      if (ast.hoists.length) {
        const staticHelpers = [
          CREATE_VNODE,
          CREATE_COMMENT,
          CREATE_TEXT,
          CREATE_STATIC
        ]
          .filter(helper => ast.helpers.includes(helper))
          .map(aliasHelper)
          .join(', ')
        push(`const { ${staticHelpers} } = _Vue\n`)
      }
    }
  }
  // 从@vue/server-renderer模块导入服务端渲染的对应创建方法
  if (!__BROWSER__ && ast.ssrHelpers && ast.ssrHelpers.length) {
    // ssr guaruntees prefixIdentifier: true
    push(
      `const { ${ast.ssrHelpers
        .map(aliasHelper)
        .join(', ')} } = require("@vue/server-renderer")\n`
    )
  }
  // 生成静态节点提升对应的代码
  genHoists(ast.hoists, context)
  newline()
  // return后接render函数
  push(`return `)
}

genHoists:
生成静态节点提升声明,形如const _hoisted_${i + 1} = ...这样的节点创建声明

function genHoists(hoists: (JSChildNode | null)[], context: CodegenContext) {
  if (!hoists.length) {
    return
  }
  context.pure = true
  const { push, newline, helper, scopeId, mode } = context
  const genScopeId = !__BROWSER__ && scopeId != null && mode !== 'function'
  newline()

  // 非浏览器环境push scopeId相关,省略代码...

  // 生成被提升静态节点的核心逻辑,具体的创建调用了genNode函数
  // genNode是真正创建vnode的地方
  hoists.forEach((exp, i) => {
    if (exp) {
      push(`const _hoisted_${i + 1} = `)
      genNode(exp, context)
      newline()
    }
  })

  // 非浏览器环境pop scopeId相关,省略代码...
  context.pure = false
}

genNode:
根据codegenNode生成对应创建代码,根据codegenNode的类型执行对应的代码生成逻辑,重点关注VNODE_CALL类型的节点,我们在render函数中调用createBlock、createVnode的相关代码都是通过VNODE_CALL类型节点来生成的。
注意:genNode并非只用来生成dom节点级别的创建代码,而是面向一切CodegenNode类型的节点,比如可以是transform后的props、dynamicProps等表达式对象,transform生成的信息绝大多数都是以表达式对象的形式来表述的。
比如genVNodeCall,在对节点级别生成创建代码过程中,会对VNodeCall对象中的各表达式信息(比如props等)调用genNode进行代码生成,其实是一个层层递归的生成过程:
genNode(节点级别) -> genVNodeCall -> genNode(属性级别)

// 入参的gencodeNode可能是表达式对象类型,也可能是模版节点类型
function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
  if (isString(node)) {
    context.push(node)
    return
  }
  if (isSymbol(node)) {
    context.push(context.helper(node))
    return
  }
  switch (node.type) {
    /**
    * 模版节点类型
    */
    case NodeTypes.ELEMENT:
    case NodeTypes.IF:
    case NodeTypes.FOR:
      // 省略无关逻辑...
      genNode(node.codegenNode!, context)
      break
    case NodeTypes.TEXT:
      genText(node, context)
      break

    /**
    * 表达式类型,这类居多
    */
    case NodeTypes.SIMPLE_EXPRESSION:
      genExpression(node, context)
      break
    case NodeTypes.INTERPOLATION:
      genInterpolation(node, context)
      break
    case NodeTypes.TEXT_CALL:
      genNode(node.codegenNode, context)
      break
    case NodeTypes.COMPOUND_EXPRESSION:
      genCompoundExpression(node, context)
      break
    case NodeTypes.COMMENT:
      genComment(node, context)
      break
    case NodeTypes.VNODE_CALL:
      // 生成VNodeCall,最常遇到的就是这种
      genVNodeCall(node, context)
      break

    case NodeTypes.JS_CALL_EXPRESSION:
      genCallExpression(node, context)
      break
    case NodeTypes.JS_OBJECT_EXPRESSION:
      genObjectExpression(node, context)
      break
    case NodeTypes.JS_ARRAY_EXPRESSION:
      genArrayExpression(node, context)
      break
    case NodeTypes.JS_FUNCTION_EXPRESSION:
      genFunctionExpression(node, context)
      break
    case NodeTypes.JS_CONDITIONAL_EXPRESSION:
      genConditionalExpression(node, context)
      break
    case NodeTypes.JS_CACHE_EXPRESSION:
      genCacheExpression(node, context)
      break

    // 服务端渲染相关逻辑省略...

    /* istanbul ignore next */
    case NodeTypes.IF_BRANCH:
      // noop
      break
    default:
      // 省略无关代码...
  }
}

genVNodeCall:
生成类似withDirectives((openBlock(true), createBlock(...args)), directivesObj)这样的代码,即render函数中的Vnode核心创建逻辑代码,和我们手写render类似。

function genVNodeCall(node: VNodeCall, context: CodegenContext) {
  const { push, helper, pure } = context
  const {
    tag,
    props,
    children,
    patchFlag,
    dynamicProps,
    directives,
    isBlock,
    disableTracking
  } = node
  // 是否有自定义指令
  if (directives) {
    push(helper(WITH_DIRECTIVES) + `(`)
  }
  // 是否创建block
  if (isBlock) {
    push(`(${helper(OPEN_BLOCK)}(${disableTracking ? `true` : ``}), `)
  }
  if (pure) {
    push(PURE_ANNOTATION)
  }
  // 决定是创建block还是普通vnode
  push(helper(isBlock ? CREATE_BLOCK : CREATE_VNODE) + `(`, node)
  // 将tag、props、children等gencodeNode转化为真正的可渲染node,即我们手写render函数时
  // 传入的props、children...
  genNodeList(
  	// 非空参数数组,tag, props...这个时候还是codegenNode表达式对象
    genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
    context
  )
  push(`)`)
  if (isBlock) {
    push(`)`)
  }
  if (directives) {
    push(`, `)
    genNode(directives, context)
    push(`)`)
  }
}

genNodeList:
将createVNode所需的参数(tag、props、children and so on)从gencodeNode形式转化为实际调用时的入参形式。

function genNodeList(
  // nodes接受的是tag、props、children这些值对应的表达式对象(gencodeNode)
  nodes: (string | symbol | CodegenNode | TemplateChildNode[])[],
  context: CodegenContext,
  multilines: boolean = false,
  comma: boolean = true
) {
  const { push, newline } = context
  // 将gencodeNode依次生成对应的最终节点,即我们createVnode时的入参
  for (let i = 0; i < nodes.length; i++) {
    const node = nodes[i]
    if (isString(node)) {
      push(node)
    } else if (isArray(node)) {
      genNodeListAsArray(node, context)
    } else {
      genNode(node, context)
    }
    if (i < nodes.length - 1) {
      if (multilines) {
        comma && push(',')
        newline()
      } else {
        comma && push(', ')
      }
    }
  }
}

五、示例

对于下面的模版

<template>
  <div id="app">
    <div id="nav">
      <p @click="handleAdd">count: {{ count }}</p>
      <p>{{ bigCount }}</p>
    </div>
  </div>
</template>

编译器生成的render函数如下方代码所示:
_hoisted_${I}是被提升的静态属性,由于render函数在编译时是以闭包的形式生成的,因此这些提升的变量声明在闭包外的函数体中。

function render(_ctx, _cache) {
    return (
        openBlock(), 
        createBlock(
            "div", 
            _hoisted_1, 
            [createVNode("div", _hoisted_2, [
                createVNode("p", {
	                onClick: _cache[1] || (_cache[1] = function ($event) {
	                    for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
	                        args[_key - 1] = arguments[_key];
	                    }
	
	                    return _ctx.handleAdd.apply(
	                    	_ctx, [$event].concat(args));
	                    })
	                }, 
	                "count: " + toDisplayString(_ctx.count), 1
	                /* TEXT */
                ), 
                createVNode("p", null, toDisplayString(_ctx.bigCount), 1
                /* TEXT */
                )]
            )]
        )
    );
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值