Vue 模板是怎样编译的
- 2022-11-10  浙江
- 本文字数:14181 字 - 阅读完需:约 47 分钟 
这一章我们开始讲模板解析编译:总结来说就是通过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函数内部运行的是parse、optimize、generate三个函数,而生成的是ast,render,staticRenderFns三个对象
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}
参考 vue 实战视频讲解:进入学习
当我们把代码折叠起来的话会看到parse函数里面核心就是parseHTML函数,他通过正则文法和start,end,chars,comment四个钩子函数来解析模板标签的:
// Regular Expressions for parsing tags and attributes// 匹配attributesconst 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 charsetconst 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 pageconst 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}
那么我们继续来看一下parseStartTag,handleStartTag两个函数分别实现了啥功能:
  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}
那么在handStartTag与handEndTag中分别调用了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对象,那么此章节到这里就完了

yyds2026
还未添加个人签名 2022-09-08 加入
还未添加个人简介









 
    
评论