After parsing the setup method and proxy data, setupState and CTX properties are set to get and set hook functions, the template template is compiled (in this example).
In this case
<div id='app'>
{{message}}
<button @click="modifyMessage">Modify the data</button>
</div>
Copy the code
Template template compiler entry
The finishComponentSetup method is executed after the setup function is executed and some proxy hooks are set
export function finishComponentSetup(instance: ComponentInternalInstance, isSSR: boolean, skipOptions? : boolean) {
const Component = instance.type as ComponentOptions
// ...
if (__NODE_JS__ && isSSR) {}
else if(! instance.render) {if(compile && ! Component.render) {const template = (__COMPAT__ && instance.vnode.props && instance.vnode.props['inline-template']) || Component.template
if (template) {
// ...
const { isCustomElement, compilerOptions } = instance.appContext.config
const { delimiters, compilerOptions: componentCompilerOptions } = Component
const finalCompilerOptions: CompilerOptions = extend(
extend(
{
isCustomElement,
delimiters
},
compilerOptions), componentCompilerOptions)
// ...
Component.render = compile(template, finalCompilerOptions)
// ...}}// ...
}
// ... support for 2.x options
}
Copy the code
Component = instance.type, which is the type property of the original VNode object passed in to createApp. And the template attribute is the internal HTML string of the root node (assigned when the new app.mount method is called). After determining whether the Render attribute is absent and if template is present, the compile method is called, taking the template template string as its first argument and some properties as its second argument (which will be analyzed later in the parsing process). Now, what does the compile function do?
// registerRuntimeCompiler method definition
export function registerRuntimeCompiler(_compile: any) {
compile = _compile
}
// registerRuntimeCompiler method call location
registerRuntimeCompiler(compileToFunction)
Copy the code
You can see that the initialization call to registerRuntimeCompiler assigns the compileToFunction method to compile, so you’re actually executing compileToFunction.
function compileToFunction(template: string | HTMLElement, options? : CompilerOptions) :RenderFunction {
if(! isString(template)) {// ...
}
const key = template
const cached = compileCache[key]
if (cached) {
return cached
}
if (template[0= = =The '#') { / *... * / }
const { code } = compile(
template,
extend(
{
hoistStatic: true.onError: __DEV__ ? onError : undefined.onWarn: __DEV__ ? e= > onError(e, true) : NOOP
} as CompilerOptions,
options
)
)
}
Copy the code
The function compileToFunction checks whether the same template already exists in cached. If it does, it returns the result, and if it does not, compile is called.
export function compile(template: string, options: CompilerOptions = {}) :CodegenResult {
return baseCompile(
template,
extend({}, parserOptions, options, {
nodeTransforms: [
// ignore <script> and <tag>
// this is not put inside DOMNodeTransforms because that list is used
// by compiler-ssr to generate vnode fallback branchesignoreSideEffectTags, ... DOMNodeTransforms, ... (options.nodeTransforms || []) ],directiveTransforms: extend(
{},
DOMDirectiveTransforms,
options.directiveTransforms || {}
),
transformHoist: __BROWSER__ ? null : stringifyStatic
})
)
}
export function baseCompile(template: string | RootNode, options: CompilerOptions = {}) :CodegenResult {
// ...
const ast = isString(template) ? baseParse(template, options) : template
// ...
}
export function baseParse(content: string, options: ParserOptions = {}) :RootNode {
const context = createParserContext(content, options)
const start = getCursor(context)
return createRoot(
parseChildren(context, TextModes.DATA/* 0 */, []),
getSelection(context, start)
)
}
// An enumerated type defined by TextModes
export const enum TextModes {
// | Elements | Entities | End sign | Inside of
DATA, / / | ✔ | ✔ | End tags of ancestors |
RCDATA, / / | ✘ | ✔ | End tag of the parent | < textarea >
RAWTEXT, / / | ✘ | ✘ | End tag of the parent | < style >, < script >
CDATA,
ATTRIBUTE_VALUE
}
Copy the code
The compile function actually ends up calling the baseParse method, first creating a context object by calling createParserContext
/ / the context object
const options = extend({}, defaultParserOptions)
for (key in rawOptions) {
// @ts-ignore
options[key] = rawOptions[key] === undefined ? defaultParserOptions[key] : rawOptions[key]
}
// ...
{
options,
column: 1.line: 1.offset: 0.originalSource: content,
source: content,
inPre: false.inVPre: false.onWarn: options.onWarn
}
Copy the code
The template is assigned to the originalSource and source attributes, which are often used in subsequent template parsing, followed by column, line, and offset, which are used to label template location information.
Call getCursor to return position information object {column, line, offset}
Finally, the createRoot method is called. The first argument to call the createRoot method is the return value of the parseChildren method, and the function begins parsing the template. Let’s look at the internal implementation of this function based on this example.
ParseChildren Parses the template
function parseChildren(
context: ParserContext,
mode: TextModes, // TextModes.DATA 0
ancestors: ElementNode[] // [] Empty array
) :TemplateChildNode[] {
const parent = last(ancestors)
const ns = parent ? parent.ns : Namespaces.HTML
const nodes: TemplateChildNode[] = []
while(! isEnd(context, mode, ancestors)) {const s = context.source
let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined
if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
// ...
}
// ...
}
// ...
}
// The isEnd method to determine whether the template is finished
function isEnd(context: ParserContext, mode: TextModes, ancestors: ElementNode[]) :boolean {
const s = context.source
switch (mode) {
case TextModes.DATA:
if (startsWith(s, '< /')) {
// TODO: probably bad performance
for (let i = ancestors.length - 1; i >= 0; --i) {
if (startsWithEndTagOpen(s, ancestors[i].tag)) {
return true}}}break
// ...}}Copy the code
You can see that inside the parseChildren function is a while loop that parses the template. The condition for the while loop is the return value of the isEnd method to determine whether the template has resolved to the end. When mode is 0, the isEnd method flows as follows:
Then we go inside the while loop and say mode is 0 | 1 and execute the if statement (in this case mode is 0).
// The inVPre property is false when the context object is created
if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
if(! context.inVPre && startsWith(s, context.options.delimiters[0])) {
node = parseInterpolation(context, mode)
} else if (mode === TextModes.DATA && s[0= = ='<') { / *... * /}}if(! node) { node = parseText(context, mode) }if (isArray(node)) {
for (let i = 0; i < node.length; i++) {
pushNode(nodes, node[i])
}
} else {
pushNode(nodes, node)
}
Copy the code
In createParserContext, the Options object is merged from the rawOptions object passed in and the defaultParserOptions object, so delimiters is [{{,}}], so the process is executed internally while
1, startsWith (s, the context. The options. Delimiters [0]) to judge whether the “{{” at the beginning (” {{” is the reference variables in Vue logo, 2, s[0] === ‘<‘ is the first character “<” (“<” is the beginning of the HTML tag element, ParseText (context, mode); parseText(context, mode);
ParseText parses text, newline characters
In this example the first parsing is the newline character, look at the internal implementation of the parseText method
function parseTextData(context: ParserContext, length: number, mode: TextModes) :string {
const rawText = context.source.slice(0, length)
advanceBy(context, length)
if (
mode === TextModes.RAWTEXT ||
mode === TextModes.CDATA ||
rawText.indexOf('&') = = = -1
) {
return rawText
} else {
// ...}}function parseText(context: ParserContext, mode: TextModes) :TextNode {
// ...
const endTokens = ['<', context.options.delimiters[0]]
// ...
let endIndex = context.source.length
for (let i = 0; i < endTokens.length; i++) {
const index = context.source.indexOf(endTokens[i], 1)
if(index ! = = -1 && endIndex > index) {
endIndex = index
}
}
// ...
const start = getCursor(context)
const content = parseTextData(context, endIndex, mode)
return {
type: NodeTypes.TEXT, // 2 text Text
content,
loc: getSelection(context, start)
}
}
Copy the code
ParseText finds the location of the closest “<” or “{{” in the template as indexOf, and then calls parseTextData with context, the subscript of the closest “<” or “{{“, and mode(0), The parseTextData method uses the slice method to fetch the contents between 0 and the nearest “<” or “{{” (possibly the contents of fixed text or a newline), and then calls advanceBy(context, length)
function advanceBy(context: ParserContext, numberOfCharacters: number) :void {
const { source } = context
__TEST__ && assert(numberOfCharacters <= source.length)
advancePositionWithMutation(context, source, numberOfCharacters)
context.source = source.slice(numberOfCharacters)
}
Copy the code
The main function of this method is to get the numberOfCharacters from the parsed content to the end of the template string by slice and reassign to context.source. Will call advancePositionWithMutation method modified position parameters (is actually changing the offset, the line, the value of the column, the length of the offset plus numberOfCharacters known content, if you meet a newline line++, etc. This method is not parsed line by line. Therefore parseTextData returns the content of the specific text or a newline \ n, parseText returns a text object with a type 2 {type: 2, the content, the text content | \ n, loc:} in the end of the initial information. At the end of the while loop, we determine that Node is not an array and push it into the Nodes array.
ParseInterpolation parses dynamic parameters
Then continue the while loop and call the node = parseInterpolation(Context, mode) method when parsing {{message}}
function parseInterpolation(context: ParserContext, mode: TextModes) :InterpolationNode | undefined {
// open is {{close is}}
const [open, close] = context.options.delimiters
// ...
const closeIndex = context.source.indexOf(close, open.length)
if (closeIndex === -1) {
// ...
}
const start = getCursor(context)
advanceBy(context, open.length)
const innerStart = getCursor(context)
const innerEnd = getCursor(context)
const rawContentLength = closeIndex - open.length
const rawContent = context.source.slice(0, rawContentLength)
const preTrimContent = parseTextData(context, rawContentLength, mode)
const content = preTrimContent.trim() // Remove Spaces at the beginning and end of the string
const startOffset = preTrimContent.indexOf(content)
if (startOffset > 0) {
advancePositionWithMutation(innerStart, rawContent, startOffset)
}
const endOffset =
rawContentLength - (preTrimContent.length - content.length - startOffset)
advancePositionWithMutation(innerEnd, rawContent, endOffset)
advanceBy(context, close.length)
return {
type: NodeTypes.INTERPOLATION, // type = 5 Dynamic data type
content: {
type: NodeTypes.SIMPLE_EXPRESSION, // 4
isStatic: false.constType: ConstantTypes.NOT_CONSTANT, / / 0
content,
loc: getSelection(context, innerStart, innerEnd)
},
loc: getSelection(context, start)
}
}
Copy the code
The internal implementation process of parseInterpolation method is as follows:
1, context.source.indexof (close, open.length) find the position of “}}” (curly end) Closeindex-open.length) template clip. rawContentLength = closeIndex-open.length 4, rawContent = closeIndex-open.length Context.source. slice(0, rawContentLength) The content of the dynamic parameter is retrieved from the slice method, RawContentLength (context.source with “{{” removed) 5. Use the parseTextData method to fetch the dynamic parameter name, in this case message 6. Then check if there is any data space around the content. {type: 5, content: {type: 4, isStatic: false, content,… }}
ParseElement parses elements
Continue with the while loop, which in this case is followed by another line break, and continue with the parseText method (the same as the first line break, which I won’t go into here). After parsing the button element, the branch s[0] === ‘<‘ is executed
if (s.length === 1) {/ /... }
else if (s[1= = ='! ') {// Parse the HTML comment code <! -- -- -- >}
else if (s[1= = ='/') {}
else if (/[a-z]/i.test(s[1]) {// Parse the beginning of the element this example executes the branch
node = parseElement(context, ancestors)
}
// parseElement method definition
function parseElement(context: ParserContext, ancestors: ElementNode[]) :ElementNode | undefined {
// ...
const wasInPre = context.inPre
const wasInVPre = context.inVPre
const parent = last(ancestors)
const element = parseTag(context, TagType.Start, parent)
constisPreBoundary = context.inPre && ! wasInPreconstisVPreBoundary = context.inVPre && ! wasInVPre// ...
}
Copy the code
ParseTag Parses the Tag
When parsing an element, the parseElement method is called. Internally, the parseTag method is called to parse the tag tag
function parseTag(
context: ParserContext,
type: TagType,
parent: ElementNode | undefined
) :ElementNode | undefined {
// ...
const start = getCursor(context)
const match = / ^ < \ /? ([a-z][^\t\r\n\f />]*)/i.exec(context.source)!
const tag = match[1]
const ns = context.options.getNamespace(tag, parent)
advanceBy(context, match[0].length)
advanceSpaces(context)
const cursor = getCursor(context)
const currentSource = context.source
// ...
let props = parseAttributes(context, type)
// ...
}
Copy the code
The parseTag method parses the HTML tag
/^<\/? ([a-z] [^ t \ r \ n \ \ f / >] *)/I parse label name (recommend regular graphic analytic address: jex. Im/regulex / #!) 2, call context. The options. The getNamespace method, the effect of this method is roughly judge the parent the parent node exists, does not exist when the tag tag for SVG or math. The tempalte template then parses the parsed “
function parseAttributes(context: ParserContext, type: TagType) : (AttributeNode | DirectiveNode) []{
const props = []
const attributeNames = new Set<string>()
while (
context.source.length > 0 &&
!startsWith(context.source, '>') &&
!startsWith(context.source, '/ >')) {// ...
const attr = parseAttribute(context, attributeNames)
if (type === TagType.Start) {
props.push(attr)
}
// ...
advanceSpaces(context)
}
return props
}
Copy the code
The parseAttributes loop parses the attributes in an element
The parseAttributes method internally parses attributes through a while loop that determines if the template string starts with “>” or “/>” (in other words, if the tag is resolved to an end). The while loop internally parses the individual attributes of the element through the parseAttribute method
ParseAttribute Parses the name and value of a single attribute
function parseAttribute(
context: ParserContext,
nameSet: Set<string>
) :AttributeNode | DirectiveNode {
// ...
const start = getCursor(context)
const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)!
const name = match[0]
// ...
advanceBy(context, name.length)
if (/^[\t\r\n\f ]*=/.test(context.source)) {
advanceSpaces(context)
advanceBy(context, 1)
advanceSpaces(context)
value = parseAttributeValue(context)
// ...
}
const loc = getSelection(context, start)
if(! context.inVPre &&/^(v-|:|\.|@|#)/.test(name)) {
const match = / (? :^v-([a-z0-9-]+))?? : (? : : | ^ \. | ^ @ | ^ #) (\ [[^ \]] + \] | [. ^ \] +))? (. +)? $/i.exec(name)!
let isPropShorthand = startsWith(name, '. ')
let dirName = match[1] || (isPropShorthand || startsWith(name, ':')?'bind' : startsWith(name, The '@')?'on' : 'slot')
let arg: ExpressionNode | undefined
if (match[2]) {
const isSlot = dirName === 'slot'
const startOffset = name.lastIndexOf(match[2])
const loc = getSelection(
context,
getNewPosition(context, start, startOffset),
getNewPosition(
context,
start,
startOffset + match[2].length + ((isSlot && match[3) | |' ').length)
)
let content = match[2]
let isStatic = true
if (content.startsWith('[')) { / *... * / }
else if (isSlot) { // this example is a slot instruction, omit code}
arg = {
type: NodeTypes.SIMPLE_EXPRESSION, // 4
content,
isStatic,
constType: isStatic
? ConstantTypes.CAN_STRINGIFY / / 3
: ConstantTypes.NOT_CONSTANT, / / 0
loc
}
}
if (value && value.isQuoted) {
const valueLoc = value.loc
valueLoc.start.offset++
valueLoc.start.column++
valueLoc.end = advancePositionWithClone(valueLoc.start, value.content)
valueLoc.source = valueLoc.source.slice(1, -1)}const modifiers = match[3]? match[3].substr(1).split('. ') : []
if (isPropShorthand) modifiers.push('prop')
// ...
return {
type: NodeTypes.DIRECTIVE, / / 7
name: dirName,
exp: value && {
type: NodeTypes.SIMPLE_EXPRESSION,
content: value.content,
isStatic: false.// Treat as non-constant by default. This can be potentially set to
// other values by `transformExpression` to make it eligible for hoisting.
constType: ConstantTypes.NOT_CONSTANT,
loc: value.loc
},
arg,
modifiers,
loc
}
}
}
Copy the code
The parseAttribute method parses the attribute flow
1, through the regular expression / ^ ^ \ r \ n \ \ t f / >] \ r \ n \ [^ \ t f / > =] * / resolve the key = value structure, in this case for @ click = “modifyMessage”, Use the regular expression /^[\t\r\n\f]*=/ to determine if the remaining template string is a “=value” structure, in this case =”modifyMessage”, and call parseAttributeValue to parse the attribute value. We look at this method follow-up internal concrete is how to parse the attribute values of 3, through the regular expression / ^ (v – | : | \. | @ # |)/judge the attribute name is v – (instruction), (data), @ one of the (event), in this case for @ the click, and then through the regular expression/(? :^v-([a-z0-9-]+))?? : (? : : | ^ \. | ^ @ | ^ #) (\ [[^ \]] + \] | [. ^ \] +))? (. +)? $/ I resolves the name of a specific instruction, data, or event. Select * from startsWith(name, ‘@’), content=match[2] for click 4, and then generate arG object for attribute name {type:4, content: Property name, isStatic: true,… }
ParseAttributeValue Resolves the attribute value
So let’s go back to the parseAttributeValue method and see how it resolves the value of an attribute. Okay
function parseAttributeValue(context: ParserContext) :AttributeValue {
const start = getCursor(context)
let content: string
const quote = context.source[0]
const isQuoted = quote === ` "` || quote === ` '`
if (isQuoted) {
advanceBy(context, 1)
const endIndex = context.source.indexOf(quote)
if (endIndex === -1) {}
else {
content = parseTextData(context, endIndex, TextModes.ATTRIBUTE_VALUE)
advanceBy(context, 1)}}else { / *... * / }
return { content, isQuoted, loc: getSelection(context, start) }
}
Copy the code
1, parseAttributeValue method first determines if the first character is “| ‘(double quotes or single quotation marks), in this case, resolution of the template string should be” modifyMessage “, and then remove the first “|”, 2. Call parseTextData to parse the name of the property value. This method has already been parsed. {content, isletter, loc: position}
Type: 7, name: ‘on’, exp: information about the value of the attribute, arg: information about the name of the attribute, etc. }. Go back to the Attributes function, push the parsed attribute object attr into the props array, and continue calling the while loop to parse the next attribute until the element terminator. Finally returns the props property array. (In this case, there is only one property click). Go back to the parseTag method and see what happens when you parse the attributes
// The parseTag method follows
let props = parseAttributes(context, type)
// Check if there is a V-pre command...
let isSelfClosing = false
if (context.source.length === 0) {
emitError(context, ErrorCodes.EOF_IN_TAG)
} else {
isSelfClosing = startsWith(context.source, '/ >')
if (type === TagType.End && isSelfClosing) {
emitError(context, ErrorCodes.END_TAG_WITH_TRAILING_SOLIDUS)
}
advanceBy(context, isSelfClosing ? 2 : 1)}// ...
// Check whether the tag is slot or template
return {
type: NodeTypes.ELEMENT, // type:1 Parsed element object
ns,
tag,
tagType,
props,
isSelfClosing,
children: [].loc: getSelection(context, start),
codegenNode: undefined // to be created during transform phase
}
Copy the code
{type: 1, tag: button, props:[{…}], children: functions () {type: 1, tag: button, props:[{…}], props: [], loC: position information, codegenNode: undefined, isSelfClosing: false} Then go back to the parseElement method and see what happens after parsing the first half of the tag
// Subsequent implementation of the parseElement method
if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
// Check whether it is a self-closing label or an empty label (similar to br or HR labels)
}
// Start parsing the child elements inside the tag
ancestors.push(element)
const mode = context.options.getTextMode(element, parent)
const children = parseChildren(context, mode, ancestors)
ancestors.pop()
// ...
Copy the code
The parseElement method then determines whether it is a self-closing tag or an empty tag, or it starts parsing the child elements inside the tag. Obtained first invocation context. The options. GetTextMode mode value, in this case the tag for the button, returns 0 (subsequent analysis to other tags to do the detail). The parseChildren method is then called to resolve the child tags, in this case reusing the parseChildren method to resolve the child tags inside the tags. In this case, the word element inside the button tag is text, and the parseText method is finally called to get the text parsed {type: 2, Content, LOc: location}. Then push it into the Nodes array. Then return to the parseElement method
After the parseChildren function’s while loop has been parsed, a bit of code is executed to filter the generated Node object (since the neutron nodes are simpler in this example, this parsing is delayed until the entire string has been parsed).
// parseElement parses the child tag
element.children = children
if (startsWithEndTagOpen(context.source, element.tag)) {
parseTag(context, TagType.End, parent)
} else { / *... * / }
element.loc = getSelection(context, element.loc.start)
return element
Copy the code
When the child element inside the tag is parsed, it is assigned to the Children property of the Element, then the closing tag is processed, the position of the entire tag element is computed, and the Element object is returned. At this point the while loop of the parseChildren method has ended because all the labels have been parsed. See what objects are in the Nodes array:
2, type=5, content={type:4, content: {type:1, props: [{…}], children: […] . } 5, type=2, content=\n(newline) text tag \
After the template parses the while loop, the code in the parseChildren method continues. How does the template filter and modify the element objects in the Nodes array
// the following code in parseChildren
let removedWhitespace = false
if(mode ! == TextModes.RAWTEXT && mode ! == TextModes.RCDATA) {constshouldCondense = context.options.whitespace ! = ='preserve'
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if(! context.inPre && node.type === NodeTypes.TEXT/ * 2 * /) {
if (!/[^\t\r\n\f ]/.test(node.content)) {
const prev = nodes[i - 1]
const next = nodes[i + 1]
if(! prev || ! next || (shouldCondense && (prev.type === NodeTypes.COMMENT || next.type === NodeTypes.COMMENT || (prev.type === NodeTypes.ELEMENT && next.type === NodeTypes.ELEMENT &&/[\r\n]/.test(node.content))))
) {
removedWhitespace = true
nodes[i] = null as any
} else {
node.content = ' '}}else if (shouldCondense) {/ *... * /}}else if(node.type === NodeTypes.COMMENT && ! context.options.comments) {} }if (context.inPre && parent && context.options.isPreTag(parent.tag)) {}
}
return removedWhitespace ? nodes.filter(Boolean) : nodes
Copy the code
1, the first is the text node type 2, through the regular expression to determine! /[^\t\r\n\f]/.test(Node.content) is one of these special symbols. The nodes[I] = null as any if the previous object does not exist. This object is directly assigned to NULL, so the first and last elements in the Nodes array become NULL. Check whether either of the two adjacent objects has a type of NodeTypes.COM (3) or both have a type of nodetypes.elemen (1) and the content property contains \r\n. In this case, one type is 5 and the other type is 1. Nodes.filter (Boolean) returns the filtered nodes object, nodes.filter(Boolean), then null elements are filtered
At this point, the parseChildren function is parsed. Using this example as a template, three objects are parsed: the dynamic data parsing result of type 5, the newline character of type 2 (content has been replaced with “”), and the HTML tag element of type 1. Step back into the baseParse method and execute the createRoot method with the parsed template object array and location information
export function createRoot(children: TemplateChildNode[], loc = locStub) :RootNode {
return {
type: NodeTypes.ROOT,
children,
helpers: [].components: [].directives: [].hoists: [].imports: [].cached: 0.temps: 0.codegenNode: undefined,
loc
}
}
Copy the code
As a result, the createRoot function returns an array of Nodes returned by the parseChildren method with type 0 and children as the root node object. The baseParse method returns the root node object. Then return to baseCompile method. After the AST object is generated, transform method will be called to transform the AST object, and then generate method will be called to generate render function.
conclusion
Inside the function of finishComponentSetup that is parsed at the end of the previous chapter, compile method is called for template compilation. In fact, baseCompile method is called for template parsing, transformation, and finally generate render function. This article starts with parsing, where the root node is generated by the createRoot method, whose arguments call the parseChildren method to actually parse the template string.
The parseChildren method mainly parses and trims through a while loop until the template string is parsed. 3. Child elements of HTML tags will continue to be parseChildren methods for deep parsing. 4. The attributes in the HTML tag are also parsed through the while loop (regular expression judgment) for all the attributes (instructions, data, and events), and finally generated property objects, which are then pushed into the props property array.