$mount = $mount = $mount = $mount = $mount = $mount In this chapter, we begin to talk about template parsing and compilation: to sum up, we use the compile Function to parse tamplate into the render Function form string 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
}
})
Copy the code
The createCompiler function runs parse, optimize, generate and generates ast, render, staticRenderFns
parse
export function parse (template: string, options: CompilerOptions) :ASTElement | void {
Error (' [Vue compiler]: ${MSG} ') */
warn = options.warn || baseWarn
// Check whether the label needs to be blank
platformIsPreTag = options.isPreTag || no
// Check if the property should be bound
platformMustUseProp = options.mustUseProp || no
// Check the tag's namespace
platformGetTagNamespace = options.getTagNamespace || no
/** * gets the value */ in modules
transforms = pluckModuleFunction(options.modules, 'transformNode')
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')
delimiters = options.delimiters
const stack = []
// Whether to leave elements directly blank
constpreserveWhitespace = options.preserveWhitespace ! = =false
let root // Return Outbound AST
let currentParent // Current parent node
let inVPre = false
let inPre = false
let warned = false
/** * Single warning */
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
/** * Check the namespace. If there is a parent nanmespace, the parent nanmespace */ is inherited
const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
// handle IE svg bug
/* istanbul ignore if */
// Internet Explorer bug
if (isIE && ns === 'svg') {
attrs = guardIESVGBug(attrs)
}
// Returns the desired AST
let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {
element.ns = ns
}
/** * Template should only be responsible for rendering the UI part * and should not contain syle, script tags */
if(isForbiddenTag(element) && ! isServerRendering()) { element.forbidden =trueprocess.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
/ / pretreatment
for (let i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element
}
if(! inVPre) { processPre(element)if (element.pre) {
inVPre = true}}// Check whether the label needs to be whitespace
if (platformIsPreTag(element.tag)) {
inPre = true
}
if (inVPre) {
// When no translation is required
processRawAttrs(element)
} else if(! element.processed) {// structural directives
// Add v-for to the AST
processFor(element)
// Add v-if v-else v-else-if to AST
processIf(element)
// Check whether v-once exists
processOnce(element)
// element-scope stuff
processElement(element, options)
}
function checkRootConstraints (el) {
if(process.env.NODE_ENV ! = ='production') {
// The root tags should not be slot and template
if (el.tag === 'slot' || el.tag === 'template') {
warnOnce(
`Cannot use <${el.tag}> as component root element because it may ` +
'contain multiple nodes.')}// The root tag should not contain 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
// Assign a value to the tag
if(! root) { root = element// Check the root label
checkRootConstraints(root)
// Whether there is a value in the cache
} else if(! stack.length) {// allow root elements with v-if, v-else-if and v-else
// If the root element has v-if, V-else -if and v-else, the response token is marked
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
// Handle slot, scoped pass value
currentParent.plain = false
const name = element.slotTarget || '"default"'; (currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element }else {
currentParent.children.push(element)
element.parent = currentParent
}
}
// Handle whether the tag is closed
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') {
/** ** when the text is not tagged */
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 */
/** * Internet Explorer triggers input events */ if textarea has placeholders
if (isIE &&
currentParent.tag === 'textarea' &&
currentParent.attrsMap.placeholder === text
) {
return
}
const children = currentParent.children
// Whether the previous Settings need to keep Spaces
text = inPre || text.trim()
// Is not a text label when true
? isTextTag(currentParent) ? text : decodeHTMLCached(text)
// only preserve whitespace if its not right after a starting tag
: preserveWhitespace && children.length ? ' ' : ' '
if (text) {
let res
/** * When the original content is not output * and the text is not empty * and the AST parses the content return */
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
}
Copy the code
The parse function uses the regular grammar and the start,end,chars, and comment hook functions to parse template tags:
// Regular Expressions for parsing tags and attributes
/ / to match the 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}) `
/** * Matches the start tag * example:
const startTagOpen = new RegExp(` ^ <${qnameCapture}`)
/** * Matches the end tag * for example (with multiple Spaces): /> or XXX> */
const startTagClose = /^\s*(\/?) >/
/ * * * is very clever method of matching closing tag * example < SSSS / > > > > > > > < aw / > > > > > * /
const endTag = new RegExp(` ^ < \ \ /${qnameCapture}[^ >] * > `)
const doctype = / ^
]+>/i
// #7298: escape - to avoid being pased as HTML comment when inlined in page
const comment = / ^
const conditionalComment = / ^
Copy the code
These re grammars are used in Vue to match start tags, end tags, attributes, tag names, comments, text, etc
Now that parseHTML(HTML,options){} takes two arguments, let’s look at how parseHTML matches:
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
// If there is no lastTag, make sure we are not in a plain text content element: script, style, textarea
if(! lastTag || ! isPlainTextElement(lastTag)) {// find the position of <
let textEnd = html.indexOf('<')
// When it is the first one
if (textEnd === 0) {
// Comment:
// Matches the comment text
if (comment.test(html)) {
const commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
// When storing comments
if (options.shouldKeepComment) {
options.comment(html.substring(4, commentEnd))
}
advance(commentEnd + 3)
continue}}// http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
// Compatible with alternative comments:
if (conditionalComment.test(html)) {
const conditionalEnd = html.indexOf('] > ')
if (conditionalEnd >= 0) {
advance(conditionalEnd + 2)
continue}}// Doctype:
/ /
something like that
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
advance(doctypeMatch[0].length)
continue
}
// End tag:
// Matches the end tag
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
// Start tag:
/** * get the match object */ in the tag
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
// Whether a new line is required
if (shouldIgnoreFirstNewline(lastTag, html)) {
advance(1)}continue}}let text, rest, next
if (textEnd >= 0) {
/** * If the textEnd is greater than or equal to 0, then all text * from the current position to the textEnd position is text * and if < is a character in plain text, continue to find the actual end of the text and proceed to the end of the text. * /
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 parsing is complete
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(/
/g.'$1') / / # 7298
.replace(/
/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()
/** ** how many */ HTML * index is truncated
function advance (n) {
index += n
html = html.substring(n)
}
function parseStartTag () {
const start = html.match(startTagOpen)
if (start) {
const match = {
tagName: start[1]./ / tag name
attrs: []./ / property
start: index // Start position
}
// Remove the label name
advance(start[0].length)
let end, attr
/** * when not the end tag * and record the attribute * for example: <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)
}
/** * Returns the saved match object */ when matched to the end tag
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
/** * whether to build for the Web */
if (expectHTML) {
/** * End the p tag */ if the current tag cannot be contained by the P tag
if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
parseEndTag(lastTag)
}
/** * is not a closed label * example: tr td */
if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
parseEndTag(tagName)
}
}
/** * is not closed and tags when * example: */
constunary = isUnaryTag(tagName) || !! unarySlash// Get the attribute length attribute
const l = match.attrs.length
const attrs = new Array(l)
// Attribute processing
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
// a strange bug on FF
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] | |' '
// Whether a tag needs to be decoded! import
const shouldDecodeNewlines = tagName === 'a' && args[1= = ='href'
? options.shouldDecodeNewlinesForHref
: options.shouldDecodeNewlines
attrs[i] = {
name: args[1]./ / decoding
value: decodeAttr(value, shouldDecodeNewlines)
}
}
/** * Caches the tag when it is not closed for subsequent loops */
if(! unary) { stack.push({tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })
lastTag = tagName
}
/** * For, V-if, V-else,v-else,slot,scoped when start is available */
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)
}
}
}
}
Copy the code
So the entire process in parseHTML is summarized as follows:
- First of all by
while (html)
Decyclic judgmenthtml
Whether the content exists. - Then determine whether the text content is in
script/style
In the label - If all of the above conditions are met, start parsing
html
string
On paper, we have to learn how to parse a string:
// This is the node information used for testing<div id="app">
<! -- Hello comment -->
<div v-if="show" class="message">{{message}}</div>
</div>
Copy the code
Begin to parse:
// Start tag:
// Get the match object in the tag
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
// Whether a new line is required
if (shouldIgnoreFirstNewline(lastTag, html)) {
advance(1)}continue
}
Copy the code
So let’s look at the parseStartTag and handleStartTag functions and see what they do:
function parseStartTag () {
// Check whether there is a start tag in the HTML
const start = html.match(startTagOpen);
// Define the match structure
if (start) {
const match = {
tagName: start[1]./ / tag name
attrs: []./ / property
start: index // Start position
}
// Remove the label name
advance(start[0].length)
let end, attr
/** * when not the end tag * and record the attribute * for example: <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)
}
/** * Returns the saved match object */ when matched to the end tag
if (end) {
match.unarySlash = end[1]
advance(end[0].length)
match.end = index
return match
}
}
}
Copy the code
Let’s look at how the parsing process matches the HTML string character by character:
/** ** how many */ HTML * index is truncated
function advance (n) {
index += n
html = html.substring(n)
}
Copy the code
// Pass the variable n to the string, this is also an important method of Vue parsing, by constantly splitting the HTML string, step by step to complete the parsing process. So let’s go back to parseStartTag, and start matching the start tag that’s pushed
{
attrs: [{0: " id="app"".1: "id".2: "=".3: "app".4: undefined.5: undefined.end: 13.groups: undefined.index: 0.input: " id="app"> ↵ <! -- Annotation --> Address <div V-If ="show" class="message"> {{message}} < / div > ↵ < / div >".start: 4,}],end: 14.start: 0.tagName: "div".unarySlash: "",}Copy the code
// Current code<! - comments -- ><div v-if="show" class="message">{{message}}</div>
</div>
Copy the code
Again matching to comments:
// Matches the comment text
if (comment.test(html)) {
const commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
// When storing comments
if (options.shouldKeepComment) {
options.comment(html.substring(4, commentEnd))
}
advance(commentEnd + 3)
continue}}Copy the code
Processing:
// Current code<div v-if="show" class="message">{{message}}</div>
</div>
Copy the code
Then proceed to the tag node
// Current code</div>
</div>
Copy the code
If you look at the tamplate, the only thing left is the closing tag, then you will no doubt go to the parseEndTag function:
// End tag:
// Matches the end tag
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
Copy the code
Options. start options.end is called in the handStartTag and handEndTag respectively, and createASTElement is called in the start hook.
export function createASTElement (
tag: string,
attrs: Array<Attr>,
parent: ASTElement | void
) :ASTElement {
return {
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
parent,
children: []}}......start(){...// Create the AST base object
letelement: ASTElement = createASTElement(tag, attrs, currentParent); . Processing server render preprocessing some dynamic types: V-model processing of vue instructions V-pre, V -if, v -forThe root node cannot be slot, template, v-forThis type of tag processing is not closed tag}Copy the code
So you parse the entire tamplate and it becomes an 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 comments -- > \ 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 comments -- > \ n < div v - if = \ "show \" class = \ "message \" > {{message}} < / div > \ n < / div >"}}Copy the code
We can also go to the AST Explorer to try
This is the first step in the parsing of the tamplate, which generates an AST object, so this chapter is finished, so the next chapter we will discuss the optimization of our Vue mechanism during the parsing process