🚀🚀🚀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的结构和节点信息来提前了解模板中的各种情况,并作出相应的优化。

VueAST静态分析中的一些处理方式:

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

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

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

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

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 ,并返回一个对象。

返回的对象包含两个方法: compilecompileToFunctions

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

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

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

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

这段代码的作用是:

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

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 函数用于判断元素是否是文本标签。如果元素的标签是 scriptstyle ,则返回 true ,否则返回 false

  • isForbiddenTag(el): boolean 函数用于判断元素是否是禁止的标签。如果元素的标签是 stylescript ,且 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. 定义了一些辅助函数和变量,包括 makeMapisBuiltInTagcachedno 等。
  2. 使用 genStaticKeysCached 函数生成静态键的缓存版本。
  3. 定义了 optimize 函数,接受两个参数:根节点 root 和编译选项 options
  4. 如果根节点不存在,则直接返回。
  5. 初始化一些变量 isStaticKeyisPlatformReservedTag,用于判断是否为静态节点以及是否为保留标签。
  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,长期交流学习...

相关推荐
cs_dn_Jie14 分钟前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic1 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿1 小时前
webWorker基本用法
前端·javascript·vue.js
cy玩具1 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
customer082 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
清灵xmf2 小时前
TypeScript 类型进阶指南
javascript·typescript·泛型·t·infer
小白学大数据2 小时前
JavaScript重定向对网络爬虫的影响及处理
开发语言·javascript·数据库·爬虫
qq_390161772 小时前
防抖函数--应用场景及示例
前端·javascript
334554323 小时前
element动态表头合并表格
开发语言·javascript·ecmascript
John.liu_Test3 小时前
js下载excel示例demo
前端·javascript·excel