共计 17152 个字符,预计需要花费 43 分钟才能阅读完成。
这一章咱们开始讲模板解析编译: 总结来说就是通过 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
}
当咱们把代码折叠起来的话会看到 parse
函数外面外围就是 parseHTML
函数,他通过 正则文法
和start
,end
,chars
,comment
四个 钩子函数
来解析模板标签
的:
// 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
}
那么咱们持续来看一下 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
对象,那么此章节到这里就完了