The previous six blogs have helped us basically understand the concepts and principles of the responsive principle of vue, Data drive, component and component update, and the part about component and component update, we are based on the existing render function, that is to say, we already have vnode, and then discuss how vnode renders and updates.
In this blog, we will go back and see how the compiler of vue turns template into render function.
** What are the benefits of understanding the compile process? I personally think it can help us have a better understanding of the principles of some of vue’s own syntactic sugar. **
Since there are many details of Compiler, there is probably a conclusion and impression. Many of them will not go into detail about the syntax processing of vue itself, but will only use slot as an example.
Compiler entry
For the compiler entry point, vue does a series of currying to finally return the compile function.
This process is to pre-process the configuration related to the platform. For example, the compile of web compile and ssr will have some different configurations, and the platform will not change during the compile process, so these differences are first curried. Encapsulated in function closures, so that there is no need to judge the platform again and again when different components execute the compiler.
Currying
If you want to see how it is curried, you can take a look at this section. Not looking at it will not affect our understanding of the compile process.
When we use Vue.js with Runtime + Compiler, its entry is src/platforms/web/entry-runtime-with-compiler.js, take a look at its definition of the $mount function:
const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && query(el)
/* istanbul ignore if */ if (el = document.body || el = document.documentElement) { process.env.NODE_ENV ! 'production' && warn( `Do not mount Vue to <html> or <body> - mount to normal elements instead.` ) returnthis }
const options = this.$options // resolve template/el and convert to render function if (!options.render) { let template = options.template if (template) { if (typeof template = 'string') { if (template.charAt(0) = '#') { template = idToTemplate(template) /* istanbul ignore if */ if (process.env.NODE_ENV ! 'production' && !template) { warn( `Template element not found or is empty: ${options.template}`, this ) } } } elseif (template.nodeType) { template = template.innerHTML } else { if (process.env.NODE_ENV ! 'production') { warn('invalid template option:' + template, this) } returnthis } } elseif (el) { template = getOuterHTML(el) } if (template) { /* istanbul ignore if */ if (process.env.NODE_ENV ! 'production' && config.performance && mark) { mark('compile') }
The compileToFunctions method is to compile the template template to generate render and staticRenderFns, which are defined in src/platforms/web/compiler/index.js:
You can see that the’compileToFunctions’ method is actually the return value of the’createCompiler ‘method, which receives a compile configuration parameter. Next, let’s look at the definition of the’createCompiler’ method, in’src/compiler/index.js’:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// `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. exportconst createCompiler = createCompilerCreator(functionbaseCompile ( 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 } })
The createCompiler method is actually returned by calling the createCompilerCreator method. The parameter passed by this method is a function. The real compile process is executed in this baseCompile function. Then what is the createCompilerCreator? Its definition is in src/compiler/create-compiler.js:
You can see that the method returns a’createCompiler ‘function, which receives a’baseOptions’ parameter, returns an object, including the’compile ‘method properties and’compileToFunctions’ properties, this’compileToFunctions’ corresponds to the’compileToFunctions’ method called by the ‘$mount’ function, which is the return value of the call to the’createCompileToFunctionFn ‘method. Let’s take a look at the’createCompileToFunctionFn’ method, which is defined in’src/compiler/to-function/js’:
/* istanbul ignore if */ if (process.env.NODE_ENV ! 'production') { // detect possible CSP restriction try { newFunction('return 1') } catch (e) { if (e.toString().match(/unsafe-eval|CSP/)) { warn( 'It seems you are using the standalone build of Vue.js in an ' + 'environment with Content Security Policy that prohibits unsafe-eval. ' + 'The template compiler cannot work in this environment. Consider ' + 'relaxing the policy to allow unsafe-eval or pre-compiling your ' + 'templates into render functions.' ) } } }
// check function generation errors. // this should only happen if there is a bug in the compiler itself. // mostly for codegen development use /* istanbul ignore if */ if (process.env.NODE_ENV ! 'production') { if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) { warn( `Failed to generate render function:\n\n` + fnGenErrors.map(({ err, code }) =>`${err.toString()} in\n\n${code}\n`).join('\n'), vm ) } }
return (cache[key] = res) } }
So far we have finally found the final definition of’compileToFunctions’, which takes 3 parameters, compile template template, compile configuration options and Vue instance vm. The core compile process is just one line of code:
1
const compiled = compile(template, options)
The’compile ‘function is passed as a parameter when executing’createCompileToFunctionFn’. It is the’compile ‘function defined in the’createCompiler’ function, as follows:
The logic of the’compile 'function is to process the configuration parameters first, and the actual execution of the compile process is one line of code:
Optimize the ast tree, add some properties for some nodes for later rendering optimization, such as some nodes that will definitely not change plus a property indicating that it is a static node.
Generate code based on the optimized ast tree, which can be used to generate vnode
Let’s see the compile process through an example
parse
The compile process first parses the template and generates an AST. It is a Syntax Tree, which is a tree representation of the abstract syntax structure of the source code. In many compile technologies, such as babel compile ES6 code, it will be generated as AST.
This process is more complex, it will use a lot of Regular Expression to parse the string, if the regular is not very understanding, it is recommended to go to the knowledge of Regular Expression. In order to visually demonstrate the process of’parse ', let’s look at an example:
1 2 3
<ul:class="bindCls"class="list"v-if="isShow"> <liv-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li> </ul>
After the’parse 'process, the generated AST is as follows:
It can be seen that the generated AST is a tree structure, and each node is an’ast element ‘. In addition to some of its own properties, it also maintains its parent-child relationship, such as’parent’ pointing to its parent node, and’children 'pointing to its All sub-nodes. First, we have some intuitive impressions of the AST, so let’s analyze how this AST is obtained.
optimize
When our template template after the parse process, the output will generate AST tree, then we need to optimize the tree, optimize the logic is far simpler than the parse logic, so it will be much easier to understand.
Why should there be an optimization process, because we know that Vue is Data driven and responsive, but not all of our templates are responsive, and there are many data that will never change after the first render, so this part The DOM generated by the data will not change, and we can skip their comparison in the’patch 'process.
As the name implies, _c executes createElement to create a VNode, while _l renderList renders a list; _v createTextVNode creates a text VNode; _e creates an empty VNode for createEmptyVNode.
Source code analysis
Through the example just now, we can have a preliminary understanding of the entire compile process and the results of each step.
If you are interested, you can take a look at the source code of vue. Since there is a lot of source code, here is just a brief analysis.
parse
First, let’s take a look at the definition of’parse ‘, in’src/compiler/parser/index.js’:
The code of’parse 'function is very long, and it is not good to post it again to understand colleagues. I will first split it into pseudocode so that colleagues can have a general understanding of the overall process. Next, we will decompose and analyze the role of each pseudocode.
From
Corresponding pseudocode:
1
getFnsAndConfigFromOptions(options)
The input of’parse ‘function is’template’ and’options’, and the output is the root node of AST. ‘template’ is our template string, and’options’ are actually some platform-related configurations, which are defined in’src/platforms/web/compiler/options’:
These methods and configurations are required for subsequent analysis, and we don’t need to care about their specific functions. Let’s look back first.
Analysis
Corresponding pseudocode:
1
parseHTML(template, options)
The parsing of the template is mainly through the parseHTML function, which is defined in src/compiler/parser/html-parser:
Since the logic of’parseHTML ‘is also very complex, so I also used the pseudocode way of expression, the overall logic is to loop parsing’template’, using regular to do a variety of matching, for different cases were treated differently, until the entire template is parsed. During the matching process, the’advance 'function will be used to continuously advance the entire template string until the end of the string.
1 2 3 4
functionadvance (n) { index += n html = html.substring(n) }
The entire matching process is divided into the following steps:
Find the position of the first left angle bracket. If it is 0, it indicates that it is a label and enters the processing flow of the label. If not, it indicates that the angle bracket is in a certain string and enters the handlePlainTextElement () process
If it is a label, determine whether it is a comment node, if it is a comment node, skip the comment part
if it is doctype, skip
if it is a closing tag, i.e.
, advance the tag length and process the closing tag
If it is the start label, the forward label length, perform handleStartTag (), this function first creates a ast node out, the second is to push the current label and assign it to lastTag, why push the stack? It is to show that all labels are closed if the same label pops up from the stack when the closed label is processed in the previous step, and finally the stack is empty after the html compile is completed.
Of course, this process is a very simple introduction, which hides a lot of details. If you are interested, you can take a look at the source code for yourself.
optimize
Take a look at the definition of the’optimize ‘method, in’src/compiler/optimizer.js’:
/** * Goal of the optimizer: walk the generated template AST tree * and detect sub-trees that are purely static, i.e. parts of * the DOM that never needs to change. * * Once we detect these sub-trees, we can: * * 1. Hoist them into constants, so that we no longer need to * create fresh nodes for them on each re-render; * 2. Completely skip them in the patching process. */ exportfunctionoptimize (root: ?ASTElement, options: CompilerOptions) { if (!root) return isStaticKey = genStaticKeysCached(options.staticKeys || '') isPlatformReservedTag = options.isReservedTag || no // first pass: mark all non-static nodes. markStatic(root) // second pass: mark static roots. markStaticRoots(root, false) }
We can optimize some AST nodes into static nodes in the compile stage, so the whole process of’optimize ‘actually does two things,’ markStatic (root) ‘marks the static node,’ markStaticRoots (root, false) 'marks the static root.
functionmarkStatic (node: ASTNode) { node.static = isStatic(node) if (node.type = 1) { // do not make component slot content static. this avoids // 1. components not able to mutate slot nodes // 2. static slot content fails for hot-reloading if ( !isPlatformReservedTag(node.tag) && node.tag ! 'slot' && node.attrsMap['inline-template'] null ) { return } for (let i = 0, l = node.children.length; i < l; i++) { const child = node.children[i] markStatic(child) if (!child.static) { node.static = false } } if (node.ifConditions) { for (let i = 1, l = node.ifConditions.length; i < l; i++) { const block = node.ifConditions[i].block markStatic(block) if (!block.static) { node.static = false } } } } }
functionisStatic (node: ASTNode): boolean { if (node.type = 2) { // expression returnfalse } if (node.type = 3) { // text returntrue } return !!(node.pre || ( !node.hasBindings && // no dynamic bindings !node.if && !node.for && // not v-if or v-for or v-else !isBuiltInTag(node.tag) && // not a built-in isPlatformReservedTag(node.tag) && // not a component !isDirectChildOfTemplateFor(node) && Object.keys(node).every(isStaticKey) )) }
First execute node.static = isStatic (node)
'isStatic ‘is a judgment on whether an AST element node is static. If it is an expression, it is non-static; if it is plain text, it is static; for an ordinary element, if it has a pre attribute, then it uses the’v-pre’ directive, which is static, otherwise the following conditions must be met at the same time: no’v-if ‘,’ v-for ‘, no other directives (excluding’v-once’), non-built-in components, platform-reserved labels, non-template tags with’v-for ’ Direct sub-node, the’key 'of all attributes of the node satisfies the static key; if these are satisfied, then this AST node is a static node.
If this node is an ordinary element, then traverse all its children and recursion executes markStatic. Because all elseif and else nodes are not in children, if the ifConditions of the node is not null, then traverse ifConditions to get the block in all conditions, which is their corresponding AST node, and recursion executes markStatic. During these recursions, whenever a sub-node is not static, the static of its parent becomes false.
functionmarkStaticRoots (node: ASTNode, isInFor: boolean) { if (node.type = 1) { if (node.static || node.once) { node.staticInFor = isInFor } // For a node to qualify as a static root, it should have children that // are not just static text. Otherwise the cost of hoisting out will // outweigh the benefits and it's better off to just always render it fresh. if (node.static && node.children.length && !( node.children.length = 1 && node.children[0].type = 3 )) { node.staticRoot = true return } else { node.staticRoot = false } if (node.children) { for (let i = 0, l = node.children.length; i < l; i++) { markStaticRoots(node.children[i], isInFor || !!node.for) } } if (node.ifConditions) { for (let i = 1, l = node.ifConditions.length; i < l; i++) { markStaticRoots(node.ifConditions[i].block, isInFor) } } } }
The second parameter of markStaticRoots is isInFor. For nodes that are already static or v-once, node.staticInFor = isInFor. Then there is the judgment logic for’staticRoot ‘. From the comments, we can see that for nodes eligible to be’staticRoot’, in addition to being a static node themselves, they must satisfy the need to have’children ‘, and’children’ cannot be just a text node, otherwise the benefits of marking it as a static root node are very small.
Next, as with the logic for marking static nodes, iterate over’children ‘and’ifConditions’, and recursion executes’markStaticRoots’.
codegen
The’generate ‘function is defined in’src/compiler/codegen/index.js’:
The’generate ‘function first generates’code’ with’genElement (ast, state) ‘, and then wraps’code’ with’with (this) {return ${code}}} ‘. Here’state’ is an instance of’CodegenState ‘, which we will introduce later when we use it. Let’s take a look at’genElement’ first:
const children = el.inlineTemplate ? null : genChildren(el, state, true) code = `_c('${el.tag}'${ data ? `,${data}` : '' // data }${ children ? `,${children}` : '' // children })` } // module transforms for (let i = 0; i < state.transforms.length; i++) { code = state.transforms[i](el, code) } return code } }
Basically, it is to determine the attributes of the current AST element node to perform different code generation functions.
slot
The above source code analysis part omits a lot of details, some are related to html parsing, such as parseHTML function only writes pseudo-code, some are related to vue syntax sugar processing, such as codegen only writes genIF, genSlot, what exactly does not say, here we use slot as an example, others need you can see for yourself.
Let’s start with compile. We know that compile occurs when’vm. $mount 'is called, so the order of compile is to compile the parent component first, and then compile the child components.
First compile the parent component. In the’parse ‘stage, the’processSlot’ will be executed to process the’slot ‘, which is defined in’src/compiler/parser/index.js’:
functionprocessSlot (el) { if (el.tag = 'slot') { el.slotName = getBindingAttr(el, 'name') if (process.env.NODE_ENV ! 'production' && el.key) { warn( `\`key\` does not work on <slot> because slots are abstract outlets ` + `and can possibly expand into multiple elements. ` + `Use the key on a wrapping element instead.` ) } } else { let slotScope if (el.tag = 'template') { slotScope = getAndRemoveAttr(el, 'scope') /* istanbul ignore if */ if (process.env.NODE_ENV ! 'production' && slotScope) { warn( `the "scope" attribute for scoped slots have been deprecated and ` + `replaced by "slot-scope" since 2.5. The new "slot-scope" attribute ` + `can also be used on plain elements in addition to <template> to ` + `denote scoped slots.`, true ) } el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope') } elseif ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) { /* istanbul ignore if */ if (process.env.NODE_ENV ! 'production' && el.attrsMap['v-for']) { warn( `Ambiguous combined usage of slot-scope and v-for on <${el.tag}> ` + `(v-for takes higher priority). Use a wrapper <template> for the ` + `scoped slot to make it clearer.`, true ) } el.slotScope = slotScope } const slotTarget = getBindingAttr(el, 'slot') if (slotTarget) { el.slotTarget = slotTarget = '""' ? '"default"' : slotTarget // preserve slot as an attribute for native shadow DOM compat // only for non-scoped slots. if (el.tag ! 'template' && !el.slotScope) { addAttr (el, 'slot', slotTarget) } } } }
When it parses to the’slot ‘attribute on the label, the corresponding AST will be given. The element node adds the’slotTarget ‘attribute, and then in the’codegen’ stage, the’slotTarget ‘will be processed in’genData’, and the relevant code is in’src/compiler/codegen/index.js’:
1 2 3
if (el.slotTarget && !el.slotScope) { data += `slot:${el.slotTarget},` }
Will add a’slot ‘property to’data’ and point to’slotTarget ', which will be used later. In our example, the code generated by the parent component is as follows:
Next compile subcomponent, also in the’parser ‘stage will execute the’processSlot’ processing function, which is defined in’src/compiler/parser/index.js’:
When the’slot ‘tag is encountered, the’slotName’ attribute will be added to the corresponding AST element node, and then in the’codegen ‘phase, it will determine if the current AST element node is a’slot’ tag, then execute the’genSlot ‘function, which is defined in’src/compiler/codegen/index.js’:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
functiongenSlot (el: ASTElement, state: CodegenState): string { const slotName = el.slotName || '"default"' const children = genChildren(el, state) let res = `_t(${slotName}${children ? `,${children}` : ''}` const attrs = el.attrs && `{${el.attrs.map(a => `${camelize(a.name)}:${a.value}`).join(',')}}` const bind = el.attrsMap['v-bind'] if ((attrs || bind) && !children) { res += `,null` } if (attrs) { res += `,${attrs}` } if (bind) { res += '${attrs ? " : ',null'},${bind}' } return res + ')' }
Let’s not consider the case of’attrs’ and’v-bind ‘on the’slot’ tag for now, so the code it generates is actually only:
1 2 3
const slotName = el.slotName || '"default"' const children = genChildren(el, state) let res = `_t(${slotName}${children ? `,${children}` : ''}`
The’slotName ‘here is taken from the property corresponding to the AST element node, the default is’default’, and’children ‘corresponds to the content wrapped in the beginning and closing tags of the’slot’. Take a look at the code finally generated by the subcomponent of our example, as follows:
In the compile section, we learned that the _t function corresponds to the renderSlot method, which is defined in src/core/instance/render-heplpers/render-slot.js:
The parameter “name” of “render-slot” represents the slot name “slotName”, and “fallback” represents the “vnode” array generated by the default content of the slot. Ignore “scoped-slot” for now, and only look at the default slot logic. If “this. $slot [name]” has a value, return its corresponding “vnode” array, otherwise return “fallback”. So where did this. $slot "come from? We know that the’init ‘timing of the subcomponent is when the parent component executes the’patch’ process, and the parent component has already compiled at this time. And the subcomponent will execute the’initRender ‘function during the’init’ process, and get the’vm. $slot ‘when the’initRender’ is performed, and the relevant code is in’src/core/instance/render.js’:
1 2 3 4 5 6
exportfunctioninitRender (vm: Component) { // ... const parentVnode = vm.$vnode = options._parentVnode// the placeholder node in parent tree const renderContext = parentVnode && parentVnode.context vm.$slots = resolveSlots(options._renderChildren, renderContext) }
‘Vm. $slots’ is returned by executing’resolveSlots (options._renderChildren, renderContext) ‘, which is defined in’src/core/instance/render-helpers/resolve-slots.js’:
/** * Runtime helper for resolving raw children VNodes into a slot object. */ exportfunctionresolveSlots ( children: ?Array<VNode>, context: ?Component ): { [key: string]: Array<VNode> } { const slots = {} if (!children) { return slots } for (let i = 0, l = children.length; i < l; i++) { const child = children[i] const data = child.data // remove slot attribute if the node is resolved as a Vue slot node if (data && data.attrs && data.attrs.slot) { delete data.attrs.slot } // named slots should only be respected if the vnode was rendered in the // same context. if ((child.context = context || child.fnContext = context) && data && data.slot != null ) { const name = data.slot const slot = (slots[name] || (slots[name] = [])) if (child.tag = 'template') { slot.push.apply(slot, child.children || []) } else { slot.push(child) } } else { (slots.default || (slots.default = [])).push(child) } } // ignore slots that contains only whitespace for (const name in slots) { if (slots[name].every(isWhitespace)) { delete slots[name] } } return slots }
The resolveSlots method takes 2 parameters. The first parameter chilren corresponds to the children of the parent vnode, which in our case is the contents of the wraps < app-layout > and . The second parameter context is the context of the parent vnode, which is the vm instance of the parent component.
The logic of the resolveSlots function is to traverse the chilren, get the data of each child, and then get the slot name through data.slot, which is the data.slot set by the compile parent component in the codegen stage. Then add’child ‘to’slots’ with the slot name’key ‘. If’data.slot’ does not exist, it is the content of the default slot, and add the corresponding’child ‘to’slots.defaults’. This gets the entire’slots’, which is an object, ‘key’ is the slot name, and’value ‘is an array of type’vnode’, because it can have multiple slots with the same name.
In this way, we get the’vm. $slots’, return to the’renderSlot ‘function,’ const slotNodes = this. $slots [name] ‘, and we can also get the corresponding’vnode’ array according to the slot name. The’vnode 'in this array is created in the parent component, so that the content of the child component slot is replaced in the parent group.
The corresponding’slot ‘is rendered as’vnodes’, and the’children ‘of the’vnode’ are rendered as the current component. The subsequent rendering process has been analyzed before and will not be repeated.