🚀🚀🚀Vue 是怎样对AST进行静态分析的?🔥🔥🔥

前言

作者简介:Quixn,专注于 Node.js 技术栈分享,前端从 JavaScript 到 Node.js,再到后端数据库,优质文章推荐,【小 Q 全栈指南】作者,Github 博客开源项目 github.com/Quixn...

大家好,我是 Quixn。今天简单聊聊 Vue 是怎样对AST进行静态分析的?。也是对此前的"小白都能看懂的 Vue 渲染过程"一文的补充。本文分为两大部分:简要概括分析(比较通俗易懂,小白都能看懂)和源码阅读(比较枯燥,适合有一定基础的同学)。文章较长,文末有精简版总结。

Vue 是怎样对 AST 进行静态分析的?

想要知道Vue是怎样对AST进行静态分析的,那么我们需要先知道一个前置条件,AST节点又是怎么来的?下面我面先简要介绍一下Vue是如何根据模板代码生成的AST节点的,然后再分两部分介绍 Vue 是怎样对AST进行静态分析的。

Vue 是如何根据模板代码生成的 AST 节点的?

Vue 根据模板代码生成 AST 节点的过程主要涉及词法分析和语法分析两个步骤。下面是详细的步骤解释:

  • 词法分析(Lexical Analysis):Vue 首先将模板代码作为输入,通过词法分析将其拆解成一个个的标记(Tokens)。词法分析器会按照一定的规则扫描模板代码,根据遇到的字符和符号,识别出不同的标记。

  • 语法分析(Syntax Analysis):在词法分析的基础上,Vue 使用语法分析器(Parser)将标记序列转换为抽象语法树(AST)。语法分析器会根据预定义的语法规则,分析标记之间的关系和结构,并构建相应的 AST 节点。

下面是一个简单示例,演示 Vue 如何根据模板代码生成 AST 节点的过程:

js 复制代码
<template>
  <div>
    <h1>{{ title }}</h1>
    <p v-if="showMessage">{{ message }}</p>
    <button @click="handleAction">小Q全栈指南</button>
  </div>
</template>

在以上示例中,Vue 会进行以下步骤来生成对应的 AST 节点:

词法分析:Vue 会通过词法分析器将模板代码拆解成一系列的标记,如下所示:

js 复制代码
[
  { type: 'tag-start', value: 'div' },
  { type: 'tag-end', value: 'div' },
  { type: 'tag-start', value: 'h1' },
  { type: 'mustache', value: 'title' },
  { type: 'tag-end', value: 'h1' },
  { type: 'tag-start', value: 'p' },
  { type: 'directive', value: 'v-if="showMessage"' },
  { type: 'mustache', value: 'message' },
  { type: 'tag-end', value: 'p' },
  { type: 'tag-start', value: 'button' },
  { type: 'event', value: '@click="handleAction"' },
  { type: 'text', value: '小Q全栈指南' },
  { type: 'tag-end', value: 'button' },
  { type: 'tag-start', value: '/div' },
  { type: 'tag-end', value: '/div' }
]

语法分析:Vue 的语法分析器会根据定义好的语法规则,将上述标记序列转换为 AST 节点。最终生成的抽象语法树如下所示:

js 复制代码
{
  type: 'root',
  children: [
    {
      type: 'element',
      tag: 'div',
      children: [
        {
          type: 'element',
          tag: 'h1',
          children: [
            {
              type: 'mustache',
              expression: 'title'
            }
          ]
        },
        {
          type: 'element',
          tag: 'p',
          directives: [
            {
              type: 'directive',
              name: 'if',
              value: 'showMessage'
            }
          ],
          children: [
            {
              type: 'mustache',
              expression: 'message'
            }
          ]
        },
        {
          type: 'element',
          tag: 'button',
          events: [
            {
              type: 'event',
              name: 'click',
              value: 'handleAction'
            }
          ],
          children: [
            {
              type: 'text',
              content: '小Q全栈指南'
            }
          ]
        }
      ]
    }
  ]
}

通过以上步骤,Vue成功将模板代码转换为了对应的AST节点。AST 节点将成为后续编译过程的基础,用于生成渲染函数以及进行其他的静态分析和优化操作。

简要概括分析

在Vue的编译过程中,它会对抽象语法树(AST)进行静态分析。这意味着Vue会通过分析AST的结构和节点信息来提前了解模板中的各种情况,并作出相应的优化。

Vue在AST静态分析中的一些处理方式:

静态节点标记:Vue 会遍历 AST,标记那些不需要动态更新的节点(即静态节点)。静态节点意味着其内容不会随数据变化而改变,可以在编译阶段直接生成静态的 DOM 片段,以提高性能。

静态属性提升:Vue 会将那些被所有实例共享且不会改变的属性提升到编译阶段,避免每个实例都创建相同的属性。

条件判断优化:Vue 会检测模板中的条件判断语句,尽可能地将静态的条件块提取出来,以减少运行时的条件判断次数。

下面是一个简单的示例,用于说明Vue对AST的静态分析过程:

js 复制代码
<template>
  <div>
    <h1>{{ title }}</h1>
    <p v-if="showMessage">{{ message }}</p>
    <button @click="handleAction">小Q全栈指南</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      title: 'Vue静态分析示例',
      showMessage: true,
      message: 'Hello, Vue!'
    };
  },
  methods: {
    handleAction() {
      this.showMessage = !this.showMessage;
    }
  }
}
</script>

在上述示例中,Vue 会进行以下静态分析操作:

  • 1、遍历模板语法树,标记<h1>{{ title }}</h1>为静态节点,因为title是一个不变的数据。
  • 2、检测到v-if="showMessage",Vue 会优化条件判断,将其提取出来,以避免每次渲染都重新计算条件。
  • 3、解析@click="handleAction",生成相应的事件处理函数。

通过以上静态分析过程,Vue 能够更好地理解模板并进行相应的优化,从而提高渲染性能和效率。

但是,具体的静态分析实现细节可能在不同版本的 Vue 中有所差异, 在 Vue 2.x 中,静态分析主要依赖于 Vue 的编译器vue-template-compile来进行。而在 Vue 3.x 中,引入了基于Proxy的响应式系统,对模板编译和渲染管道进行了全面重写,以提供更好的性能和更小的包体积。

源码阅读

Vue 的源码是通过编译器将模板代码转换成抽象语法树(AST)节点的。

具体来说,Vue 的编译过程主要包括以下几个步骤:

  1. 解析:编译器会先对模板代码进行解析,将其转换为一个初始的 AST。这个过程使用了HTML解析器和文本解析器,通过遍历模板代码的字符来构建初始的 AST。

  2. 优化:编译器会对初始的 AST 进行一些优化处理,例如静态节点的标记、静态属性的提升等。这些优化可以减少运行时的性能开销。

  3. 代码生成:在完成优化后,编译器会将AST转换为可执行的渲染函数。在这个过程中,编译器会遍历 AST 节点,并根据节点的类型生成相应的代码片段。例如,对于元素节点,编译器会生成创建元素的代码;对于文本节点,编译器会生成插入文本的代码。

下面是 Vue2 源码中涉及到 AST 生成的相关文件和函数:

以上是 Vue 源码中与 AST 生成相关的文件和函数,通过这些模块的协作,Vue 的编译器能够将模板代码转换为可执行的渲染函数。

compiler/index.js源码:

ts 复制代码
import { parse } from './parser/index'
import { optimize } from './optimizer'
import { generate } from './codegen/index'
import { createCompilerCreator } from './create-compiler'
import { CompilerOptions, CompiledResult } from 'types/compiler'

// `createCompilerCreator` allows creating compilers that use alternative
// parser/optimizer/codegen, e.g the SSR optimizing compiler.
// Here we just export a default compiler using the default parts.
export const createCompiler = createCompilerCreator(function baseCompile(
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

在这段代码中,通过调用 createCompilerCreator 函数创建了一个编译器 baseCompile。该编译器接受一个模板字符串和编译选项作为参数,并返回编译结果对象 CompiledResult。具体的编译过程如下:

  1. 首先,使用 parse 函数将模板字符串解析成 AST,去除首尾空格。
  2. 如果编译选项中的 optimize 属性值不为 false,则调用 optimize 函数对 AST 进行优化处理。
  3. 使用 generate 函数将优化后的 AST 生成可执行的 JavaScript 代码。
  4. 最后,将生成的 AST、渲染函数和静态渲染函数作为属性存储在编译结果对象中,并返回该对象。

这段代码的目的是创建一个基础的编译器,它使用默认的解析器、优化器和代码生成器。可以根据需要,使用 createCompilerCreator 函数创建其他类型的编译器,例如用于 SSR(服务器端渲染)的优化编译器。

compiler/create-compiler源码:

ts 复制代码
import { extend } from 'shared/util'
import { CompilerOptions, CompiledResult, WarningMessage } from 'types/compiler'
import { detectErrors } from './error-detector'
import { createCompileToFunctionFn } from './to-function'

export function createCompilerCreator(baseCompile: Function): Function {
  return function createCompiler(baseOptions: CompilerOptions) {
    function compile(
      template: string,
      options?: CompilerOptions
    ): CompiledResult {
      const finalOptions = Object.create(baseOptions)
      const errors: WarningMessage[] = []
      const tips: WarningMessage[] = []

      let warn = (
        msg: WarningMessage,
        range: { start: number; end: number },
        tip: string
      ) => {
        ;(tip ? tips : errors).push(msg)
      }

      if (options) {
        if (__DEV__ && options.outputSourceRange) {
          // $flow-disable-line
          const leadingSpaceLength = template.match(/^\s*/)![0].length

          warn = (
            msg: WarningMessage | string,
            range: { start: number; end: number },
            tip: string
          ) => {
            const data: WarningMessage = typeof msg === 'string' ? { msg } : msg
            if (range) {
              if (range.start != null) {
                data.start = range.start + leadingSpaceLength
              }
              if (range.end != null) {
                data.end = range.end + leadingSpaceLength
              }
            }
            ;(tip ? tips : errors).push(data)
          }
        }
        // merge custom modules
        if (options.modules) {
          finalOptions.modules = (baseOptions.modules || []).concat(
            options.modules
          )
        }
        // merge custom directives
        if (options.directives) {
          finalOptions.directives = extend(
            Object.create(baseOptions.directives || null),
            options.directives
          )
        }
        // copy other options
        for (const key in options) {
          if (key !== 'modules' && key !== 'directives') {
            finalOptions[key] = options[key as keyof CompilerOptions]
          }
        }
      }

      finalOptions.warn = warn

      const compiled = baseCompile(template.trim(), finalOptions)
      if (__DEV__) {
        detectErrors(compiled.ast, warn)
      }
      compiled.errors = errors
      compiled.tips = tips
      return compiled
    }

    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}

代码逻辑:

它导出了一个函数 createCompilerCreator 和一个对象。

函数 createCompilerCreator 接受一个参数 baseCompile ,并返回一个函数。返回的函数接受一个参数 baseOptions ,并返回一个对象。

返回的对象包含两个方法: compile 和 compileToFunctions 。

compile 方法接受两个参数: template 和 options ,并返回一个编译结果对象。在这个方法中,首先创建了一个 finalOptions 对象,作为编译选项的基础。然后创建了两个空数组 errors 和 tips ,用于存储警告信息。接下来定义了一个 warn 函数,用于将警告信息推入对应的数组中。

如果传入了 options 参数,会根据不同的选项进行处理。首先判断是否开启了源码范围输出,如果开启了,会计算模板的前导空格长度,并重写 warn 函数,将警告信息的范围进行调整后再推入数组中。然后,会合并自定义的模块和指令到 finalOptions 中,并将其他选项复制到 finalOptions 中。

最后,将 warn 函数赋值给 finalOptions 的 warn 属性,调用 baseCompile 方法对模板进行编译,并将编译结果赋值给 compiled 。在开发环境下,还会调用 detectErrors 方法检测编译结果中的错误,并将错误信息推入 errors 数组中。最后,将 errors 和 tips 数组赋值给 compiled 的 errors 和 tips 属性,并返回 compiled 。

compileToFunctions 方法调用了 createCompileToFunctionFn 函数,并将 compile 方法作为参数传入,返回一个编译为函数的方法。

这段代码的作用是:

创建一个编译器,用于将模板编译为可执行的函数。它接受一个基础编译函数 baseCompile 和编译选项 baseOptions ,并返回一个包含 compile 和 compileToFunctions 方法的对象。

compile 方法接受模板和可选的编译选项作为参数,将模板编译为一个包含编译结果的对象。在编译过程中,它会处理选项,合并自定义模块和指令,以及处理警告信息。最后,它将编译结果返回。

compileToFunctions 方法则是将 compile 方法进行进一步封装,将编译结果转换为可执行的函数形式。

总的来说,这段代码实现了一个编译器的创建和模板编译的功能。

compiler/parser/index.js源码:

ts 复制代码
import he from 'he'
import { parseHTML } from './html-parser'
import { parseText } from './text-parser'
import { parseFilters } from './filter-parser'
import { genAssignmentCode } from '../directives/model'
import { extend, cached, no, camelize, hyphenate } from 'shared/util'
import { isIE, isEdge, isServerRendering } from 'core/util/env'

import {
  addProp,
  addAttr,
  baseWarn,
  addHandler,
  addDirective,
  getBindingAttr,
  getAndRemoveAttr,
  getRawBindingAttr,
  pluckModuleFunction,
  getAndRemoveAttrByRegex
} from '../helpers'

import {
  ASTAttr,
  ASTElement,
  ASTIfCondition,
  ASTNode,
  ASTText,
  CompilerOptions
} from 'types/compiler'

export const onRE = /^@|^v-on:/
export const dirRE = process.env.VBIND_PROP_SHORTHAND
  ? /^v-|^@|^:|^\.|^#/
  : /^v-|^@|^:|^#/
export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g
const dynamicArgRE = /^\[.*\]$/

const argRE = /:(.*)$/
export const bindRE = /^:|^\.|^v-bind:/
const propBindRE = /^\./
const modifierRE = /\.[^.\]]+(?=[^\]]*$)/g

export const slotRE = /^v-slot(:|$)|^#/

const lineBreakRE = /[\r\n]/
const whitespaceRE = /[ \f\t\r\n]+/g

const invalidAttributeRE = /[\s"'<>\/=]/

const decodeHTMLCached = cached(he.decode)

export const emptySlotScopeToken = `_empty_`

// configurable state
export let warn: any
let delimiters
let transforms
let preTransforms
let postTransforms
let platformIsPreTag
let platformMustUseProp
let platformGetTagNamespace
let maybeComponent

export function createASTElement(
  tag: string,
  attrs: Array<ASTAttr>,
  parent: ASTElement | void
): ASTElement {
  return {
    type: 1,
    tag,
    attrsList: attrs,
    attrsMap: makeAttrsMap(attrs),
    rawAttrsMap: {},
    parent,
    children: []
  }
}

/**
 * Convert HTML string to AST.
 */
export function parse(template: string, options: CompilerOptions): ASTElement {
  warn = options.warn || baseWarn

  platformIsPreTag = options.isPreTag || no
  platformMustUseProp = options.mustUseProp || no
  platformGetTagNamespace = options.getTagNamespace || no
  const isReservedTag = options.isReservedTag || no
  maybeComponent = (el: ASTElement) =>
    !!(
      el.component ||
      el.attrsMap[':is'] ||
      el.attrsMap['v-bind:is'] ||
      !(el.attrsMap.is ? isReservedTag(el.attrsMap.is) : isReservedTag(el.tag))
    )
  transforms = pluckModuleFunction(options.modules, 'transformNode')
  preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
  postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')

  delimiters = options.delimiters

  const stack: any[] = []
  const preserveWhitespace = options.preserveWhitespace !== false
  const whitespaceOption = options.whitespace
  let root
  let currentParent
  let inVPre = false
  let inPre = false
  let warned = false

  function warnOnce(msg, range) {
    if (!warned) {
      warned = true
      warn(msg, range)
    }
  }

  function closeElement(element) {
    trimEndingWhitespace(element)
    if (!inVPre && !element.processed) {
      element = processElement(element, options)
    }
    // tree management
    if (!stack.length && element !== root) {
      // allow root elements with v-if, v-else-if and v-else
      if (root.if && (element.elseif || element.else)) {
        if (__DEV__) {
          checkRootConstraints(element)
        }
        addIfCondition(root, {
          exp: element.elseif,
          block: element
        })
      } else if (__DEV__) {
        warnOnce(
          `Component template should contain exactly one root element. ` +
            `If you are using v-if on multiple elements, ` +
            `use v-else-if to chain them instead.`,
          { start: element.start }
        )
      }
    }
    if (currentParent && !element.forbidden) {
      if (element.elseif || element.else) {
        processIfConditions(element, currentParent)
      } else {
        if (element.slotScope) {
          // scoped slot
          // keep it in the children list so that v-else(-if) conditions can
          // find it as the prev node.
          const name = element.slotTarget || '"default"'
          ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[
            name
          ] = element
        }
        currentParent.children.push(element)
        element.parent = currentParent
      }
    }

    // final children cleanup
    // filter out scoped slots
    element.children = element.children.filter(c => !c.slotScope)
    // remove trailing whitespace node again
    trimEndingWhitespace(element)

    // check pre state
    if (element.pre) {
      inVPre = false
    }
    if (platformIsPreTag(element.tag)) {
      inPre = false
    }
    // apply post-transforms
    for (let i = 0; i < postTransforms.length; i++) {
      postTransforms[i](element, options)
    }
  }

  function trimEndingWhitespace(el) {
    // remove trailing whitespace node
    if (!inPre) {
      let lastNode
      while (
        (lastNode = el.children[el.children.length - 1]) &&
        lastNode.type === 3 &&
        lastNode.text === ' '
      ) {
        el.children.pop()
      }
    }
  }

  function checkRootConstraints(el) {
    if (el.tag === 'slot' || el.tag === 'template') {
      warnOnce(
        `Cannot use <${el.tag}> as component root element because it may ` +
          'contain multiple nodes.',
        { start: el.start }
      )
    }
    if (el.attrsMap.hasOwnProperty('v-for')) {
      warnOnce(
        'Cannot use v-for on stateful component root element because ' +
          'it renders multiple elements.',
        el.rawAttrsMap['v-for']
      )
    }
  }

  parseHTML(template, {
    warn,
    expectHTML: options.expectHTML,
    isUnaryTag: options.isUnaryTag,
    canBeLeftOpenTag: options.canBeLeftOpenTag,
    shouldDecodeNewlines: options.shouldDecodeNewlines,
    shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
    shouldKeepComment: options.comments,
    outputSourceRange: options.outputSourceRange,
    start(tag, attrs, unary, start, end) {
      // check namespace.
      // inherit parent ns if there is one
      const ns =
        (currentParent && currentParent.ns) || platformGetTagNamespace(tag)

      // handle IE svg bug
      /* istanbul ignore if */
      if (isIE && ns === 'svg') {
        attrs = guardIESVGBug(attrs)
      }

      let element: ASTElement = createASTElement(tag, attrs, currentParent)
      if (ns) {
        element.ns = ns
      }

      if (__DEV__) {
        if (options.outputSourceRange) {
          element.start = start
          element.end = end
          element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => {
            cumulated[attr.name] = attr
            return cumulated
          }, {})
        }
        attrs.forEach(attr => {
          if (invalidAttributeRE.test(attr.name)) {
            warn(
              `Invalid dynamic argument expression: attribute names cannot contain ` +
                `spaces, quotes, <, >, / or =.`,
              options.outputSourceRange
                ? {
                    start: attr.start! + attr.name.indexOf(`[`),
                    end: attr.start! + attr.name.length
                  }
                : undefined
            )
          }
        })
      }

      if (isForbiddenTag(element) && !isServerRendering()) {
        element.forbidden = true
        __DEV__ &&
          warn(
            'Templates should only be responsible for mapping the state to the ' +
              'UI. Avoid placing tags with side-effects in your templates, such as ' +
              `<${tag}>` +
              ', as they will not be parsed.',
            { start: element.start }
          )
      }

      // apply pre-transforms
      for (let i = 0; i < preTransforms.length; i++) {
        element = preTransforms[i](element, options) || element
      }

      if (!inVPre) {
        processPre(element)
        if (element.pre) {
          inVPre = true
        }
      }
      if (platformIsPreTag(element.tag)) {
        inPre = true
      }
      if (inVPre) {
        processRawAttrs(element)
      } else if (!element.processed) {
        // structural directives
        processFor(element)
        processIf(element)
        processOnce(element)
      }

      if (!root) {
        root = element
        if (__DEV__) {
          checkRootConstraints(root)
        }
      }

      if (!unary) {
        currentParent = element
        stack.push(element)
      } else {
        closeElement(element)
      }
    },

    end(tag, start, end) {
      const element = stack[stack.length - 1]
      // pop stack
      stack.length -= 1
      currentParent = stack[stack.length - 1]
      if (__DEV__ && options.outputSourceRange) {
        element.end = end
      }
      closeElement(element)
    },

    chars(text: string, start?: number, end?: number) {
      if (!currentParent) {
        if (__DEV__) {
          if (text === template) {
            warnOnce(
              'Component template requires a root element, rather than just text.',
              { start }
            )
          } else if ((text = text.trim())) {
            warnOnce(`text "${text}" outside root element will be ignored.`, {
              start
            })
          }
        }
        return
      }
      // IE textarea placeholder bug
      /* istanbul ignore if */
      if (
        isIE &&
        currentParent.tag === 'textarea' &&
        currentParent.attrsMap.placeholder === text
      ) {
        return
      }
      const children = currentParent.children
      if (inPre || text.trim()) {
        text = isTextTag(currentParent)
          ? text
          : (decodeHTMLCached(text) as string)
      } else if (!children.length) {
        // remove the whitespace-only node right after an opening tag
        text = ''
      } else if (whitespaceOption) {
        if (whitespaceOption === 'condense') {
          // in condense mode, remove the whitespace node if it contains
          // line break, otherwise condense to a single space
          text = lineBreakRE.test(text) ? '' : ' '
        } else {
          text = ' '
        }
      } else {
        text = preserveWhitespace ? ' ' : ''
      }
      if (text) {
        if (!inPre && whitespaceOption === 'condense') {
          // condense consecutive whitespaces into single space
          text = text.replace(whitespaceRE, ' ')
        }
        let res
        let child: ASTNode | undefined
        if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
          child = {
            type: 2,
            expression: res.expression,
            tokens: res.tokens,
            text
          }
        } else if (
          text !== ' ' ||
          !children.length ||
          children[children.length - 1].text !== ' '
        ) {
          child = {
            type: 3,
            text
          }
        }
        if (child) {
          if (__DEV__ && options.outputSourceRange) {
            child.start = start
            child.end = end
          }
          children.push(child)
        }
      }
    },
    comment(text: string, start, end) {
      // adding anything as a sibling to the root node is forbidden
      // comments should still be allowed, but ignored
      if (currentParent) {
        const child: ASTText = {
          type: 3,
          text,
          isComment: true
        }
        if (__DEV__ && options.outputSourceRange) {
          child.start = start
          child.end = end
        }
        currentParent.children.push(child)
      }
    }
  })
  return root
}

function processPre(el) {
  if (getAndRemoveAttr(el, 'v-pre') != null) {
    el.pre = true
  }
}

function processRawAttrs(el) {
  const list = el.attrsList
  const len = list.length
  if (len) {
    const attrs: Array<ASTAttr> = (el.attrs = new Array(len))
    for (let i = 0; i < len; i++) {
      attrs[i] = {
        name: list[i].name,
        value: JSON.stringify(list[i].value)
      }
      if (list[i].start != null) {
        attrs[i].start = list[i].start
        attrs[i].end = list[i].end
      }
    }
  } else if (!el.pre) {
    // non root node in pre blocks with no attributes
    el.plain = true
  }
}

export function processElement(element: ASTElement, options: CompilerOptions) {
  processKey(element)

  // determine whether this is a plain element after
  // removing structural attributes
  element.plain =
    !element.key && !element.scopedSlots && !element.attrsList.length

  processRef(element)
  processSlotContent(element)
  processSlotOutlet(element)
  processComponent(element)
  for (let i = 0; i < transforms.length; i++) {
    element = transforms[i](element, options) || element
  }
  processAttrs(element)
  return element
}

function processKey(el) {
  const exp = getBindingAttr(el, 'key')
  if (exp) {
    if (__DEV__) {
      if (el.tag === 'template') {
        warn(
          `<template> cannot be keyed. Place the key on real elements instead.`,
          getRawBindingAttr(el, 'key')
        )
      }
      if (el.for) {
        const iterator = el.iterator2 || el.iterator1
        const parent = el.parent
        if (
          iterator &&
          iterator === exp &&
          parent &&
          parent.tag === 'transition-group'
        ) {
          warn(
            `Do not use v-for index as key on <transition-group> children, ` +
              `this is the same as not using keys.`,
            getRawBindingAttr(el, 'key'),
            true /* tip */
          )
        }
      }
    }
    el.key = exp
  }
}

function processRef(el) {
  const ref = getBindingAttr(el, 'ref')
  if (ref) {
    el.ref = ref
    el.refInFor = checkInFor(el)
  }
}

export function processFor(el: ASTElement) {
  let exp
  if ((exp = getAndRemoveAttr(el, 'v-for'))) {
    const res = parseFor(exp)
    if (res) {
      extend(el, res)
    } else if (__DEV__) {
      warn(`Invalid v-for expression: ${exp}`, el.rawAttrsMap['v-for'])
    }
  }
}

type ForParseResult = {
  for: string
  alias: string
  iterator1?: string
  iterator2?: string
}

export function parseFor(exp: string): ForParseResult | undefined {
  const inMatch = exp.match(forAliasRE)
  if (!inMatch) return
  const res: any = {}
  res.for = inMatch[2].trim()
  const alias = inMatch[1].trim().replace(stripParensRE, '')
  const iteratorMatch = alias.match(forIteratorRE)
  if (iteratorMatch) {
    res.alias = alias.replace(forIteratorRE, '').trim()
    res.iterator1 = iteratorMatch[1].trim()
    if (iteratorMatch[2]) {
      res.iterator2 = iteratorMatch[2].trim()
    }
  } else {
    res.alias = alias
  }
  return res
}

function processIf(el) {
  const exp = getAndRemoveAttr(el, 'v-if')
  if (exp) {
    el.if = exp
    addIfCondition(el, {
      exp: exp,
      block: el
    })
  } else {
    if (getAndRemoveAttr(el, 'v-else') != null) {
      el.else = true
    }
    const elseif = getAndRemoveAttr(el, 'v-else-if')
    if (elseif) {
      el.elseif = elseif
    }
  }
}

function processIfConditions(el, parent) {
  const prev = findPrevElement(parent.children)
  if (prev && prev.if) {
    addIfCondition(prev, {
      exp: el.elseif,
      block: el
    })
  } else if (__DEV__) {
    warn(
      `v-${el.elseif ? 'else-if="' + el.elseif + '"' : 'else'} ` +
        `used on element <${el.tag}> without corresponding v-if.`,
      el.rawAttrsMap[el.elseif ? 'v-else-if' : 'v-else']
    )
  }
}

function findPrevElement(children: Array<any>): ASTElement | void {
  let i = children.length
  while (i--) {
    if (children[i].type === 1) {
      return children[i]
    } else {
      if (__DEV__ && children[i].text !== ' ') {
        warn(
          `text "${children[i].text.trim()}" between v-if and v-else(-if) ` +
            `will be ignored.`,
          children[i]
        )
      }
      children.pop()
    }
  }
}

export function addIfCondition(el: ASTElement, condition: ASTIfCondition) {
  if (!el.ifConditions) {
    el.ifConditions = []
  }
  el.ifConditions.push(condition)
}

function processOnce(el) {
  const once = getAndRemoveAttr(el, 'v-once')
  if (once != null) {
    el.once = true
  }
}

// handle content being passed to a component as slot,
// e.g. <template slot="xxx">, <div slot-scope="xxx">
function processSlotContent(el) {
  let slotScope
  if (el.tag === 'template') {
    slotScope = getAndRemoveAttr(el, 'scope')
    /* istanbul ignore if */
    if (__DEV__ && slotScope) {
      warn(
        `the "scope" attribute for scoped slots have been deprecated and ` +
          `replaced by "slot-scope" since 2.5. The new "slot-scope" attribute ` +
          `can also be used on plain elements in addition to <template> to ` +
          `denote scoped slots.`,
        el.rawAttrsMap['scope'],
        true
      )
    }
    el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope')
  } else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
    /* istanbul ignore if */
    if (__DEV__ && el.attrsMap['v-for']) {
      warn(
        `Ambiguous combined usage of slot-scope and v-for on <${el.tag}> ` +
          `(v-for takes higher priority). Use a wrapper <template> for the ` +
          `scoped slot to make it clearer.`,
        el.rawAttrsMap['slot-scope'],
        true
      )
    }
    el.slotScope = slotScope
  }

  // slot="xxx"
  const slotTarget = getBindingAttr(el, 'slot')
  if (slotTarget) {
    el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
    el.slotTargetDynamic = !!(
      el.attrsMap[':slot'] || el.attrsMap['v-bind:slot']
    )
    // preserve slot as an attribute for native shadow DOM compat
    // only for non-scoped slots.
    if (el.tag !== 'template' && !el.slotScope) {
      addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'))
    }
  }

  // 2.6 v-slot syntax
  if (process.env.NEW_SLOT_SYNTAX) {
    if (el.tag === 'template') {
      // v-slot on <template>
      const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
      if (slotBinding) {
        if (__DEV__) {
          if (el.slotTarget || el.slotScope) {
            warn(`Unexpected mixed usage of different slot syntaxes.`, el)
          }
          if (el.parent && !maybeComponent(el.parent)) {
            warn(
              `<template v-slot> can only appear at the root level inside ` +
                `the receiving component`,
              el
            )
          }
        }
        const { name, dynamic } = getSlotName(slotBinding)
        el.slotTarget = name
        el.slotTargetDynamic = dynamic
        el.slotScope = slotBinding.value || emptySlotScopeToken // force it into a scoped slot for perf
      }
    } else {
      // v-slot on component, denotes default slot
      const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
      if (slotBinding) {
        if (__DEV__) {
          if (!maybeComponent(el)) {
            warn(
              `v-slot can only be used on components or <template>.`,
              slotBinding
            )
          }
          if (el.slotScope || el.slotTarget) {
            warn(`Unexpected mixed usage of different slot syntaxes.`, el)
          }
          if (el.scopedSlots) {
            warn(
              `To avoid scope ambiguity, the default slot should also use ` +
                `<template> syntax when there are other named slots.`,
              slotBinding
            )
          }
        }
        // add the component's children to its default slot
        const slots = el.scopedSlots || (el.scopedSlots = {})
        const { name, dynamic } = getSlotName(slotBinding)
        const slotContainer = (slots[name] = createASTElement(
          'template',
          [],
          el
        ))
        slotContainer.slotTarget = name
        slotContainer.slotTargetDynamic = dynamic
        slotContainer.children = el.children.filter((c: any) => {
          if (!c.slotScope) {
            c.parent = slotContainer
            return true
          }
        })
        slotContainer.slotScope = slotBinding.value || emptySlotScopeToken
        // remove children as they are returned from scopedSlots now
        el.children = []
        // mark el non-plain so data gets generated
        el.plain = false
      }
    }
  }
}

function getSlotName(binding) {
  let name = binding.name.replace(slotRE, '')
  if (!name) {
    if (binding.name[0] !== '#') {
      name = 'default'
    } else if (__DEV__) {
      warn(`v-slot shorthand syntax requires a slot name.`, binding)
    }
  }
  return dynamicArgRE.test(name)
    ? // dynamic [name]
      { name: name.slice(1, -1), dynamic: true }
    : // static name
      { name: `"${name}"`, dynamic: false }
}

// handle <slot/> outlets
function processSlotOutlet(el) {
  if (el.tag === 'slot') {
    el.slotName = getBindingAttr(el, 'name')
    if (__DEV__ && el.key) {
      warn(
        `\`key\` does not work on <slot> because slots are abstract outlets ` +
          `and can possibly expand into multiple elements. ` +
          `Use the key on a wrapping element instead.`,
        getRawBindingAttr(el, 'key')
      )
    }
  }
}

function processComponent(el) {
  let binding
  if ((binding = getBindingAttr(el, 'is'))) {
    el.component = binding
  }
  if (getAndRemoveAttr(el, 'inline-template') != null) {
    el.inlineTemplate = true
  }
}

function processAttrs(el) {
  const list = el.attrsList
  let i, l, name, rawName, value, modifiers, syncGen, isDynamic
  for (i = 0, l = list.length; i < l; i++) {
    name = rawName = list[i].name
    value = list[i].value
    if (dirRE.test(name)) {
      // mark element as dynamic
      el.hasBindings = true
      // modifiers
      modifiers = parseModifiers(name.replace(dirRE, ''))
      // support .foo shorthand syntax for the .prop modifier
      if (process.env.VBIND_PROP_SHORTHAND && propBindRE.test(name)) {
        ;(modifiers || (modifiers = {})).prop = true
        name = `.` + name.slice(1).replace(modifierRE, '')
      } else if (modifiers) {
        name = name.replace(modifierRE, '')
      }
      if (bindRE.test(name)) {
        // v-bind
        name = name.replace(bindRE, '')
        value = parseFilters(value)
        isDynamic = dynamicArgRE.test(name)
        if (isDynamic) {
          name = name.slice(1, -1)
        }
        if (__DEV__ && value.trim().length === 0) {
          warn(
            `The value for a v-bind expression cannot be empty. Found in "v-bind:${name}"`
          )
        }
        if (modifiers) {
          if (modifiers.prop && !isDynamic) {
            name = camelize(name)
            if (name === 'innerHtml') name = 'innerHTML'
          }
          if (modifiers.camel && !isDynamic) {
            name = camelize(name)
          }
          if (modifiers.sync) {
            syncGen = genAssignmentCode(value, `$event`)
            if (!isDynamic) {
              addHandler(
                el,
                `update:${camelize(name)}`,
                syncGen,
                null,
                false,
                warn,
                list[i]
              )
              if (hyphenate(name) !== camelize(name)) {
                addHandler(
                  el,
                  `update:${hyphenate(name)}`,
                  syncGen,
                  null,
                  false,
                  warn,
                  list[i]
                )
              }
            } else {
              // handler w/ dynamic event name
              addHandler(
                el,
                `"update:"+(${name})`,
                syncGen,
                null,
                false,
                warn,
                list[i],
                true // dynamic
              )
            }
          }
        }
        if (
          (modifiers && modifiers.prop) ||
          (!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name))
        ) {
          addProp(el, name, value, list[i], isDynamic)
        } else {
          addAttr(el, name, value, list[i], isDynamic)
        }
      } else if (onRE.test(name)) {
        // v-on
        name = name.replace(onRE, '')
        isDynamic = dynamicArgRE.test(name)
        if (isDynamic) {
          name = name.slice(1, -1)
        }
        addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic)
      } else {
        // normal directives
        name = name.replace(dirRE, '')
        // parse arg
        const argMatch = name.match(argRE)
        let arg = argMatch && argMatch[1]
        isDynamic = false
        if (arg) {
          name = name.slice(0, -(arg.length + 1))
          if (dynamicArgRE.test(arg)) {
            arg = arg.slice(1, -1)
            isDynamic = true
          }
        }
        addDirective(
          el,
          name,
          rawName,
          value,
          arg,
          isDynamic,
          modifiers,
          list[i]
        )
        if (__DEV__ && name === 'model') {
          checkForAliasModel(el, value)
        }
      }
    } else {
      // literal attribute
      if (__DEV__) {
        const res = parseText(value, delimiters)
        if (res) {
          warn(
            `${name}="${value}": ` +
              'Interpolation inside attributes has been removed. ' +
              'Use v-bind or the colon shorthand instead. For example, ' +
              'instead of <div id="{{ val }}">, use <div :id="val">.',
            list[i]
          )
        }
      }
      addAttr(el, name, JSON.stringify(value), list[i])
      // #6887 firefox doesn't update muted state if set via attribute
      // even immediately after element creation
      if (
        !el.component &&
        name === 'muted' &&
        platformMustUseProp(el.tag, el.attrsMap.type, name)
      ) {
        addProp(el, name, 'true', list[i])
      }
    }
  }
}

function checkInFor(el: ASTElement): boolean {
  let parent: ASTElement | void = el
  while (parent) {
    if (parent.for !== undefined) {
      return true
    }
    parent = parent.parent
  }
  return false
}

function parseModifiers(name: string): Object | void {
  const match = name.match(modifierRE)
  if (match) {
    const ret = {}
    match.forEach(m => {
      ret[m.slice(1)] = true
    })
    return ret
  }
}

function makeAttrsMap(attrs: Array<Record<string, any>>): Record<string, any> {
  const map = {}
  for (let i = 0, l = attrs.length; i < l; i++) {
    if (__DEV__ && map[attrs[i].name] && !isIE && !isEdge) {
      warn('duplicate attribute: ' + attrs[i].name, attrs[i])
    }
    map[attrs[i].name] = attrs[i].value
  }
  return map
}

// for script (e.g. type="x/template") or style, do not decode content
function isTextTag(el): boolean {
  return el.tag === 'script' || el.tag === 'style'
}

function isForbiddenTag(el): boolean {
  return (
    el.tag === 'style' ||
    (el.tag === 'script' &&
      (!el.attrsMap.type || el.attrsMap.type === 'text/javascript'))
  )
}

const ieNSBug = /^xmlns:NS\d+/
const ieNSPrefix = /^NS\d+:/

/* istanbul ignore next */
function guardIESVGBug(attrs) {
  const res: any[] = []
  for (let i = 0; i < attrs.length; i++) {
    const attr = attrs[i]
    if (!ieNSBug.test(attr.name)) {
      attr.name = attr.name.replace(ieNSPrefix, '')
      res.push(attr)
    }
  }
  return res
}

function checkForAliasModel(el, value) {
  let _el = el
  while (_el) {
    if (_el.for && _el.alias === value) {
      warn(
        `<${el.tag} v-model="${value}">: ` +
          `You are binding v-model directly to a v-for iteration alias. ` +
          `This will not be able to modify the v-for source array because ` +
          `writing to the alias is like modifying a function local variable. ` +
          `Consider using an array of objects and use v-model on an object property instead.`,
        el.rawAttrsMap['v-model']
      )
    }
    _el = _el.parent
  }
}

这是模板解析器的代码,用于将 HTML 模板字符串转换为抽象语法树(AST)。以下是代码的步骤:

  1. 导入所需的模块和函数。

  2. 定义一些正则表达式和常量。

  3. 定义一些全局变量和函数。

  4. 创建一个AST元素的函数,用于创建 AST 树的节点。

  5. 定义一个解析函数,接收模板字符串和选项作为参数,并返回 AST 树。

  6. 在解析函数中,根据选项初始化一些全局变量。

  7. 定义一些辅助函数和变量。

  8. 调用 parseHTML 函数,将模板字符串解析为 AST 树。

  9. 在解析过程中,根据解析的标签、属性和文本内容,创建 AST 节点,并将其添加到 AST 树中。

  10. 在解析过程中,根据标签、属性和文本内容的不同,执行相应的处理逻辑,如处理 v-for 指令、处理 v-if 指令等。

  11. 解析完成后,对 AST 树进行一些处理和优化,如去除空白节点、处理 v-pre 指令等。

  12. 返回最终的 AST 树。

  13. 之后是一个 Vue 编译器的处理元素的函数。它接收一个 AST 元素对象和编译选项作为参数,然后对元素进行一系列处理操作,并返回处理后的元素对象。首先,函数调用了 processKey、processRef、processSlotContent、processSlotOutlet、processComponent 和 processAttrs 等函数来处理元素的 key、ref、插槽内容、插槽出口、组件和属性等。然后,使用一个循环遍历 transforms 数组,对元素对象进行一系列转换操作。最后,返回处理后的元素对象。

  14. 之后定义了一个类型 ForParseResult,表示解析 v-for 属性后的结果,包括 for、alias、iterator1 和 iterator2 四个属性。定义了一个函数 parseFor,用来解析 v-for 属性。该函数首先使用正则表达式匹配 v-for 属性,如果匹配不到,则返回 undefined。如果匹配到了 v-for 属性,则创建一个空对象 res,将匹配到的值存入 res 的 for 属性中。然后,提取 v-for 属性中的 alias 部分,并使用正则表达式去除可能存在的括号,将结果存入 res 的 alias 属性中。接着,再次使用正则表达式匹配 alias 属性中的迭代器部分,如果匹配到了,则将匹配到的值存入 res 的 iterator1 属性中。如果还存在第二个迭代器,则将其存入 res 的 iterator2 属性中。如果没有匹配到迭代器,则直接将 alias 存入 res 的 alias 属性中。最后,返回 res 作为解析结果。

  15. 接下来,定义了一系列处理 v-if、v-else-if、v-else、v-once 和 slot 相关属性的函数,包括 processIf、processIfConditions、processOnce 和 processSlotContent。

  • processIf 函数用于处理 v-if 属性。
  • processIfConditions 函数用于处理 v-else-if 和 v-else 属性。
  • findPrevElement 函数用于在 children 数组中找到前一个元素,如果该元素是 ASTElement 类型,则返回该元素,否则,如果该元素是文本节点且不为空格,则将其从 children 数组中删除。该函数用于处理 v-if 和 v-else 之间的文本节点。
  • addIfCondition 函数用于将条件添加到 ASTElement 的 ifConditions 数组中。
  • 定义了一个函数 processSlotContent,用于处理 slot 相关的属性。
  1. 最后是用于处理模板中的属性和指令的方法。
  • getSlotName(binding) 函数用于获取插槽的名称。它首先从 binding.name 中移除插槽的正则表达式匹配项,并将结果赋给 name 变量。如果 name 为空,则判断 binding.name[0] 是否为 '#' ,如果是,则在开发环境下会发出警告。最后,根据 name 是否满足动态参数的正则表达式,返回一个对象,包含插槽的名称和是否为动态插槽。

  • processSlotOutlet(el) 函数用于处理 <slot/> 元素。如果元素的标签为 slot ,则通过 getBindingAttr(el, 'name') 获取插槽的名称,并赋值给 el.slotName 。如果在开发环境下 el.key 存在,则发出警告。

  • processComponent(el) 函数用于处理组件元素。首先通过 getBindingAttr(el, 'is') 获取组件的 is 属性,并将结果赋给 el.component 。然后判断 el 是否存在 inline-template 属性,并将结果赋给 el.inlineTemplate 。

  • processAttrs(el) 函数用于处理元素的属性。首先获取元素的属性列表,并遍历列表中的每个属性。如果属性的名称满足指令的正则表达式,则表示该属性是一个指令。在处理指令的过程中,会标记元素为动态元素,并解析指令的修饰符。如果指令的名称满足 .foo 的缩写语法,则将修饰符中的 prop 设置为 true ,并修改指令的名称。然后判断指令的类型,如果是 v-bind 指令,则移除指令的前缀,并解析指令的值。如果指令的名称满足动态参数的正则表达式,则将指令的名称修改为去掉头尾字符的结果。如果指令的值为空,并且在开发环境下,会发出警告。如果指令存在修饰符,并且修饰符中包含 prop ,并且指令的名称不是动态的,则将指令的名称转为驼峰命名,并将 innerHtml 转为 innerHTML 。如果修饰符中包含 camel ,并且指令的名称不是动态的,则将指令的名称转为驼峰命名。如果修饰符中包含 sync ,则生成一个同步更新的处理函数,并根据指令的名称是否是动态的,添加相应的事件处理函数。如果指令需要使用 prop ,或者元素不是组件且需要使用 prop ,则调用 addProp 函数添加属性。否则,调用 addAttr 函数添加属性。如果指令的名称满足 v-on 的正则表达式,则移除指令的前缀,并判断指令的名称是否是动态的,然后调用 addHandler 函数添加事件处理函数。如果指令的名称不满足以上两个正则表达式,则表示该属性是一个普通的属性。在开发环境下,会检查属性值是否包含插值,并发出相应的警告。最后,调用 addAttr 函数添加属性。

  • checkInFor(el: ASTElement): boolean 函数用于检查元素是否在 v-for 指令中。通过遍历元素的父级元素,判断父级元素是否存在 for 属性,如果存在,则返回 true ,否则返回 false 。

  • parseModifiers(name: string): Object | void 函数用于解析指令的修饰符。通过正则表达式匹配修饰符,并返回一个包含修饰符的对象。

  • makeAttrsMap(attrs: Array<Record<string, any>>): Record<string, any> 函数用于将属性列表转换为属性映射对象。遍历属性列表,将属性的名称作为键,属性的值作为值,存储在映射对象中。

  • isTextTag(el): boolean 函数用于判断元素是否是文本标签。如果元素的标签是 script 或 style ,则返回 true ,否则返回 false 。

  • isForbiddenTag(el): boolean 函数用于判断元素是否是禁止的标签。如果元素的标签是 style 或 script ,且 type 属性为空或为 text/javascript ,则返回 true ,否则返回 false 。

  • guardIESVGBug(attrs) 函数用于修复 IE 中的 SVG bug。遍历属性列表,将不满足 ieNSBug 正则表达式的属性的名称中的 ieNSPrefix 替换为空字符串,并将结果存储在新的数组中。

  • checkForAliasModel(el, value) 函数用于检查 v-model 指令是否直接绑定到 v-for 循环的别名上。通过遍历元素的父级元素,检查父级元素是否存在 for 属性,并且别名与 v-model 的值相同。如果满足条件,则发出相应的警告。

compiler/optimizer.js源码:

ts 复制代码
import { makeMap, isBuiltInTag, cached, no } from 'shared/util'
import { ASTElement, CompilerOptions, ASTNode } from 'types/compiler'

let isStaticKey
let isPlatformReservedTag

const genStaticKeysCached = cached(genStaticKeys)

/**
 * Goal of the optimizer: walk the generated template AST tree
 * and detect sub-trees that are purely static, i.e. parts of
 * the DOM that never needs to change.
 *
 * Once we detect these sub-trees, we can:
 *
 * 1. Hoist them into constants, so that we no longer need to
 *    create fresh nodes for them on each re-render;
 * 2. Completely skip them in the patching process.
 */
export function optimize(
  root: ASTElement | null | undefined,
  options: CompilerOptions
) {
  if (!root) return
  isStaticKey = genStaticKeysCached(options.staticKeys || '')
  isPlatformReservedTag = options.isReservedTag || no
  // first pass: mark all non-static nodes.
  markStatic(root)
  // second pass: mark static roots.
  markStaticRoots(root, false)
}

function genStaticKeys(keys: string): Function {
  return makeMap(
    'type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap' +
      (keys ? ',' + keys : '')
  )
}

function markStatic(node: ASTNode) {
  node.static = isStatic(node)
  if (node.type === 1) {
    // do not make component slot content static. this avoids
    // 1. components not able to mutate slot nodes
    // 2. static slot content fails for hot-reloading
    if (
      !isPlatformReservedTag(node.tag) &&
      node.tag !== 'slot' &&
      node.attrsMap['inline-template'] == null
    ) {
      return
    }
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i]
      markStatic(child)
      if (!child.static) {
        node.static = false
      }
    }
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        const block = node.ifConditions[i].block
        markStatic(block)
        if (!block.static) {
          node.static = false
        }
      }
    }
  }
}

function markStaticRoots(node: ASTNode, isInFor: boolean) {
  if (node.type === 1) {
    if (node.static || node.once) {
      node.staticInFor = isInFor
    }
    // For a node to qualify as a static root, it should have children that
    // are not just static text. Otherwise the cost of hoisting out will
    // outweigh the benefits and it's better off to just always render it fresh.
    if (
      node.static &&
      node.children.length &&
      !(node.children.length === 1 && node.children[0].type === 3)
    ) {
      node.staticRoot = true
      return
    } else {
      node.staticRoot = false
    }
    if (node.children) {
      for (let i = 0, l = node.children.length; i < l; i++) {
        markStaticRoots(node.children[i], isInFor || !!node.for)
      }
    }
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        markStaticRoots(node.ifConditions[i].block, isInFor)
      }
    }
  }
}

function isStatic(node: ASTNode): boolean {
  if (node.type === 2) {
    // expression
    return false
  }
  if (node.type === 3) {
    // text
    return true
  }
  return !!(
    node.pre ||
    (!node.hasBindings && // no dynamic bindings
      !node.if &&
      !node.for && // not v-if or v-for or v-else
      !isBuiltInTag(node.tag) && // not a built-in
      isPlatformReservedTag(node.tag) && // not a component
      !isDirectChildOfTemplateFor(node) &&
      Object.keys(node).every(isStaticKey))
  )
}

function isDirectChildOfTemplateFor(node: ASTElement): boolean {
  while (node.parent) {
    node = node.parent
    if (node.tag !== 'template') {
      return false
    }
    if (node.for) {
      return true
    }
  }
  return false
}

这段代码是编译器中的优化器部分,主要的作用是对生成的模板抽象语法树(AST)进行遍历,检测出那些纯静态的子树,也就是那些在重新渲染时不需要变化的 DOM 部分。

具体的优化过程如下:

  1. 定义了一些辅助函数和变量,包括 makeMap、isBuiltInTag、cached、no 等。
  2. 使用 genStaticKeysCached 函数生成静态键的缓存版本。
  3. 定义了 optimize 函数,接受两个参数:根节点 root 和编译选项 options。
  4. 如果根节点不存在,则直接返回。
  5. 初始化一些变量 isStaticKey 和 isPlatformReservedTag,用于判断是否为静态节点以及是否为保留标签。
  6. 第一次遍历:标记所有非静态节点。通过调用 markStatic 函数实现。
  7. 第二次遍历:标记静态根节点。通过调用 markStaticRoots 函数实现。

具体的函数解释如下:

  • genStaticKeys 函数:根据传入的键值字符串生成一个函数,用于创建一个包含指定键的映射表。
  • markStatic 函数:递归遍历 AST 节点,并为每个节点标记是否为静态节点。对于元素节点,判断标签是否为保留标签以及是否包含 inline-template 属性。
  • markStaticRoots 函数:递归遍历 AST 节点,并为每个节点标记是否为静态根节点。对于元素节点,判断其是否是静态节点或只包含一个文本节点。
  • isStatic 函数:判断一个节点是否为静态节点。对于元素节点,判断是否满足一系列条件,如不包含动态绑定、条件渲染和循环等特性。
  • isDirectChildOfTemplateFor 函数:判断一个元素节点是否是直接处于 template 标签内并且存在 for 循环的子节点。

通过优化器的处理,可以将那些静态的 DOM 部分提取为常量,避免每次重新渲染时创建新的节点,从而提高渲染性能。同时,在更新过程中也可以完全跳过这些静态的子树,减少不必要的比对和计算操作。

compiler/codegen/index.js源码:

ts 复制代码
import { genHandlers } from './events'
import baseDirectives from '../directives/index'
import { camelize, no, extend, capitalize } from 'shared/util'
import { baseWarn, pluckModuleFunction } from '../helpers'
import { emptySlotScopeToken } from '../parser/index'
import {
  ASTAttr,
  ASTDirective,
  ASTElement,
  ASTExpression,
  ASTIfConditions,
  ASTNode,
  ASTText,
  CompilerOptions
} from 'types/compiler'
import { BindingMetadata, BindingTypes } from 'sfc/types'

type TransformFunction = (el: ASTElement, code: string) => string
type DataGenFunction = (el: ASTElement) => string
type DirectiveFunction = (
  el: ASTElement,
  dir: ASTDirective,
  warn: Function
) => boolean

export class CodegenState {
  options: CompilerOptions
  warn: Function
  transforms: Array<TransformFunction>
  dataGenFns: Array<DataGenFunction>
  directives: { [key: string]: DirectiveFunction }
  maybeComponent: (el: ASTElement) => boolean
  onceId: number
  staticRenderFns: Array<string>
  pre: boolean

  constructor(options: CompilerOptions) {
    this.options = options
    this.warn = options.warn || baseWarn
    this.transforms = pluckModuleFunction(options.modules, 'transformCode')
    this.dataGenFns = pluckModuleFunction(options.modules, 'genData')
    this.directives = extend(extend({}, baseDirectives), options.directives)
    const isReservedTag = options.isReservedTag || no
    this.maybeComponent = (el: ASTElement) =>
      !!el.component || !isReservedTag(el.tag)
    this.onceId = 0
    this.staticRenderFns = []
    this.pre = false
  }
}

export type CodegenResult = {
  render: string
  staticRenderFns: Array<string>
}

export function generate(
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  // fix #11483, Root level <script> tags should not be rendered.
  const code = ast
    ? ast.tag === 'script'
      ? 'null'
      : genElement(ast, state)
    : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

export function genElement(el: ASTElement, state: CodegenState): string {
  if (el.parent) {
    el.pre = el.pre || el.parent.pre
  }

  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {
    return genSlot(el, state)
  } else {
    // component or element
    let code
    if (el.component) {
      code = genComponent(el.component, el, state)
    } else {
      let data
      const maybeComponent = state.maybeComponent(el)
      if (!el.plain || (el.pre && maybeComponent)) {
        data = genData(el, state)
      }

      let tag: string | undefined
      // check if this is a component in <script setup>
      const bindings = state.options.bindings
      if (maybeComponent && bindings && bindings.__isScriptSetup !== false) {
        tag = checkBindingType(bindings, el.tag)
      }
      if (!tag) tag = `'${el.tag}'`

      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      code = `_c(${tag}${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    // module transforms
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    return code
  }
}

function checkBindingType(bindings: BindingMetadata, key: string) {
  const camelName = camelize(key)
  const PascalName = capitalize(camelName)
  const checkType = (type) => {
    if (bindings[key] === type) {
      return key
    }
    if (bindings[camelName] === type) {
      return camelName
    }
    if (bindings[PascalName] === type) {
      return PascalName
    }
  }
  const fromConst =
    checkType(BindingTypes.SETUP_CONST) ||
    checkType(BindingTypes.SETUP_REACTIVE_CONST)
  if (fromConst) {
    return fromConst
  }

  const fromMaybeRef =
    checkType(BindingTypes.SETUP_LET) ||
    checkType(BindingTypes.SETUP_REF) ||
    checkType(BindingTypes.SETUP_MAYBE_REF)
  if (fromMaybeRef) {
    return fromMaybeRef
  }
}

// hoist static sub-trees out
function genStatic(el: ASTElement, state: CodegenState): string {
  el.staticProcessed = true
  // Some elements (templates) need to behave differently inside of a v-pre
  // node.  All pre nodes are static roots, so we can use this as a location to
  // wrap a state change and reset it upon exiting the pre node.
  const originalPreState = state.pre
  if (el.pre) {
    state.pre = el.pre
  }
  state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
  state.pre = originalPreState
  return `_m(${state.staticRenderFns.length - 1}${
    el.staticInFor ? ',true' : ''
  })`
}

// v-once
function genOnce(el: ASTElement, state: CodegenState): string {
  el.onceProcessed = true
  if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  } else if (el.staticInFor) {
    let key = ''
    let parent = el.parent
    while (parent) {
      if (parent.for) {
        key = parent.key!
        break
      }
      parent = parent.parent
    }
    if (!key) {
      __DEV__ &&
        state.warn(
          `v-once can only be used inside v-for that is keyed. `,
          el.rawAttrsMap['v-once']
        )
      return genElement(el, state)
    }
    return `_o(${genElement(el, state)},${state.onceId++},${key})`
  } else {
    return genStatic(el, state)
  }
}

export function genIf(
  el: any,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  el.ifProcessed = true // avoid recursion
  return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}

function genIfConditions(
  conditions: ASTIfConditions,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  if (!conditions.length) {
    return altEmpty || '_e()'
  }

  const condition = conditions.shift()!
  if (condition.exp) {
    return `(${condition.exp})?${genTernaryExp(
      condition.block
    )}:${genIfConditions(conditions, state, altGen, altEmpty)}`
  } else {
    return `${genTernaryExp(condition.block)}`
  }

  // v-if with v-once should generate code like (a)?_m(0):_m(1)
  function genTernaryExp(el) {
    return altGen
      ? altGen(el, state)
      : el.once
      ? genOnce(el, state)
      : genElement(el, state)
  }
}

export function genFor(
  el: any,
  state: CodegenState,
  altGen?: Function,
  altHelper?: string
): string {
  const exp = el.for
  const alias = el.alias
  const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
  const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''

  if (
    __DEV__ &&
    state.maybeComponent(el) &&
    el.tag !== 'slot' &&
    el.tag !== 'template' &&
    !el.key
  ) {
    state.warn(
      `<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
        `v-for should have explicit keys. ` +
        `See https://v2.vuejs.org/v2/guide/list.html#key for more info.`,
      el.rawAttrsMap['v-for'],
      true /* tip */
    )
  }

  el.forProcessed = true // avoid recursion
  return (
    `${altHelper || '_l'}((${exp}),` +
    `function(${alias}${iterator1}${iterator2}){` +
    `return ${(altGen || genElement)(el, state)}` +
    '})'
  )
}

export function genData(el: ASTElement, state: CodegenState): string {
  let data = '{'

  // directives first.
  // directives may mutate the el's other properties before they are generated.
  const dirs = genDirectives(el, state)
  if (dirs) data += dirs + ','

  // key
  if (el.key) {
    data += `key:${el.key},`
  }
  // ref
  if (el.ref) {
    data += `ref:${el.ref},`
  }
  if (el.refInFor) {
    data += `refInFor:true,`
  }
  // pre
  if (el.pre) {
    data += `pre:true,`
  }
  // record original tag name for components using "is" attribute
  if (el.component) {
    data += `tag:"${el.tag}",`
  }
  // module data generation functions
  for (let i = 0; i < state.dataGenFns.length; i++) {
    data += state.dataGenFns[i](el)
  }
  // attributes
  if (el.attrs) {
    data += `attrs:${genProps(el.attrs)},`
  }
  // DOM props
  if (el.props) {
    data += `domProps:${genProps(el.props)},`
  }
  // event handlers
  if (el.events) {
    data += `${genHandlers(el.events, false)},`
  }
  if (el.nativeEvents) {
    data += `${genHandlers(el.nativeEvents, true)},`
  }
  // slot target
  // only for non-scoped slots
  if (el.slotTarget && !el.slotScope) {
    data += `slot:${el.slotTarget},`
  }
  // scoped slots
  if (el.scopedSlots) {
    data += `${genScopedSlots(el, el.scopedSlots, state)},`
  }
  // component v-model
  if (el.model) {
    data += `model:{value:${el.model.value},callback:${el.model.callback},expression:${el.model.expression}},`
  }
  // inline-template
  if (el.inlineTemplate) {
    const inlineTemplate = genInlineTemplate(el, state)
    if (inlineTemplate) {
      data += `${inlineTemplate},`
    }
  }
  data = data.replace(/,$/, '') + '}'
  // v-bind dynamic argument wrap
  // v-bind with dynamic arguments must be applied using the same v-bind object
  // merge helper so that class/style/mustUseProp attrs are handled correctly.
  if (el.dynamicAttrs) {
    data = `_b(${data},"${el.tag}",${genProps(el.dynamicAttrs)})`
  }
  // v-bind data wrap
  if (el.wrapData) {
    data = el.wrapData(data)
  }
  // v-on data wrap
  if (el.wrapListeners) {
    data = el.wrapListeners(data)
  }
  return data
}

function genDirectives(el: ASTElement, state: CodegenState): string | void {
  const dirs = el.directives
  if (!dirs) return
  let res = 'directives:['
  let hasRuntime = false
  let i, l, dir, needRuntime
  for (i = 0, l = dirs.length; i < l; i++) {
    dir = dirs[i]
    needRuntime = true
    const gen: DirectiveFunction = state.directives[dir.name]
    if (gen) {
      // compile-time directive that manipulates AST.
      // returns true if it also needs a runtime counterpart.
      needRuntime = !!gen(el, dir, state.warn)
    }
    if (needRuntime) {
      hasRuntime = true
      res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
        dir.value
          ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}`
          : ''
      }${dir.arg ? `,arg:${dir.isDynamicArg ? dir.arg : `"${dir.arg}"`}` : ''}${
        dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
      }},`
    }
  }
  if (hasRuntime) {
    return res.slice(0, -1) + ']'
  }
}

function genInlineTemplate(
  el: ASTElement,
  state: CodegenState
): string | undefined {
  const ast = el.children[0]
  if (__DEV__ && (el.children.length !== 1 || ast.type !== 1)) {
    state.warn(
      'Inline-template components must have exactly one child element.',
      { start: el.start }
    )
  }
  if (ast && ast.type === 1) {
    const inlineRenderFns = generate(ast, state.options)
    return `inlineTemplate:{render:function(){${
      inlineRenderFns.render
    }},staticRenderFns:[${inlineRenderFns.staticRenderFns
      .map(code => `function(){${code}}`)
      .join(',')}]}`
  }
}

function genScopedSlots(
  el: ASTElement,
  slots: { [key: string]: ASTElement },
  state: CodegenState
): string {
  // by default scoped slots are considered "stable", this allows child
  // components with only scoped slots to skip forced updates from parent.
  // but in some cases we have to bail-out of this optimization
  // for example if the slot contains dynamic names, has v-if or v-for on them...
  let needsForceUpdate =
    el.for ||
    Object.keys(slots).some(key => {
      const slot = slots[key]
      return (
        slot.slotTargetDynamic || slot.if || slot.for || containsSlotChild(slot) // is passing down slot from parent which may be dynamic
      )
    })

  // #9534: if a component with scoped slots is inside a conditional branch,
  // it's possible for the same component to be reused but with different
  // compiled slot content. To avoid that, we generate a unique key based on
  // the generated code of all the slot contents.
  let needsKey = !!el.if

  // OR when it is inside another scoped slot or v-for (the reactivity may be
  // disconnected due to the intermediate scope variable)
  // #9438, #9506
  // TODO: this can be further optimized by properly analyzing in-scope bindings
  // and skip force updating ones that do not actually use scope variables.
  if (!needsForceUpdate) {
    let parent = el.parent
    while (parent) {
      if (
        (parent.slotScope && parent.slotScope !== emptySlotScopeToken) ||
        parent.for
      ) {
        needsForceUpdate = true
        break
      }
      if (parent.if) {
        needsKey = true
      }
      parent = parent.parent
    }
  }

  const generatedSlots = Object.keys(slots)
    .map(key => genScopedSlot(slots[key], state))
    .join(',')

  return `scopedSlots:_u([${generatedSlots}]${
    needsForceUpdate ? `,null,true` : ``
  }${
    !needsForceUpdate && needsKey ? `,null,false,${hash(generatedSlots)}` : ``
  })`
}

function hash(str) {
  let hash = 5381
  let i = str.length
  while (i) {
    hash = (hash * 33) ^ str.charCodeAt(--i)
  }
  return hash >>> 0
}

function containsSlotChild(el: ASTNode): boolean {
  if (el.type === 1) {
    if (el.tag === 'slot') {
      return true
    }
    return el.children.some(containsSlotChild)
  }
  return false
}

function genScopedSlot(el: ASTElement, state: CodegenState): string {
  const isLegacySyntax = el.attrsMap['slot-scope']
  if (el.if && !el.ifProcessed && !isLegacySyntax) {
    return genIf(el, state, genScopedSlot, `null`)
  }
  if (el.for && !el.forProcessed) {
    return genFor(el, state, genScopedSlot)
  }
  const slotScope =
    el.slotScope === emptySlotScopeToken ? `` : String(el.slotScope)
  const fn =
    `function(${slotScope}){` +
    `return ${
      el.tag === 'template'
        ? el.if && isLegacySyntax
          ? `(${el.if})?${genChildren(el, state) || 'undefined'}:undefined`
          : genChildren(el, state) || 'undefined'
        : genElement(el, state)
    }}`
  // reverse proxy v-slot without scope on this.$slots
  const reverseProxy = slotScope ? `` : `,proxy:true`
  return `{key:${el.slotTarget || `"default"`},fn:${fn}${reverseProxy}}`
}

export function genChildren(
  el: ASTElement,
  state: CodegenState,
  checkSkip?: boolean,
  altGenElement?: Function,
  altGenNode?: Function
): string | void {
  const children = el.children
  if (children.length) {
    const el: any = children[0]
    // optimize single v-for
    if (
      children.length === 1 &&
      el.for &&
      el.tag !== 'template' &&
      el.tag !== 'slot'
    ) {
      const normalizationType = checkSkip
        ? state.maybeComponent(el)
          ? `,1`
          : `,0`
        : ``
      return `${(altGenElement || genElement)(el, state)}${normalizationType}`
    }
    const normalizationType = checkSkip
      ? getNormalizationType(children, state.maybeComponent)
      : 0
    const gen = altGenNode || genNode
    return `[${children.map(c => gen(c, state)).join(',')}]${
      normalizationType ? `,${normalizationType}` : ''
    }`
  }
}

// determine the normalization needed for the children array.
// 0: no normalization needed
// 1: simple normalization needed (possible 1-level deep nested array)
// 2: full normalization needed
function getNormalizationType(
  children: Array<ASTNode>,
  maybeComponent: (el: ASTElement) => boolean
): number {
  let res = 0
  for (let i = 0; i < children.length; i++) {
    const el: ASTNode = children[i]
    if (el.type !== 1) {
      continue
    }
    if (
      needsNormalization(el) ||
      (el.ifConditions &&
        el.ifConditions.some(c => needsNormalization(c.block)))
    ) {
      res = 2
      break
    }
    if (
      maybeComponent(el) ||
      (el.ifConditions && el.ifConditions.some(c => maybeComponent(c.block)))
    ) {
      res = 1
    }
  }
  return res
}

function needsNormalization(el: ASTElement): boolean {
  return el.for !== undefined || el.tag === 'template' || el.tag === 'slot'
}

function genNode(node: ASTNode, state: CodegenState): string {
  if (node.type === 1) {
    return genElement(node, state)
  } else if (node.type === 3 && node.isComment) {
    return genComment(node)
  } else {
    return genText(node)
  }
}

export function genText(text: ASTText | ASTExpression): string {
  return `_v(${
    text.type === 2
      ? text.expression // no need for () because already wrapped in _s()
      : transformSpecialNewlines(JSON.stringify(text.text))
  })`
}

export function genComment(comment: ASTText): string {
  return `_e(${JSON.stringify(comment.text)})`
}

function genSlot(el: ASTElement, state: CodegenState): string {
  const slotName = el.slotName || '"default"'
  const children = genChildren(el, state)
  let res = `_t(${slotName}${children ? `,function(){return ${children}}` : ''}`
  const attrs =
    el.attrs || el.dynamicAttrs
      ? genProps(
          (el.attrs || []).concat(el.dynamicAttrs || []).map(attr => ({
            // slot props are camelized
            name: camelize(attr.name),
            value: attr.value,
            dynamic: attr.dynamic
          }))
        )
      : null
  const bind = el.attrsMap['v-bind']
  if ((attrs || bind) && !children) {
    res += `,null`
  }
  if (attrs) {
    res += `,${attrs}`
  }
  if (bind) {
    res += `${attrs ? '' : ',null'},${bind}`
  }
  return res + ')'
}

// componentName is el.component, take it as argument to shun flow's pessimistic refinement
function genComponent(
  componentName: string,
  el: ASTElement,
  state: CodegenState
): string {
  const children = el.inlineTemplate ? null : genChildren(el, state, true)
  return `_c(${componentName},${genData(el, state)}${
    children ? `,${children}` : ''
  })`
}

function genProps(props: Array<ASTAttr>): string {
  let staticProps = ``
  let dynamicProps = ``
  for (let i = 0; i < props.length; i++) {
    const prop = props[i]
    const value = transformSpecialNewlines(prop.value)
    if (prop.dynamic) {
      dynamicProps += `${prop.name},${value},`
    } else {
      staticProps += `"${prop.name}":${value},`
    }
  }
  staticProps = `{${staticProps.slice(0, -1)}}`
  if (dynamicProps) {
    return `_d(${staticProps},[${dynamicProps.slice(0, -1)}])`
  } else {
    return staticProps
  }
}

// #3895, #4268
function transformSpecialNewlines(text: string): string {
  return text.replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029')
}

代码逻辑如下:

首先,代码导入了一些需要使用的函数和类型。

然后,定义了一个名为CodegenState的类,该类包含了一些用于存储编译选项和状态的属性和方法。

接下来,定义了一个名为CodegenResult的类型,用于表示生成的代码结果。

然后,定义了一个名为generate的函数,该函数接受一个AST和编译选项作为参数,返回一个CodegenResult对象。在函数内部,首先创建了一个CodegenState实例,然后根据AST的类型生成相应的代码。如果AST存在且为script标签,则返回'null',否则调用genElement函数生成元素代码。最后,返回一个包含render代码和静态渲染函数数组的对象。

接下来,定义了一个名为genElement的函数,该函数接受一个AST元素和CodegenState实例作为参数,返回一个字符串表示的代码。在函数内部,首先根据AST元素的父元素设置pre属性,然后根据AST元素的类型调用相应的生成函数生成代码。如果AST元素是静态根且未处理过,则调用genStatic函数生成静态代码;如果AST元素是v-once指令且未处理过,则调用genOnce函数生成v-once代码;如果AST元素是v-for指令且未处理过,则调用genFor函数生成v-for代码;如果AST元素是v-if指令且未处理过,则调用genIf函数生成v-if代码;如果AST元素是template标签且没有插槽目标且不是pre标签,则调用genChildren函数生成子元素代码,如果没有子元素则返回'void 0';如果AST元素是slot标签,则调用genSlot函数生成slot代码;否则,根据AST元素的类型生成组件或元素代码。最后,根据模块的转换函数对代码进行转换。

接下来是一些辅助函数,如checkBindingType用于检查绑定类型,genStatic用于生成静态代码,genOnce用于生成v-once代码,genIf用于生成v-if代码,genIfConditions用于生成v-if条件代码,genFor用于生成v-for代码。

之后,是一个用于生成数据的函数。函数的参数是一个AST元素和一个代码生成状态对象。

个别函数说明:

  • genDirectives函数接受一个AST元素和一个代码生成状态对象作为参数。函数首先获取AST元素的directives属性,如果不存在则直接返回。接下来,函数初始化一个res变量,值为一个字符串''。然后,函数遍历directives数组,对每个directive进行处理。函数首先获取directive的name属性,并根据该属性从state.directives中获取对应的生成函数gen。然后,函数判断gen是否存在,如果存在则调用该函数对AST元素和directive进行处理,并根据返回值判断是否需要运行时处理。最后,函数将处理后的directive数据添加到res中。最后,函数根据是否存在运行时处理的directive来决定返回的数据。

  • genInlineTemplate函数接受一个AST元素和一个代码生成状态对象作为参数。函数首先获取AST元素的第一个子元素,并进行一些判断。如果子元素的数量不等于1或者类型不为1,则会调用state.warn函数进行警告。然后,函数判断子元素是否存在且类型为1。如果满足条件,则调用generate函数生成内联模板相关的数据,并将其作为字符串返回。

  • genScopedSlots函数接受一个AST元素、一个包含作用域插槽的对象和一个代码生成状态对象作为参数。函数首先判断是否需要强制更新。如果AST元素有for属性或者作用域插槽对象中的某个插槽满足一定条件,则需要强制更新。然后,函数判断是否需要生成唯一的key。如果AST元素有if属性,则需要生成唯一的key。接下来,函数遍历作用域插槽对象,对每个插槽进行处理,并将处理后的数据拼接成字符串。最后,函数返回生成的作用域插槽数据的字符串。

  • hash函数接受一个字符串作为参数。函数首先初始化一个hash变量为5381。然后,函数遍历字符串的每个字符,对每个字符进行一些运算。最后,函数返回计算后的hash值。

  • containsSlotChild函数接受一个AST节点作为参数。函数首先判断节点的类型是否为1。如果是1,则判断节点的标签是否为'slot',如果是则返回true。否则,递归遍历节点的子节点,并判断是否存在包含插槽的子节点。如果存在,则返回true。如果不满足上述条件,则返回false。

最后,是一个用于生成AST元素的子元素的函数。

  1. 首先,函数接受一个AST元素(el)和一个代码生成状态(state)作为参数。
  2. 然后,将el的子元素赋值给一个名为children的变量。
  3. 如果children数组的长度大于0,则继续执行下面的步骤。
  4. 检查children数组的长度是否为1,并且第一个子元素(el)具有v-for属性,并且标签不是"template"和"slot"。如果满足这些条件,则进行优化处理。
  5. 根据checkSkip参数的值决定是否需要规范化处理。如果checkSkip为true,则调用state.maybeComponent(el)函数来确定是否是组件,如果是则返回",1",否则返回",0"。如果checkSkip为false,则返回空字符串。
  6. 返回一个字符串,其中包括调用altGenElement函数(如果存在)或genElement函数生成的元素代码和规范化类型。如果altGenElement不存在,则调用genElement函数生成元素代码。
  7. 如果checkSkip为true,则调用getNormalizationType函数来确定children数组是否需要规范化处理。否则,返回0。
  8. 根据altGenNode参数的值决定是否需要调用altGenNode函数来生成节点代码。如果altGenNode存在,则调用altGenNode函数,否则调用genNode函数。
  9. 返回一个包含children数组中每个子元素调用gen函数生成的代码的字符串。如果规范化类型不为0,则在字符串末尾添加规范化类型。
  10. 如果children数组的长度为0,则返回空。

getNormalizationType函数用于确定children数组是否需要规范化处理。

needsNormalization函数用于确定一个AST元素是否需要规范化处理。

genNode函数用于生成一个AST节点的代码。

  1. 检查节点的类型。如果是1,则调用genElement函数生成元素代码。
  2. 如果节点的类型是3并且isComment属性为true,则调用genComment函数生成注释代码。
  3. 否则,调用genText函数生成文本代码。

genText函数用于生成文本节点的代码。

  1. 返回一个字符串,其中包括调用_v函数生成的代码。
  2. 如果文本节点的类型是2,则返回文本节点的表达式。
  3. 否则,调用transformSpecialNewlines函数对文本进行特殊换行符的转换,并将结果作为参数传递给JSON.stringify函数。

genComment函数用于生成注释节点的代码。

  1. 返回一个字符串,其中包括调用_e函数生成的代码。
  2. 将注释节点的文本作为参数传递给JSON.stringify函数。

genSlot函数用于生成插槽节点的代码。

  1. 首先,获取插槽的名称,如果插槽没有名称,则默认为"default"。
  2. 调用genChildren函数生成插槽的子元素代码,并将结果赋值给children变量。
  3. 初始化一个字符串变量res,其中包括调用_t函数生成的代码和插槽名称。如果children存在,则添加一个匿名函数,返回children的代码。
  4. 检查插槽的属性(attrs)或动态属性(dynamicAttrs)是否存在。如果存在,则调用genProps函数生成属性的代码。
  5. 检查插槽的属性中是否有"v-bind"属性。如果attrs或bind存在,并且children不存在,则添加",null"到res字符串中。
  6. 如果attrs存在,则将attrs的代码添加到res字符串中。
  7. 如果bind存在,则将bind的代码添加到res字符串中。
  8. 返回res字符串。

genComponent函数用于生成组件节点的代码。

  1. 首先,获取组件的名称。
  2. 调用genChildren函数生成组件的子元素代码,并将结果赋值给children变量。如果组件具有内联模板,则children为null。
  3. 返回一个字符串,其中包括调用_c函数生成的代码、组件名称和genData函数生成的数据代码。如果children存在,则添加children的代码。

genProps函数用于生成属性的代码。

  1. 初始化两个字符串变量staticProps和dynamicProps。
  2. 使用循环遍历props数组中的每个属性。
  3. 获取属性的值,并对特殊换行符进行转换。
  4. 如果属性是动态属性,则将属性的名称和值添加到dynamicProps字符串中。
  5. 否则,将属性的名称和值添加到staticProps字符串中。
  6. 将staticProps的末尾逗号去掉,并将其包装在花括号中。
  7. 如果dynamicProps存在,则调用_d函数生成动态属性的代码,并将staticProps和dynamicProps作为参数传递。
  8. 否则,返回staticProps。

transformSpecialNewlines函数用于将特殊换行符转换为转义序列。

  1. 使用正则表达式替换所有的"\u2028"为"\u2028"的转义序列。
  2. 使用正则表达式替换所有的"\u2029"为"\u2029"的转义序列。
  3. 返回转换后的文本。

总结

  1. 本文我们先简单的介绍了 Vue 是如何根据模板代码生成的 AST 节点的?答案是:Vue 根据模板代码生成 AST 节点的过程主要涉及词法分析和语法分析两个步骤。Vue在AST静态分析中的一些处理方式有:静态节点标记、静态属性提升、条件判断优化等。

  2. 源码部分,我们首先明确了是通过编译器将模板代码转换成抽象语法树(AST)节点的。

  3. Vue 的编译过程主要包括以下3个步骤:先解析,再优化,最后代码生成。

  4. 解析的过程使用了HTML解析器和文本解析器,通过遍历模板代码的字符来构建初始的 AST。

  5. 优化的过程编译器会对初始的 AST 进行一些优化处理,例如静态节点的标记、静态属性的提升等。

  6. 代码生成的过程,编译器会将AST转换为可执行的渲染函数。在这个过程中,编译器会遍历 AST 节点,并根据节点的类型生成相应的代码片段。例如,对于元素节点,编译器会生成创建元素的代码;对于文本节点,编译器会生成插入文本的代码。

  7. 最后,我们提及了Vue2 源码中涉及到AST 生成的5个重要的相关文件,并做了一些解读。

  8. compiler/index.ts 文件是编译器的入口文件,定义了编译器的主要逻辑。通过调用 createCompilerCreator 函数创建了一个编译器 baseCompile。该编译器接受一个模板字符串和编译选项作为参数,并返回编译结果对象 CompiledResult。

  9. compiler/create-compiler.ts 文件里创建编译器的辅助函数,用于根据不同的配置创建不同类型的编译器。

  10. compiler/parser/index.ts 文件定义了解析器的逻辑,包括 HTML 解析器和文本解析器。

  11. compiler/optimizer.ts 文件定义了优化器的逻辑,包括静态节点的标记和属性提升等优化过程。

  12. compiler/codegen/index.ts 文件定义了代码生成器的逻辑,包括根据 AST 生成渲染函数的代码。

欢迎关注,公众号回复【vue是怎样对AST进行静态分析的】获取文章的全部脑图资源。

关于我 & Node交流群

大家好,我是 Quixn,专注于 Node.js 技术栈分享,前端从 JavaScript 到 Node.js,再到后端数据库,优质文章推荐。如果你对 Node.js 学习感兴趣的话(后续有计划也可以),可以关注我,加我微信【 Quixn1314 】,拉你进交流群一起交流、学习、共建,或者关注我的公众号【 小Q全栈指南 】。Github 博客开源项目 github.com/Quixn...

欢迎加我微信【 Quixn1314 】,拉你 进 Node.js 高级进阶群,一起学Node,长期交流学习...

相关推荐
子兮曰5 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰5 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万5 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝5 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
A黄俊辉A5 天前
uniapp webview中实现 app和内嵌的H5双向通信
vue.js·json
三十而立洋5 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁5 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
李少兄5 天前
JavaScript 隐式全局变量解析
javascript
honkun65 天前
vue 表格组件 vxe-table 配置 ajax 请求自动加载数据与表单查询
vue.js·vxe-table
kybs19915 天前
全球灾害数据分析可视化 毕业设计-附源码66794
vue.js·spring boot·mysql·安全·django·c#·asp.net