首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Vue模板是怎样编译的

Vue模板是怎样编译的

原创
作者头像
yyds2026
发布2022-10-19 15:49:10
9590
发布2022-10-19 15:49:10
举报
文章被收录于专栏:前端开发面经前端开发面经

这一章我们开始讲模板解析编译:总结来说就是通过compile函数把tamplate解析成render Function形式的字符串compiler/index.js

import { parse } from './parser/index'
import { optimize } from './optimizer'
import { generate } from './codegen/index'
import { createCompilerCreator } from './create-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
  }
})

我们可以看出createCompiler函数内部运行的是parseoptimizegenerate三个函数,而生成的是astrenderstaticRenderFns三个对象

parse

export function parse (
  template: string,
  options: CompilerOptions
): ASTElement | void {
  /**   * 有自定义warn用自定义没有用基础: console.error(`[Vue compiler]: ${msg}`)   */
  warn = options.warn || baseWarn
  // 检查标签是否需要保留空格
  platformIsPreTag = options.isPreTag || no
  // 检查属性是否应被绑定
  platformMustUseProp = options.mustUseProp || no
  // 检查标记的名称空间
  platformGetTagNamespace = options.getTagNamespace || no

  /**   * 获取modules中的值   */
  transforms = pluckModuleFunction(options.modules, 'transformNode')
  preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
  postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')

  delimiters = options.delimiters

  const stack = []
  // 是否保留elements直接的空白
  const preserveWhitespace = options.preserveWhitespace !== false
  let root //return 出去的AST
  let currentParent //当前父节点
  let inVPre = false
  let inPre = false
  let warned = false
  /**   * 单次警告   */
  function warnOnce (msg) {
    if (!warned) {
      warned = true
      warn(msg)
    }
  }

  function closeElement (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)
    }
  }

  parseHTML(template, {
    warn,
    expectHTML: options.expectHTML,
    isUnaryTag: options.isUnaryTag,
    canBeLeftOpenTag: options.canBeLeftOpenTag,
    shouldDecodeNewlines: options.shouldDecodeNewlines,
    shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
    shouldKeepComment: options.comments,
    start (tag, attrs, unary) {
      // check namespace.
      // inherit parent ns if there is one
      /**       * 检查命名空间。如果有父nanmespace,则继承父nanmespace       */
      const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)

      // handle IE svg bug
      /* istanbul ignore if */
      // IE的另类bug
      if (isIE && ns === 'svg') {
        attrs = guardIESVGBug(attrs)
      }
      // 返回应对的AST
      let element: ASTElement = createASTElement(tag, attrs, currentParent)
      if (ns) {
        element.ns = ns
      }
      /**       * 不是服务段渲染的时候,template 应该只负责渲染UI部分       * 不应该包含syle, script 的标签       */
      if (isForbiddenTag(element) && !isServerRendering()) {
        element.forbidden = true
        process.env.NODE_ENV !== 'production' && 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.'
        )
      }

      // 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
        // 给AST加上v-for响应属性
        processFor(element)
        // 给AST加上v-if v-else v-else-if相应属性
        processIf(element)
        // 判断是否含有v-once
        processOnce(element)
        // element-scope stuff
        processElement(element, options)
      }

      function checkRootConstraints (el) {
        if (process.env.NODE_ENV !== 'production') {
          // 根标签不应该是slot和template
          if (el.tag === 'slot' || el.tag === 'template') {
            warnOnce(
              `Cannot use <${el.tag}> as component root element because it may ` +
              'contain multiple nodes.'
            )
          }
          // 根标签不应该含有v-for
          if (el.attrsMap.hasOwnProperty('v-for')) {
            warnOnce(
              'Cannot use v-for on stateful component root element because ' +
              'it renders multiple elements.'
            )
          }
        }
      }

      // tree management
      // 赋值给跟标签
      if (!root) {
        root = element
        //  用于检查根标签
        checkRootConstraints(root)
        // 缓存中是否有值
      } else if (!stack.length) {
        // allow root elements with v-if, v-else-if and v-else
        // 如果根元素有v-if, v-else-if and v-else 则打上响应记号
        if (root.if && (element.elseif || element.else)) {
          checkRootConstraints(element)
          addIfCondition(root, {
            exp: element.elseif,
            block: element
          })
        } else if (process.env.NODE_ENV !== 'production') {
          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.`
          )
        }
      }
      if (currentParent && !element.forbidden) {
        if (element.elseif || element.else) {
          processIfConditions(element, currentParent)
        } else if (element.slotScope) { // scoped slot
          // 处理slot, scoped传值
          currentParent.plain = false
          const name = element.slotTarget || '"default"'
          ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
        } else {
          currentParent.children.push(element)
          element.parent = currentParent
        }
      }
      // 处理是否是自闭标签
      if (!unary) {
        currentParent = element
        stack.push(element)
      } else {
        closeElement(element)
      }
    },

    end () {
      // remove trailing whitespace
      const element = stack[stack.length - 1]
      const lastNode = element.children[element.children.length - 1]
      if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
        element.children.pop()
      }
      // pop stack
      stack.length -= 1
      currentParent = stack[stack.length - 1]
      closeElement(element)
    },

    chars (text: string) {
      if (!currentParent) {
        if (process.env.NODE_ENV !== 'production') {
           /**           * 当文本没有跟标签的时候           */
          if (text === template) {
            warnOnce(
              'Component template requires a root element, rather than just text.'
            )
          } else if ((text = text.trim())) {
            /**            * 需要跟标签的时候            */
            warnOnce(
              `text "${text}" outside root element will be ignored.`
            )
          }
        }
        return
      }
      // IE textarea placeholder bug
      /* istanbul ignore if */
      /**       * IE的神奇bug       * 如果textarea具有占位符,则IE会触发输入事件       */
      if (isIE &&
        currentParent.tag === 'textarea' &&
        currentParent.attrsMap.placeholder === text
      ) {
        return
      }
      const children = currentParent.children
      // 之前设置的是否需要保留空格
      text = inPre || text.trim()
        // 当为true时是不是文本标签
        ? isTextTag(currentParent) ? text : decodeHTMLCached(text)
        // only preserve whitespace if its not right after a starting tag
        : preserveWhitespace && children.length ? ' ' : ''
      if (text) {
        let res
        /**         * 当不是原内容输出时         * 并且text不是空内容         * 且AST解析时有内容返回         */  
        if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
          children.push({
            type: 2,
            expression: res.expression,
            tokens: res.tokens,
            text
          })
        } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
          children.push({
            type: 3,
            text
          })
        }
      }
    },

    comment (text: string) {
      currentParent.children.push({
        type: 3,
        text,
        isComment: true
      })
    }
  })
  return root
}

当我们把代码折叠起来的话会看到parse函数里面核心就是parseHTML函数,他通过正则文法start,end,chars,comment四个钩子函数解析模板标签的:

参考vue实战视频讲解:进入学习

// Regular Expressions for parsing tags and attributes
// 匹配attributes
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
// could use https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName
// but for Vue templates we can enforce a simple charset
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
/** * 匹配开始标签 * 例子:<XXXXXX */
const startTagOpen = new RegExp(`^<${qnameCapture}`)
/** * 匹配结束标签 * 例如(有多个空格的):     />  or XXX> */
const startTagClose = /^\s*(\/?)>/
/** * 很巧妙的匹配闭合标签的方法 * 例子 <ssss/>>>>>>>   <aw/>>>>> */
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
// #7298: escape - to avoid being pased as HTML comment when inlined in page
const comment = /^<!\--/
const conditionalComment = /^<!\[/

这些正则文法都是用来Vue中匹配开始标签结束标签属性标签名注释文本

我们知道了parseHTML(html,options){}接受俩个参数,我们再来看一下parseHTML中是如何去匹配的:

export function parseHTML (html, options) {
  const stack = []
  const expectHTML = options.expectHTML
  const isUnaryTag = options.isUnaryTag || no
  const canBeLeftOpenTag = options.canBeLeftOpenTag || no
  let index = 0
  let last, lastTag
  while (html) {
    last = html
    // Make sure we're not in a plaintext content element like script/style
    // 如果没有lastTag,并确保我们不是在一个纯文本内容元素中:script、style、textarea
    if (!lastTag || !isPlainTextElement(lastTag)) {
      // 查找<的位置
      let textEnd = html.indexOf('<')
      // 当是第一个的时候
      if (textEnd === 0) {
        // Comment:
        // 匹配注释文本
        if (comment.test(html)) {
          const commentEnd = html.indexOf('-->')

          if (commentEnd >= 0) {
            // 当要储存注释时
            if (options.shouldKeepComment) {
              options.comment(html.substring(4, commentEnd))
            }
            advance(commentEnd + 3)
            continue
          }
        }

        // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
        // 兼容另类注释 例子:<![if!IE]> 
        if (conditionalComment.test(html)) {
          const conditionalEnd = html.indexOf(']>')

          if (conditionalEnd >= 0) {
            advance(conditionalEnd + 2)
            continue
          }
        }

        // Doctype:
        // <!doctype> 这类开头
        const doctypeMatch = html.match(doctype)
        if (doctypeMatch) {
          advance(doctypeMatch[0].length)
          continue
        }

        // End tag:
        // 匹配结束标签
        const endTagMatch = html.match(endTag)
        if (endTagMatch) {
          const curIndex = index
          advance(endTagMatch[0].length)
          parseEndTag(endTagMatch[1], curIndex, index)
          continue
        }

        // Start tag:
        /**         * 获取标签里的match对象         */
        const startTagMatch = parseStartTag()
        if (startTagMatch) {
          handleStartTag(startTagMatch)
          // 是否需要需要新的一行
          if (shouldIgnoreFirstNewline(lastTag, html)) {
            advance(1)
          }
          continue
        }
      }

      let text, rest, next
      if (textEnd >= 0) {
        /**         * 接下来判断 textEnd 是否大于等于 0 的,满足则说明到从当前位置到 textEnd 位置都是文本         * 并且如果 < 是纯文本中的字符,就继续找到真正的文本结束的位置,然后前进到结束的位置。         */
        rest = html.slice(textEnd)
        while (
          !endTag.test(rest) &&
          !startTagOpen.test(rest) &&
          !comment.test(rest) &&
          !conditionalComment.test(rest)
        ) {
          // < in plain text, be forgiving and treat it as text
          next = rest.indexOf('<', 1)
          if (next < 0) break
          textEnd += next
          rest = html.slice(textEnd)
        }
        text = html.substring(0, textEnd)
        advance(textEnd)
      }
      // html解析结束了
      if (textEnd < 0) {
        text = html
        html = ''
      }

      if (options.chars && text) {
        options.chars(text)
      }
    } else {
      let endTagLength = 0
      const stackedTag = lastTag.toLowerCase()
      const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
      const rest = html.replace(reStackedTag, function (all, text, endTag) {
        endTagLength = endTag.length
        if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
          text = text
            .replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298
            .replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1')
        }
        if (shouldIgnoreFirstNewline(stackedTag, text)) {
          text = text.slice(1)
        }
        if (options.chars) {
          options.chars(text)
        }
        return ''
      })
      index += html.length - rest.length
      html = rest
      parseEndTag(stackedTag, index - endTagLength, index)
    }

    if (html === last) {
      options.chars && options.chars(html)
      if (process.env.NODE_ENV !== 'production' && !stack.length && options.warn) {
        options.warn(`Mal-formatted tag at end of template: "${html}"`)
      }
      break
    }
  }

  // Clean up any remaining tags
  parseEndTag()
  /**   * 截取html   * index记录多少个   */
  function advance (n) {
    index += n
    html = html.substring(n)
  }

  function parseStartTag () {
    const start = html.match(startTagOpen)
    if (start) {
      const match = {
        tagName: start[1], // 标签名
        attrs: [], // 属性
        start: index // 开始位置
      }
      // 去除标签名
      advance(start[0].length) 
      let end, attr
      /**       * 当不是结束标签时       * 并记录attribute       * 例如:<div @click="test"></div> 中的@click="test"       * tip: match       */
      while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
        advance(attr[0].length)
        match.attrs.push(attr)
      }
      /**       * 当匹配到结束标签时       * 返回存进去的match对象       */
      if (end) {
        match.unarySlash = end[1]
        advance(end[0].length)
        match.end = index
        return match
      }
    }
  }

  function handleStartTag (match) {
    const tagName = match.tagName
    const unarySlash = match.unarySlash
    /**     * 是否是对于web的构建     */ 
    if (expectHTML) {
      /**       * 如果当前的tag不能被p标签包含的的时候就先结束p标签       */
      if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
        parseEndTag(lastTag)
      }
      /**       * 是不是不闭合的标签       * 例子: tr td       */
      if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
        parseEndTag(tagName)
      }
    }
    /**     * 是不是自闭和标签的时候     * 例子: <img>     */
    const unary = isUnaryTag(tagName) || !!unarySlash
    // 获取属性长度属性
    const l = match.attrs.length
    const attrs = new Array(l)
    // 属性处理
    for (let i = 0; i < l; i++) {
      const args = match.attrs[i]
      // hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
      // FF上的很奇怪的bug
      if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
        if (args[3] === '') { delete args[3] }
        if (args[4] === '') { delete args[4] }
        if (args[5] === '') { delete args[5] }
      }
      const value = args[3] || args[4] || args[5] || ''
      // a标签是否需要解码 !import
      const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
        ? options.shouldDecodeNewlinesForHref
        : options.shouldDecodeNewlines
      attrs[i] = {
        name: args[1],
        // 解码
        value: decodeAttr(value, shouldDecodeNewlines)
      }
    }
    /**     * 当不是闭合标签的时候缓存该标签用于之后的循环     */
    if (!unary) {
      stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })
      lastTag = tagName
    }
    /**     * 当有start函数时     * 主要是对v-for,v-if, v-else-if,v-else,slot,scoped的处理     * 检测根标签     */
    if (options.start) {
      options.start(tagName, attrs, unary, match.start, match.end)
    }
  }

  function parseEndTag (tagName, start, end) {
    let pos, lowerCasedTagName
    if (start == null) start = index
    if (end == null) end = index

    if (tagName) {
      lowerCasedTagName = tagName.toLowerCase()
    }

    // Find the closest opened tag of the same type
    if (tagName) {
      for (pos = stack.length - 1; pos >= 0; pos--) {
        if (stack[pos].lowerCasedTag === lowerCasedTagName) {
          break
        }
      }
    } else {
      // If no tag name is provided, clean shop
      pos = 0
    }

    if (pos >= 0) {
      // Close all the open elements, up the stack
      for (let i = stack.length - 1; i >= pos; i--) {
        if (process.env.NODE_ENV !== 'production' &&
          (i > pos || !tagName) &&
          options.warn
        ) {
          options.warn(
            `tag <${stack[i].tag}> has no matching end tag.`
          )
        }
        if (options.end) {
          options.end(stack[i].tag, start, end)
        }
      }

      // Remove the open elements from the stack
      stack.length = pos
      lastTag = pos && stack[pos - 1].tag
    } else if (lowerCasedTagName === 'br') {
      if (options.start) {
        options.start(tagName, [], true, start, end)
      }
    } else if (lowerCasedTagName === 'p') {
      if (options.start) {
        options.start(tagName, [], false, start, end)
      }
      if (options.end) {
        options.end(tagName, start, end)
      }
    }
  }
}

所以整个parseHTML中的流程总结为:

  • 首先通过while (html)去循环判断html内容是否存在。
  • 再判断文本内容是否在script/style标签中
  • 上述条件都满足的话,开始解析html字符串 纸上得来终觉浅,绝知此事要躬行,那我么来实操一下如何解析一段字符串吧:
//此为测试所用节点信息
<div id="app">
    <!-- Hello 注释 -->
    <div v-if="show" class="message">{{message}}</div>
</div>

开始解析:

// Start tag:
//获取标签里的match对象
const startTagMatch = parseStartTag()
if (startTagMatch) {
    handleStartTag(startTagMatch)
// 是否需要需要新的一行
    if (shouldIgnoreFirstNewline(lastTag, html)) {
        advance(1)
    }
    continue
}

那么我们继续来看一下parseStartTaghandleStartTag两个函数分别实现了啥功能:

  function parseStartTag () {
    //判断html中是否存在开始标签
    const start = html.match(startTagOpen);
    // 定义 match 结构
    if (start) {
      const match = {
        tagName: start[1], // 标签名
        attrs: [], // 属性
        start: index // 开始位置
      }
      // 去除标签名
      advance(start[0].length) 
      let end, attr
      /**       * 当不是结束标签时       * 并记录attribute       * 例如:<div @click="test"></div> 中的@click="test"       * tip: match       */
      while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
        advance(attr[0].length)
        match.attrs.push(attr)
      }
      /**       * 当匹配到结束标签时       * 返回存进去的match对象       */
      if (end) {
        match.unarySlash = end[1]
        advance(end[0].length)
        match.end = index
        return match
      }
    }
  }

我们再来看看解析过程中是如何一个字符一个字符的匹配html字符串的:

  /** * 截取html * index记录多少个 */
function advance (n) {
  index += n
  html = html.substring(n)
}

//通过传入变量n截取字符串,这也是Vue解析的重要方法,通过不断地分割html字符串,一步步完成对他的解析过程。

那么我们再回到parseStartTag上,首先开始匹配开始标签那入栈的是

{
    attrs: [
        {
            0: " id="app"",
            1: "id",
            2: "=",
            3: "app",
            4: undefined,
            5: undefined,
            end: 13,
            groups: undefined,
            index: 0,
            input: " id="app">↵        <!-- 注释 -->↵        <div v-if="show" class="message">{{message}}</div>↵    </div>",
            start: 4,
        }
    ],
    end: 14,
    start: 0,
    tagName: "div",
    unarySlash: "",
}
//目前代码
<!-- 注释 -->
    <div v-if="show" class="message">{{message}}</div>
</div>

再者匹配到注释:

// 匹配注释文本
if (comment.test(html)) {
    const commentEnd = html.indexOf('-->')
    if (commentEnd >= 0) {
    // 当要储存注释时
        if (options.shouldKeepComment) {
        options.comment(html.substring(4, commentEnd))
        }
    advance(commentEnd + 3)
    continue
    }
}

处理成:

//目前代码
    <div v-if="show" class="message">{{message}}</div>
</div>

然后继续处理标签节点<div v-if="show" class="message">,再处理{{message}}之后模板变成

//目前代码
    </div>
</div>

tamplate已经是只剩下结束标签了,那么毫无疑问就会走到parseEndTag函数:

// End tag:
// 匹配结束标签
const endTagMatch = html.match(endTag)
if (endTagMatch) {
        const curIndex = index
        advance(endTagMatch[0].length)
        parseEndTag(endTagMatch[1], curIndex, index)
        continue
}

那么在handStartTaghandEndTag中分别调用了options.start options.end钩子函数,而在start钩子函数中直接调用createASTElement函数(语法分析阶段):

export function createASTElement (
  tag: string,
  attrs: Array<Attr>,
  parent: ASTElement | void
): ASTElement {
  return {
    type: 1,
    tag,
    attrsList: attrs,
    attrsMap: makeAttrsMap(attrs),
    parent,
    children: []
  }
}
......
start(){
    ......
    //创建ast基础对象
    let element: ASTElement = createASTElement(tag, attrs, currentParent);
      ......
        处理服务端渲染
        预处理一些动态类型:v-model
        对vue的指令进行处理v-pre、v-if、v-for、v-once、slot、key、ref
        限制处理根节点不能是slot,template,v-for这类标签
        处理是否是自闭标签

}

那么就解析完了整个tamplate变成了AST:

{
  "type": 0,
  "children": [
    {
      "type": 1,
      "ns": 0,
      "tag": "div",
      "tagType": 0,
      "props": [
        {
          "type": 6,
          "name": "id",
          "value": {
            "type": 2,
            "content": "app",
            "loc": {
              "start": {
                "column": 9,
                "line": 1,
                "offset": 8
              },
              "end": {
                "column": 14,
                "line": 1,
                "offset": 13
              },
              "source": "\"app\""
            }
          },
          "loc": {
            "start": {
              "column": 6,
              "line": 1,
              "offset": 5
            },
            "end": {
              "column": 14,
              "line": 1,
              "offset": 13
            },
            "source": "id=\"app\""
          }
        }
      ],
      "isSelfClosing": false,
      "children": [
        {
          "type": 1,
          "ns": 0,
          "tag": "div",
          "tagType": 0,
          "props": [
            {
              "type": 7,
              "name": "if",
              "exp": {
                "type": 4,
                "content": "show",
                "isStatic": false,
                "isConstant": false,
                "loc": {
                  "start": {
                    "column": 16,
                    "line": 3,
                    "offset": 52
                  },
                  "end": {
                    "column": 20,
                    "line": 3,
                    "offset": 56
                  },
                  "source": "show"
                }
              },
              "modifiers": [],
              "loc": {
                "start": {
                  "column": 10,
                  "line": 3,
                  "offset": 46
                },
                "end": {
                  "column": 21,
                  "line": 3,
                  "offset": 57
                },
                "source": "v-if=\"show\""
              }
            },
            {
              "type": 6,
              "name": "class",
              "value": {
                "type": 2,
                "content": "message",
                "loc": {
                  "start": {
                    "column": 28,
                    "line": 3,
                    "offset": 64
                  },
                  "end": {
                    "column": 37,
                    "line": 3,
                    "offset": 73
                  },
                  "source": "\"message\""
                }
              },
              "loc": {
                "start": {
                  "column": 22,
                  "line": 3,
                  "offset": 58
                },
                "end": {
                  "column": 37,
                  "line": 3,
                  "offset": 73
                },
                "source": "class=\"message\""
              }
            }
          ],
          "isSelfClosing": false,
          "children": [
            {
              "type": 5,
              "content": {
                "type": 4,
                "isStatic": false,
                "isConstant": false,
                "content": "message",
                "loc": {
                  "start": {
                    "column": 40,
                    "line": 3,
                    "offset": 76
                  },
                  "end": {
                    "column": 47,
                    "line": 3,
                    "offset": 83
                  },
                  "source": "message"
                }
              },
              "loc": {
                "start": {
                  "column": 38,
                  "line": 3,
                  "offset": 74
                },
                "end": {
                  "column": 49,
                  "line": 3,
                  "offset": 85
                },
                "source": "{{message}}"
              }
            }
          ],
          "loc": {
            "start": {
              "column": 5,
              "line": 3,
              "offset": 41
            },
            "end": {
              "column": 55,
              "line": 3,
              "offset": 91
            },
            "source": "<div v-if=\"show\" class=\"message\">{{message}}</div>"
          }
        }
      ],
      "loc": {
        "start": {
          "column": 1,
          "line": 1,
          "offset": 0
        },
        "end": {
          "column": 7,
          "line": 4,
          "offset": 98
        },
        "source": "<div id=\"app\">\n    <!-- Hello 注释 -->\n    <div v-if=\"show\" class=\"message\">{{message}}</div>\n</div>"
      }
    }
  ],
  "helpers": [],
  "components": [],
  "directives": [],
  "hoists": [],
  "imports": [],
  "cached": 0,
  "temps": 0,
  "loc": {
    "start": {
      "column": 1,
      "line": 1,
      "offset": 0
    },
    "end": {
      "column": 7,
      "line": 4,
      "offset": 98
    },
    "source": "<div id=\"app\">\n    <!-- Hello 注释 -->\n    <div v-if=\"show\" class=\"message\">{{message}}</div>\n</div>"
  }
}

咱们也可以去AST Explorer上面去尝试

这是tamplate经过解析的第一步,生成了一个AST对象,那么此章节到这里就完了

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • parse
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档