comprehensive
A virtual DOM requires a VNode, so where does a VNode come from?
A VNode is produced by compiling user-written templates
Vue compiles what looks like native HTML that the user wrote in the
tag
After a series of logical processing to generate the render function, also known as the render function
The Render function generates the template content to the corresponding VNode
The user in
tag to write similar to the content of native HTML to compile, find out the content of native HTML, and then find out the non-native HTML, after a series of logical processing to generate the rendering function, that is, the render function of this section of the process is called template compilation process.
Internal processes
Abstract syntax tree AST
Online Test AST
process
After parsing a bunch of string templates into an abstract syntax tree AST, we can perform various manipulations on them, and then use the AST to generate the Render function
- Template parsing stage: a bunch of template strings are parsed into abstract syntax trees by means of re
AST
; - Optimization phase: traversal
AST
, find out the static nodes and mark them; - Code generation phase: will
AST
Convert to render functions;
Template parser SRC/compiler/parser/index, js
Parser parser
There can’t be just one parser, it should have an HTML parser that parses regular HTML, but also a text parser that parses text and a filter parser that parses text if it contains filters
/* The main function in this file converts HTML templates into AST trees */
export function parse (template: string, options: CompilerOptions) :ASTElement | void { warn = options.warn || baseWarn platformIsPreTag = options.isPreTag || no platformMustUseProp = options.mustUseProp || no platformGetTagNamespace = options.getTagNamespace || noconst isReservedTag = options.isReservedTag || no
maybeComponent = (el: ASTElement) = >!!!!! ( el.component || el.attrsMap[':is'] ||
el.attrsMap['v-bind:is') | |! (el.attrsMap.is ? isReservedTag(el.attrsMap.is) : isReservedTag(el.tag)) ) transforms = pluckModuleFunction(options.modules,'transformNode')
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')
delimiters = options.delimiters
const stack = []
constpreserveWhitespace = options.preserveWhitespace ! = =false
const whitespaceOption = options.whitespace
let root
let currentParent
let inVPre = false
let inPre = false
let warned = false
function warnOnce (msg, range) {
if(! warned) { warned =true
warn(msg, range)
}
}
function closeElement (element) {
trimEndingWhitespace(element)
if(! inVPre && ! element.processed) { element = processElement(element, options) }// tree management
if(! stack.length && element ! == root) {// allow root elements with v-if, v-else-if and v-else
if (root.if && (element.elseif || element.else)) {
if(process.env.NODE_ENV ! = ='production') {
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.`,
{ start: element.start }
)
}
}
if(currentParent && ! element.forbidden) {if (element.elseif || element.else) {
processIfConditions(element, currentParent)
} else {
if (element.slotScope) {
// scoped slot
// keep it in the children list so that v-else(-if) conditions can
// find it as the prev node.
const name = element.slotTarget || '"default"'; (currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element } currentParent.children.push(element) element.parent = currentParent } }// final children cleanup
// filter out scoped slots
element.children = element.children.filter(c= >! (c: any).slotScope)// remove trailing whitespace node again
trimEndingWhitespace(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)
}
}
function trimEndingWhitespace (el) {
// remove trailing whitespace node
if(! inPre) {let lastNode
while (
(lastNode = el.children[el.children.length - 1]) &&
lastNode.type === 3 &&
lastNode.text === ' '
) {
el.children.pop()
}
}
}
function checkRootConstraints (el) {
if (el.tag === 'slot' || el.tag === 'template') {
warnOnce(
`Cannot use <${el.tag}> as component root element because it may ` +
'contain multiple nodes.',
{ start: el.start }
)
}
if (el.attrsMap.hasOwnProperty('v-for')) {
warnOnce(
'Cannot use v-for on stateful component root element because ' +
'it renders multiple elements.',
el.rawAttrsMap['v-for']
)
}
}
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
outputSourceRange: options.outputSourceRange,
start (tag, attrs, unary, start, end) {
// check namespace.
// inherit parent ns if there is one
const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
// handle IE svg bug
/* istanbul ignore if */
if (isIE && ns === 'svg') {
attrs = guardIESVGBug(attrs)
}
let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {
element.ns = ns
}
if(process.env.NODE_ENV ! = ='production') {
if (options.outputSourceRange) {
element.start = start
element.end = end
element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) = > {
cumulated[attr.name] = attr
return cumulated
}, {})
}
attrs.forEach(attr= > {
if (invalidAttributeRE.test(attr.name)) {
warn(
`Invalid dynamic argument expression: attribute names cannot contain ` +
`spaces, quotes, <, >, / or =.`,
{
start: attr.start + attr.name.indexOf(` [`),
end: attr.start + attr.name.length
}
)
}
})
}
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.',
{ start: element.start }
)
}
// 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
processFor(element)
processIf(element)
processOnce(element)
}
if(! root) { root = elementif(process.env.NODE_ENV ! = ='production') {
checkRootConstraints(root)
}
}
if(! unary) { currentParent = element stack.push(element) }else {
closeElement(element)
}
},
end (tag, start, end) {
const element = stack[stack.length - 1]
// pop stack
stack.length -= 1
currentParent = stack[stack.length - 1]
if(process.env.NODE_ENV ! = ='production' && options.outputSourceRange) {
element.end = end
}
closeElement(element)
},
chars (text: string, start: number, end: number) {
if(! currentParent) {if(process.env.NODE_ENV ! = ='production') {
if (text === template) {
warnOnce(
'Component template requires a root element, rather than just text.',
{ start }
)
} else if ((text = text.trim())) {
warnOnce(
`text "${text}" outside root element will be ignored.`,
{ start }
)
}
}
return
}
// IE textarea placeholder bug
/* istanbul ignore if */
if (isIE &&
currentParent.tag === 'textarea' &&
currentParent.attrsMap.placeholder === text
) {
return
}
const children = currentParent.children
if (inPre || text.trim()) {
text = isTextTag(currentParent) ? text : decodeHTMLCached(text)
} else if(! children.length) {// remove the whitespace-only node right after an opening tag
text = ' '
} else if (whitespaceOption) {
if (whitespaceOption === 'condense') {
// in condense mode, remove the whitespace node if it contains
// line break, otherwise condense to a single space
text = lineBreakRE.test(text) ? ' ' : ' '
} else {
text = ' '}}else {
text = preserveWhitespace ? ' ' : ' '
}
if (text) {
if(! inPre && whitespaceOption ==='condense') {
// condense consecutive whitespaces into single space
text = text.replace(whitespaceRE, ' ')}let res
letchild: ? ASTNode// If a text message is encountered, the text parser parseText function is called for text parsing
if(! inVPre && text ! = =' ' && (res = parseText(text, delimiters))) {
child = {
type: 2.expression: res.expression,
tokens: res.tokens,
text
}
} else if(text ! = =' '| |! children.length || children[children.length -1].text ! = =' ') {
child = {
type: 3,
text
}
}
if (child) {
if(process.env.NODE_ENV ! = ='production' && options.outputSourceRange) {
child.start = start
child.end = end
}
children.push(child)
}
}
},
comment (text: string, start, end) {
// adding anything as a sibling to the root node is forbidden
// comments should still be allowed, but ignored
if (currentParent) {
const child: ASTText = {
type: 3,
text,
isComment: true
}
if(process.env.NODE_ENV ! = ='production' && options.outputSourceRange) {
child.start = start
child.end = end
}
currentParent.children.push(child)
}
}
})
return root
}
Copy the code
Template parsing is actually based on the characteristics of the content to be parsed using regular and other ways to extract effective information parsing, according to the different parsing content is divided into HTML parser, text parser and filter parser. And text information and filter information exists in the HTML tags, so first in the parser main function parse HTML parser parseHTML function called the template parsing a string, if encountered during parsing text or filter information and calls the corresponding parser for parsing, finishing on the interpretation of the template string