preface
To get started, take a look at an example lifecycle diagram from the Vue website.
As you can see from the figure, when we instantiate and initialize a Vue object, Vue checks the EL and template properties for the template string. The resulting template is then compiled into the render function.
Vue uses the outerHTML of the specified EL element as a template only if template is not specified.
But if we also specify a custom render function, vue will no longer use the first two to get the template.
This article focuses on how Vue finally compiles a template obtained from template or EL into the Render function.
Parse the HTML template into an AST node tree
As mentioned above, after obtaining the template string, VUE parses the HTML template character by character through regular expression to parse the element nodes and the instructions, attributes and event bindings set on each element node, and finally builds an AST node tree that completely describes the INFORMATION of HTML nodes.
Take a look at a few core regular expressions and AST objects.
HTML characters match core regular expressions
// 1. Match the start tag (excluding the trailing >), such as
const startTagOpen = ^ < / ((? :[a-zA-Z_][\-\.0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u207 0-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*\:)? [a-zA-Z_][\-\.0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070 -\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*)/
// 2. Match normal HTML attributes, such as id="app"
const attribute = /^\s*([^\s"'<>\/=]+)(? :\s*(=)\s*(? :"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))? /
V-bind: SRC ="imageSrc" :[key]="value" // 3.
const dynamicArgAttribute = /^\s*((? :v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(? :\s*(=)\s*(? :"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))? /
4.Matches the end of the start tag, such as > or />const startTagClose = /^\s*(\/?) >/
// 5. Match closed tags, such as
const endTag = < \ \ / / ^((? :[a-zA-Z_][\\-\\. 0-9_a-zA-Z((? :[a-zA-Z_][\-\. 0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\ u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*\:)? [a-zA-Z_][\-\. 0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\ u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*)]*\\:)? [a-zA-Z_][\\-\\. 0-9_a-zA-Z((? :[a-zA-Z_][\-\. 0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\ u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*\:)? [a-zA-Z_][\-\. 0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\ u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*)]*)[^>]*>/// 6. Match the DOCTYPE
const doctype = / ^
]+>/i
// 7. Match HTML comments
const comment = / ^
// 8. Match HTML conditional comments
const conditionalComment = / ^
Copy the code
AST node object
The following code is the creation function for the AST node. The code gives you a basic understanding of AST nodes. In a nutshell,
AST nodes are descriptions of HTML node information. For example, the AST saves the content parsed by HTML tag attributes to attrsList, attrsMap, and rawAttrsMap, keeps references to the parent node in the parent node, and points to its children through children.
{
// Node type
type: 1.// Tag name, such as div
tag: "div".// Attributes that the node contains
attrsList: [].attrsMap: {},
rawAttrsMap: {},
// Parent node pointer
parent: undefined.// Child node pointer
children: []}Copy the code
Simple tag parsing
Assume the following template:
<div id="app">{{msg}}</div>
Copy the code
The process of parsing is to capture the tag name and attribute name and value through several core regular expressions, and then use this information to create an AST node object. The main code is as follows:
// 1. Parse tags and attributes
function parseStartTag () {
// Match the start tag
var start = html.match(startTagOpen);
if (start) {
var match = {
tagName: start[1].attrs: [].start: index
};
// Move the pointer forward
advance(start[0].length);
var end, attr;
// Matches the attributes in the tag
while(! (end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) { attr.start = index; advance(attr[0].length);
attr.end = index;
match.attrs.push(attr);
}
if (end) {
match.unarySlash = end[1];
advance(end[0].length);
match.end = index;
returnmatch } } } ... Omit other code// 2. Create an AST node object
varelement = createASTElement(tag, attrs, currentParent); . Omit other codeCopy the code
After the above analysis, the following AST node objects are obtained:
{
attrsList: [{name: "id".value: "app".start: 5.end: 13}].attrsMap: {id: "app"},
children: [].end: 14.parent: undefined.rawAttrsMap: {id: {name: "id".value: "app".start: 5.end: 13}},
start: 0.tag: "div".type: 1
}
Copy the code
The next thing you need to deal with is the div child node {{MSG}}. Since the children are text nodes, we use parseText to process the text nodes, and then use this information to create the AST node object. The main code is as follows:
// 1. Parse the characters in the text node
function parseText (text: string, delimiters? : [string, string]) :TextParseResult | void {
/ /... Omit other code
const tokens = []
const rawTokens = []
// tagRE defaults to /\{\{((? :.|\r? \n)+?) \}\}/g, which identifies the value inserted in the text by {{value}}
let lastIndex = tagRE.lastIndex = 0
let match, index, tokenValue
while ((match = tagRE.exec(text))) {
index = match.index
if (index > lastIndex) {
rawTokens.push(tokenValue = text.slice(lastIndex, index))
tokens.push(JSON.stringify(tokenValue))
}
// If a filter is used in template interpolation, the filter needs to be parsed first
const exp = parseFilters(match[1].trim())
tokens.push(`_s(${exp}) `)
rawTokens.push({ '@binding': exp })
lastIndex = index + match[0].length
}
if (lastIndex < text.length) {
rawTokens.push(tokenValue = text.slice(lastIndex))
tokens.push(JSON.stringify(tokenValue))
}
// Expression and tokens
return {
expression: tokens.join('+'),
tokens: rawTokens
}
}
/ /... Omit other code
// 2. Create a text AST object
child = {
type: 2.expression: res.expression,
tokens: res.tokens,
text: text
}
child.start = start;
child.end = end;
/ /... Omit other code
// Add to div's child node array
children.push(child);
Copy the code
The result of text node parsing is as follows:
{
type: 2.expression: "_s(message)".tokens: [{@binding: "message"}].text: "{{message}}"
}
Copy the code
The AST of the root div is updated to:
{
attrsList: [{name: "id".value: "app".start: 5.end: 13}]
attrsMap: {id: "app"}
children: [{type: 2.expression: "_s(msg)".tokens: [{@binding: "msg"}].text: "{{msg}}"}]
end: 14
parent: undefined
rawAttrsMap: {id: {name: "id".value: "app".start: 5.end: 13}}
start: 0
tag: "div"
type: 1
}
Copy the code
Finally, parse the closing tag </div>. When the parser matches a closed tag, it means that a tag has been matched.
In addition to HTML attributes such as ID =”app”, placeholder=” Edit me”, there are also a lot of VUE directive attributes, such as V-for, V-ON, V-model. The parser takes the AST node object, which was already generated, further. Eventually this information is added to the AST object in the form of directives, ON, domProps, and other properties.
// Final processing of nodes
function closeElement (element) {
// Remove the empty child node of the element
trimEndingWhitespace(element)
// Handle the AST and add additional attributes
if(! inVPre && ! element.processed) { element = processElement(element, options) }/ /... Omit other code
}
// Add additional attributes to the AST
function processElement (element: ASTElement, options: CompilerOptions) {
// Add a key attribute to the AST
processKey(element)
// Add the plain attribute to the ASTelement.plain = ( ! element.key && ! element.scopedSlots && ! element.attrsList.length )// Handle v-ref, add ref attribute to AST object
processRef(element)
// Process the slot passed to the component, adding the slotScope property to the AST
processSlotContent(element)
// Process slot tags and add slotName to the AST
processSlotOutlet(element)
// Add the Component or inlineTemplate attribute to the AST
processComponent(element)
/ / processing
for (let i = 0; i < transforms.length; i++) {
element = transforms[i](element, options) || element
}
// Add the AST object properties, such as directives, events, and props, depending on the properties
processAttrs(element)
return element
}
Copy the code
Knowing the simple tag parsing process, let’s look at the parsing of v-for, V-IF, V-Model and V-ON several common instructions.
Processing of V-for instructions
Let’s look at it with a simple HTML template. Given the following HTML template:
<div id="app"><p v-for="(item, index) in items">{{item}}</p></div>
Copy the code
Before parsing the </p> tag, you get an initial AST object that looks something like this:
{
After initial parsing, the attribute is temporarily stored in attrsList
attrsList: [{name: "v-for".value: "(item, index) in items".start: 17.end: 47},
{name: ":key".value: "index".start: 48.end: 60}].attrsMap: {v-for: "(item, index) in items", :key: "index"},
children: [].parent: {type: 1.tag: "div".attrsList: Array(1), attrsMap: {... },rawAttrsMap: {... },... },rawAttrsMap: {},
tag: "p".type: 1
}
Copy the code
ProcessFor is then called to parse the V-for directive and add the results to the AST object:
function processFor (el: ASTElement) {
// Parse the v-for instruction on the node
let exp
// Get the v-for expression from attrsList and remove the V-for from attrsList
if ((exp = getAndRemoveAttr(el, 'v-for'))) {
{alias: "item",for:"items",iterator1: "index"}
const res = parseFor(exp)
// Merge into the AST object
if (res) {
extend(el, res)
} else if(process.env.NODE_ENV ! = ='production') {
warn(
`Invalid v-for expression: ${exp}`,
el.rawAttrsMap['v-for'])}}}Copy the code
The AST object becomes the following:
{
// V-for is incorporated into the AST object as an attribute
alias: "item".for: "items".iterator1: "index".// At this point, v-for has been removed from attrsList
attrsList: [{name: ":key".value: "index".start: 48.end: 60}].attrsMap: {v-for: "(item, index) in items", :key: "index"},
children: [].parent: {type: 1.tag: "div".attrsList: Array(1), attrsMap: {... },rawAttrsMap: {... },... },rawAttrsMap: {},
tag: "p".type: 1
}
Copy the code
For the sake of simplicity, the AST node objects we have shown in the example do not contain complete information, but only important information that will be helpful.
Processing of V-if instructions
We will adjust the HTML template in the above example as follows:
<div id="app"><p v-if="seen">you can see me</p></div>
Copy the code
After parsing, the AST object of the P tag is roughly as follows:
{
// focus on attrsList
attrsList: [{name: "v-if".value: "seen".start: 17.end: 28}].attrsMap: {v-if: "seen"},
children: [].parent: {type: 1.tag: "div".attrsList: Array(1), attrsMap: {... },rawAttrsMap: {... },... },rawAttrsMap: {},
tag: "p".type: 1
}
Copy the code
Next, vue calls processIf to further process v-if:
function processIf (el) {
// Get the v-if expression and remove the v-if from attrsList
const exp = getAndRemoveAttr(el, 'v-if')
if (exp) {
// Add if and ifConditions attributes to AST
el.if = exp
addIfCondition(el, {
exp: exp,
block: el
})
} else {
// Add else or elseIf to AST
if (getAndRemoveAttr(el, 'v-else') != null) {
el.else = true
}
const elseif = getAndRemoveAttr(el, 'v-else-if')
if (elseif) {
el.elseif = elseif
}
}
}
Copy the code
The AST object becomes the following:
{
if: "seen".// ifConditions array, where each item represents a condition statement, exp represents the expression of the condition statement, and block is the AST object of the label to which the condition statement applies.
ifConditions: [{exp: "seen".block: {... }}].// focus on attrsList
attrsList: [{name: "v-if".value: "seen".start: 17.end: 28}].attrsMap: {v-if: "seen"},
children: [].parent: {type: 1.tag: "div".attrsList: Array(1), attrsMap: {... },rawAttrsMap: {... },... },rawAttrsMap: {},
tag: "p".type: 1
}
Copy the code
If v-if is followed by a p tag that uses the V-else directive, we also need to execute processIfConditions to push the parsing result into the ifConditions array of the previous node with the if attribute:
function processIfConditions (el, parent) {
// Get the last node of the V-else
const prev = findPrevElement(parent.children)
// If the previous node exists and has an if attribute, push the v-else parsing result to the ifConditions attribute array of that node
if (prev && prev.if) {
addIfCondition(prev, {
exp: el.elseif,
block: el
})
} else if(process.env.NODE_ENV ! = ='production') {
warn(
`v-${el.elseif ? ('else-if="' + el.elseif + '"') : 'else'} ` +
`used on element <${el.tag}> without corresponding v-if.`,
el.rawAttrsMap[el.elseif ? 'v-else-if' : 'v-else'])}}Copy the code
The AST node where v-if resides becomes the following:
{
if: "seen".// The v-else node after v-if is added to the ifConditions array.
ifConditions: [{exp: "seen".block: {... }}, {exp: undefined.block: {... }}].// focus on attrsList
attrsList: [{name: "v-if".value: "seen".start: 17.end: 28}].attrsMap: {v-if: "seen"},
children: [].parent: {type: 1.tag: "div".attrsList: Array(1), attrsMap: {... },rawAttrsMap: {... },... },rawAttrsMap: {},
tag: "p".type: 1
}
Copy the code
Processing of V-ON instructions
Adjust the HTML template in the above example as follows:
<div id="app"><p @click="show">click me</p></div>
Copy the code
After parsing, the AST object of the P tag is roughly as follows:
{
attrsList: [{name: "@click".value: "show".start: 17.end: 30].attrsMap: {@click: "show"},
children: [].parent: {type: 1.tag: "div".attrsList: Array(1), attrsMap: {... },rawAttrsMap: {... },... },rawAttrsMap: {},
tag: "p".type: 1
}
Copy the code
In the next step, vUE will add the event and time handlers bound by V-ON to the Events property of the AST object:
/ /... Omit other code
if (onRE.test(name)) {
// If it is a V-ON instruction, give it to an event handler
name = name.replace(onRE, ' ')
isDynamic = dynamicArgRE.test(name)
if (isDynamic) {
name = name.slice(1, -1)}// Add an on attribute event handler to the AST object
addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic)
}
// Add an event handler
function addHandler (el: ASTElement, name: string, value: string, modifiers: ? ASTModifiers, important? : boolean, warn? :?Function, range? : Range, dynamic? : boolean) {
/ /... Omit other code
let events = el.events || (el.events = {})
const newHandler: any = rangeSetItem({ value: value.trim(), dynamic }, range)
const handlers = events[name]
// Add the event handler to events
/* istanbul ignore if */
if (Array.isArray(handlers)) {
important ? handlers.unshift(newHandler) : handlers.push(newHandler)
} else if (handlers) {
events[name] = important ? [newHandler, handlers] : [handlers, newHandler]
} else {
events[name] = newHandler
}
el.plain = false
}
Copy the code
The AST object becomes the following:
{
attrsList: [{name: "@click".value: "show".start: 17.end: 30}].attrsMap: {@click: "show"},
children: [].parent: {type: 1.tag: "div".attrsList: Array(1), attrsMap: {... },rawAttrsMap: {... },... },rawAttrsMap: {},
tag: "p".type: 1.// add a record to events
events: {click: {value: "show".dynamic: false.start: 17.end: 30}},
hasBindings: true
}
Copy the code
Processing of V-model instructions
Adjust the HTML template in the above example as follows:
<div id="app"><input v-model="message" placeholder="edit me"></div>
Copy the code
After parsing, the AST object for the input tag looks like this:
{
attrsList: [{name: "v-model".value: "message".start: 21.end: 38},
{name: "placeholder".value: "edit me".start: 39.end: 60}].attrsMap: {v-model: "message".placeholder: "edit me"},
children: [].parent: {type: 1.tag: "div".attrsList: Array(1), attrsMap: {... },rawAttrsMap: {... },... },rawAttrsMap: {},
tag: "input".type: 1
}
Copy the code
In the next processing, the compiler calls addDirective to add the DIRECTIVES property to the AST node and to add the resolution result of the V-Model to the directives.
// Add a directive object to the CACHE property array of the AST node, el.directives = [{name,rawName,...}]
function addDirective (el: ASTElement, name: string, rawName: string, value: string, arg: ? string, isDynamicArg: boolean, modifiers: ? ASTModifiers, range? : Range) {
(el.directives || (el.directives = [])).push(rangeSetItem({
name,
rawName,
value,
arg,
isDynamicArg,
modifiers
}, range))
el.plain = false
}
Copy the code
The AST node becomes the following:
{
attrsList: [{name: "placeholder".value: "edit me".start: 39.end: 60}].attrsMap: {v-model: "message".placeholder: "edit me"},
children: [].// Add the cache property and add a record to this array
directives: [{name: "model".rawName: "v-model".value: "message".arg: null.isDynamicArg: false.modifiers: undefined.start:21.end:38}].parent: {type: 1.tag: "div".attrsList: Array(1), attrsMap: {... },rawAttrsMap: {... },... },rawAttrsMap: {},
tag: "input".type: 1
}
Copy the code
Generate the render function for the AST
The HTML template is parsed into a tree of AST nodes. The compiler then executes generate to generate the body of the render function for the AST.
function generate (ast, options) {
var state = new CodegenState(options);
// Generate code based on the AST node
var code = ast ? genElement(ast, state) : '_c("div")';
return {
render: ("with(this){return " + code + "}"),
staticRenderFns: state.staticRenderFns
}
}
Copy the code
The main function of the Render function is to create a virtual node vnode. To create a VNode, you need three parameters: the element label, the data object, and the list of child elements. GenElement, as the core code generation method, generates all three parts of the code in sequence.
/** * Code generation functions for AST elements *@param {ASTElement} El AST object *@param {CodegenState} State Code generation status */
function genElement (el: ASTElement, state: CodegenState) :string {
/ /... Omit other code
let data
Attrs :{"id":"app"}}"
if(! el.plain || (el.pre && state.maybeComponent(el))) { data = genData(el, state) }// 2. Next, generate the child element creation code
const children = el.inlineTemplate ? null : genChildren(el, state, true)
_c(tag,data,children);
code = `_c('${el.tag}'${
data ? `,${data}` : ' ' // data
}${
children ? `,${children}` : ' ' // children
}) `
/ /... Omit other code
Copy the code
Data Data object code
Using the first HTML template as an example:
<div id="app">{{msg}}</div>
Copy the code
The parsed AST object is as follows:
{
attrsList: [{name: "id".value: "app".start: 5.end: 13}].attrsMap: {id: "app"},
children: [{type: 2.expression: "_s(msg)".tokens: [{@binding: "msg"}].text: "{{msg}}"}].end: 14.parent: undefined.plain: false.rawAttrsMap: {id: {name: "id".value: "app".start: 5.end: 13}},
start: 0.tag: "div".type: 1.static: false.staticRoot: false
}
Copy the code
The following code is the generation logic of the data object code:
function genData (el: ASTElement, state: CodegenState) :string {
let data = '{'
// Start by generating code for directives that may modify other attributes of the element
const dirs = genDirectives(el, state)
if (dirs) data += dirs + ', '
// key
if (el.key) {
data += `key:${el.key}, `
}
// if ref='myref' and ref='myref',
if (el.ref) {
data += `ref:${el.ref}, `
}
// refInFor
if (el.refInFor) {
data += `refInFor:true,`
}
// pre
if (el.pre) {
data += `pre:true,`
}
// record original tag name for components using "is" attribute
if (el.component) {
data += `tag:"${el.tag}", `
}
/ / is mainly the class and style code generated staticClass, staticStyle, classBinding, styleBinding
for (let i = 0; i < state.dataGenFns.length; i++) {
data += state.dataGenFns[i](el)
}
// Attribute generation of the attributes element, such as given el.attrs=[{name: 'id', value:'app', dynamic: Undefined, start: 0, end: 5}], it returns' attrs: {" id ":" app "} '
if (el.attrs) {
data += `attrs:${genProps(el.attrs)}, `
}
// DOM props
if (el.props) {
data += `domProps:${genProps(el.props)}, `
}
// event handlers
if (el.events) {
data += `${genHandlers(el.events, false)}, `
}
if (el.nativeEvents) {
data += `${genHandlers(el.nativeEvents, true)}, `
}
// slot target
// only for non-scoped slots
if(el.slotTarget && ! el.slotScope) { data +=`slot:${el.slotTarget}, `
}
// scoped slots
if (el.scopedSlots) {
data += `${genScopedSlots(el, el.scopedSlots, state)}, `
}
// component v-model
if (el.model) {
data += `model:{value:${ el.model.value },callback:${ el.model.callback },expression:${ el.model.expression }}, `
}
// inline-template
if (el.inlineTemplate) {
const inlineTemplate = genInlineTemplate(el, state)
if (inlineTemplate) {
data += `${inlineTemplate}, `}}// Remove the trailing comma and add curly braces
data = data.replace($/ /,.' ') + '} '
if (el.dynamicAttrs) {
data = `_b(${data},"${el.tag}",${genProps(el.dynamicAttrs)}) `
}
// v-bind data wrap
if (el.wrapData) {
data = el.wrapData(data)
}
// v-on data wrap
if (el.wrapListeners) {
data = el.wrapListeners(data)
}
return data
}
Copy the code
As you can see from the above code, Vue generates each of the props, Attrs, Events, and directives. In this example, the AST node object only has the attrs property, so its processing will only execute the following statement:
if (el.attrs) {
data += `attrs:${genProps(el.attrs)}, `
}
Copy the code
The genProps function is as follows:
function genProps (props: Array<ASTAttr>) :string {
let staticProps = ` `
let dynamicProps = ` `
for (let i = 0; i < props.length; i++) {
const prop = props[i]
const value = __WEEX__
? generateValue(prop.value)
: transformSpecialNewlines(prop.value)
if (prop.dynamic) {
dynamicProps += `${prop.name}.${value}, `
} else {
staticProps += `"${prop.name}":${value}, `
}
}
staticProps = ` {${staticProps.slice(0, -1)}} `
if (dynamicProps) {
return `_d(${staticProps}[${dynamicProps.slice(0, -1)}]) `
} else {
return staticProps
}
}
Copy the code
[{name: “id”, value: “app”, start: 5, end: 13}]] as a string like this:
"{attrs:{"id":"app"}}"
Copy the code
Generate children code
The child element creation code generation function is as follows:
function genChildren (el: ASTElement, state: CodegenState, checkSkip? : boolean, altGenElement? :Function, altGenNode? :Function
) :string | void {
const children = el.children
// No child element is processed
if (children.length) {
const el: any = children[0]
// If the child element uses the v-for directive
if (children.length === 1&& el.for && el.tag ! = ='template'&& el.tag ! = ='slot'
) {
const normalizationType = checkSkip
? state.maybeComponent(el) ? ` `, 1 : ` `, 0
: ` `
return `${(altGenElement || genElement)(el, state)}${normalizationType}`
}
const normalizationType = checkSkip
? getNormalizationType(children, state.maybeComponent)
: 0
// Iterate through the Children array, generating code for each child element
const gen = altGenNode || genNode
return ` [${children.map(c => gen(c, state)).join(', ')}]${
normalizationType ? `,${normalizationType}` : ' '
}`}}/** * Generates code to create node nodes, encapsulating various node types *@param {*} node
* @param {*} state
*/
function genNode (node: ASTNode, state: CodegenState) :string {
// Element node
if (node.type === 1) {
return genElement(node, state)
/ / comment
} else if (node.type === 3 && node.isComment) {
return genComment(node)
// Text node
} else {
return genText(node)
}
}
/** * generate create text node string, return content such as: "_v("see me")" *@param {*} Text AST node */ of the text type
function genText (text: ASTText | ASTExpression) :string {
return `_v(${text.type === 2
? text.expression // no need for () because already wrapped in _s()
: transformSpecialNewlines(JSON.stringify(text.text))
}) `
}
Copy the code
For this example, the child element of the div is a text node, and genText generates the following code:
"[_v(_s(message))]"
Copy the code
The complete code for the Render function
Finally, in the genElement function, the pieces of code are spliced together to form a complete piece of code:
'with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(message))])}'
Copy the code
This is the basic render function creation steps, let’s take a look at the code generated for v-if, V-model, and V-for (v-ON is also involved in v-model processing, not a separate example).
V – if the processing
Let’s look at the v-if processing logic in the genElement function:
function genElement (el: ASTElement, state: CodegenState) :string {
/ /... Omit other code
// The genIf command exists on the node
if(el.if && ! el.ifProcessed) {return genIf(el, state)
}
}
Copy the code
If an HTML tag in the template uses v-if, the compiler calls genIf. The main code is as follows:
function genIf (el: any, state: CodegenState, altGen? :Function, altEmpty? : string) :string {
el.ifProcessed = true
return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}
function genIfConditions (conditions: ASTIfConditions, state: CodegenState, altGen? :Function, altEmpty? : string) :string {
if(! conditions.length) {return altEmpty || '_e()'
}
// Convert the conditional statement in the ifConditions array into a ternary expression
const condition = conditions.shift()
// Generate a ternary expression of the form a? 1:2
if (condition.exp) {
return ` (${condition.exp})?${ genTernaryExp(condition.block) }:${ genIfConditions(conditions, state, altGen, altEmpty) }`
} else {
return `${genTernaryExp(condition.block)}`
}
// v-if uses v-once (a)? _m(0):_m(1)
function genTernaryExp (el) {
return altGen
? altGen(el, state)
: el.once
? genOnce(el, state)
: genElement(el, state)
}
}
Copy the code
After genIf is executed, a ternary expression is created. The following template is used as an example:
div id="app"><p v-if="seen">you can see me</p><p v-else>you can not see me</p></div>
Copy the code
The corresponding code generated using the V-if and V-else p tags is as follows:
"(seen)? _c('p',[_v("you can see me")]):_c('p',[_v("you can not see me")])"
Copy the code
From the generated code, we can understand the difference between V-show and V-if. When using V-if to control the hiding and display of DOM elements, they need to be removed and recreated each time.
V – processing model
If we use v-Model in the following template:
<div id="app"><input v-model="message" placeholder="edit me"></div>
Copy the code
The AST node core information corresponding to the INPUT label is as follows:
{
attrs: [{name: "placeholder".value: ""edit me"".dynamic: undefined.start: 39.end: 60}]
attrsList: [{name: "v-model".value: "message".start: 21.end: 38}, {name: "placeholder".value: "edit me".start: 39.end: 60}].attrsMap: {v-model: "message".placeholder: "edit me"},
children: [].directives: [{name: "model".rawName: "v-model".value: "message".arg: null.isDynamicArg: false.modifiers: undefined.start:21.end:38}].events: {input: {value: "if($event.target.composing)return; message=$event.target.value".dynamic: undefined}},
props: [{name: "value".value: "(message)".dynamic: undefined}].parent: {type: 1.tag: "div".attrsList: Array(1), attrsMap: {... },rawAttrsMap: {... },... },rawAttrsMap: {placeholder: {name: "placeholder".value: "edit me".start: 39.end: 60},v-model: {name: "v-model".value: "message".start: 21.end: 38}},
hasBindings: true.tag: "input".type: 1
}
Copy the code
Input has no child elements, so you only need to generate data data objects. The generated code looks like this:
"{directives:[{name:"model",rawName:"v-model",value:(message),expression:"message"}],attrs:{"placeholder":"edit me"},domProps:{"value":(message)},on:{"input":function($event){if($event.target.composing)return; message=$event.target.value}}}"
Copy the code
In the data object, domProps and ON are added to specify the value and input event handlers. So you understand that v-model is essentially a syntactically sugar wrapped around V-bind and V-on :input.
After concatenation, the complete input generates the following code:
"_c('input',{directives:[{name:"model",rawName:"v-model",value:(message),expression:"message"}],attrs:{"placeholder":"edit me"},domProps:{"value":(message)},on:{"input":function($event){if($event.target.composing)return; message=$event.target.value}}})"
Copy the code
V – for processing
If v-for is used in the following template:
<div id="app"><p v-for="(item, index) in items" :key="index">{{item}}</p></div>
Copy the code
The AST core information corresponding to the P label is as follows:
{
alias: "item".for: "items".iterator1: "index".forProcessed: true.key: "index".attrsList: [].attrsMap: {v-for: "(item, index) in items", :key: "index"},
children: [{type: 2.expression: "_s(item)".tokens: Array(1), text: "{{item}}".start: 61,... }].parent: {type: 1.tag: "div".attrsList: Array(1), attrsMap: {... },rawAttrsMap: {... },... },rawAttrsMap: {:key: {name: ":key".value: "index".start: 48.end: 60},v-for: {name: "v-for".value: "(item, index) in items".start: 17.end: 47}},
tag: "p".type: 1
}
Copy the code
Call the genFor function:
function genFor (el: any, state: CodegenState, altGen? :Function, altHelper? : string) :string {
const exp = el.for
const alias = el.alias
const iterator1 = el.iterator1 ? `,${el.iterator1}` : ' '
const iterator2 = el.iterator2 ? `,${el.iterator2}` : ' '
// In non-production environments, if :key is not specified, the console prompts the user
if(process.env.NODE_ENV ! = ='production'&& state.maybeComponent(el) && el.tag ! = ='slot'&& el.tag ! = ='template' &&
!el.key
) {
state.warn(
` <${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
`v-for should have explicit keys. ` +
`See https://vuejs.org/guide/list.html#key for more info.`,
el.rawAttrsMap['v-for'].true /* tip */
)
}
el.forProcessed = true
// Concatenate complete code
return `${altHelper || '_l'}((${exp}), ` +
`function(${alias}${iterator1}${iterator2}) {` +
`return ${(altGen || genElement)(el, state)}` +
'}) '
}
Copy the code
The following code can be generated:
"_l((items),function(item,index){return _c('p',{key:index},[_v(_s(item))])}),0"
Copy the code
Here’s a description of the help methods used in the Render function:
- Vue.prototype._s converts to character type
- Vue.prototype._l Render list
- Vue.prototype._v Creates a vnode of text type
- Vue. Prototype. _c create vnode
conclusion
This is the general process of vUE template compilation: parsing HTML tags into AST nodes one by one using core regular expressions, and finally generating the body of the Render function based on the AST nodes. The Render function is responsible for generating vNodes.
This article tries to explain the compilation process of VUE clearly, but in the process of writing, I still feel that it is a little awkward. If you don’t understand, you can leave a message for discussion.
Next time we’ll talk about vUE rendering, such as vNode creation and diff.