共计 17155 个字符,预计需要花费 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
四个 钩子函数
来解析模板标签
的:
参考 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 | |
} |
那么咱们持续来看一下 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
对象,那么此章节到这里就完了