It took two sections to introduce components, from the principle of components to the application of components, including the implementation and usage scenarios of asynchronous components and functional components. As we all know, components run through the entire Vue design concept, and are also the core ideas that guide our development. Therefore, in the next few articles, we will return to the content of components for source code analysis. First, we will start with commonly used dynamic components. Including the principle of inline templates, the concept of built-in components will be briefly mentioned at the end, paving the way for future articles.
12.1 Dynamic components
I believe that most of the dynamic components will be used in the development process. When we need to switch states between different components, dynamic components can meet our needs very well. The core is the component tag and the is attribute. use.
12.1.1 Basic usage
The example is a basic usage scenario of a dynamic component. When the button is clicked, the view switches among components child1, child2, and child3 according to the value of this.chooseTabs.
copy// vue <div id="app"> <button @click="changeTabs('child1')">child1</button> <button @click="changeTabs('child2')">child2</button> <button @click="changeTabs('child3')">child3</button> <component :is="chooseTabs"> </component> </div> // js var child1 = { template: '<div>content1</div>', } var child2 = { template: '<div>content2</div>' } var child3 = { template: '<div>content3</div>' } var vm = new Vue({ el: '#app', components: { child1, child2, child3 }, methods: { changeTabs(tab) { this.chooseTabs = tab; } } })
12.1.2 AST parsing
The interpretation of <component> is consistent with the content of the previous articles. It will start from the AST parsing stage, and the process will not focus on every detail, but will specifically explain the differences from the previous processing methods. For the differences in dynamic component parsing, the focus is on processComponent. Due to the existence of the is attribute on the tag, it will mark the component attribute on the final ast tree.
copy// Analysis for dynamic components function processComponent (el) { var binding; // Get the value corresponding to the is attribute if ((binding = getBindingAttr(el, 'is'))) { // There are more component attributes on the ast tree el.component = binding; } if (getAndRemoveAttr(el, 'inline-template') != null) { el.inlineTemplate = true; } }
The final ast tree is as follows:
12.1.3 render function
With the ast tree, the next step is to generate an executable render function based on the ast tree. Due to the component attribute, the generation process of the render function will go to the genComponent branch.
copy// render function generator function var code = generate(ast, options); // Implementation of the generate function function generate (ast,options) { var state = new CodegenState(options); var code = ast ? genElement(ast, state) : '_c("div")'; return { render: ("with(this){return " + code + "}"), staticRenderFns: state.staticRenderFns } } function genElement(el, state) { ··· var code; // Dynamic component branching if (el.component) { code = genComponent(el.component, el, state); } }
The processing logic for dynamic components is actually very simple. When there is no inline template flag (will be discussed later), the subsequent child nodes are obtained for splicing. The only difference from ordinary components is that the first parameter of _c is no longer a The specified string, but a variable representing the component.
copy// Handling for dynamic components function genComponent ( componentName, el, state ) { // children is null when having inlineTemplate attribute var children = el.inlineTemplate ? null : genChildren(el, state, true); return ("_c(" + componentName + "," + (genData$2(el, state)) + (children ? ("," + children) : '') + ")") }
12.1.4 Comparison between ordinary components and dynamic components
In fact, we can compare the difference between ordinary components and dynamic components in the render function, and the result is clear at a glance.
The render function of common components
"with(this){return _c('div',{attrs:{"id":"app"}},[_c('child1',[_v(_s(test))])],1)}"
The render function of the dynamic component
"with(this){return _c('div',{attrs:{"id":"app"}},[_c(chooseTabs,{tag:"component"})],1)}"
In a nutshell, the difference between dynamic components and normal components is:
- The component attribute is added to the ast stage, which is the symbol of dynamic components
- In the stage of generating the render function, due to the existence of the component attribute, the genComponent branch will be executed. genComponent will perform special processing for the execution function of the dynamic component. Unlike ordinary components, the first parameter of _c is no longer a constant string. Instead, specify the component name variable.
- The process from render to vnode is the same as that of ordinary components, except that the string is replaced by a variable, and there is a data attribute of { tag: 'component' }. In the example, chooseTabs takes child1 at this time.
With the render function, the next process from vnode to real node is basically the same in process and idea as ordinary components. At this stage, you can review the analysis of the component process introduced earlier
12.1.5 Doubt
Since my understanding of the source code is not thorough enough, after reading the creation process of the dynamic component, I have a question in my mind. From the process analysis of the principle, the core of the dynamic component is actually the keyword is. It uses the component attribute in the compilation stage Define the component as a dynamic component, and the component as a label does not seem to be particularly useful. As long as the is keyword exists, setting the component label name to any custom label can achieve the effect of a dynamic component? (componenta, componentb). This string only exists in the data attribute of vnode in the form of { tag: 'component' }. Does that mean that the so-called dynamic components are only due to the unilateral limitation of is? Where is the meaning of the component tag? (Ask for advice, big brother!!)
12.2 Inline Templates
Since dynamic components can have inline-template as configuration in addition to is as a value, this premise can just clarify the principle and design idea of inline templates in Vue. Vue has a striking statement on the official website, reminding us that inline-template will make the scope of the template more difficult to understand. Therefore, it is recommended to use the template option to define templates instead of inline templates. Next, let's locate the reason why the so-called scope is difficult to understand through the source code.
Let's simply adjust the above example first, starting from the usage point of view:
copy// html <div id="app"> <button @click="changeTabs('child1')">child1</button> <button @click="changeTabs('child2')">child2</button> <button @click="changeTabs('child3')">child3</button> <component :is="chooseTabs" inline-template> <span>{{test}}</span> </component> </div> // js var child1 = { data() { return { test: 'content1' } } } var child2 = { data() { return { test: 'content2' } } } var child3 = { data() { return { test: 'content3' } } } var vm = new Vue({ el: '#app', components: { child1, child2, child3 }, data() { return { chooseTabs: 'child1', } }, methods: { changeTabs(tab) { this.chooseTabs = tab; } } })
The effect achieved in the example is consistent with the first example in the article. Obviously, the biggest difference from the previous cognition is that the environment in the parent component can access the environment variables inside the child component. At first glance, it seems incredible. Let's recall the previous situation where the parent component can access the child component. There are two general directions:
- 1. Using the event mechanism, the child component informs the parent component of the state of the child component through the $emit event, so that the parent can access the child. - 2. Use the scope slot method to pass the child's variables to the parent in the form of props, and the parent receives it through the grammatical sugar of v-slot, and the result of our previous analysis is that this method is essentially passed The form of event dispatch to notify the parent component.
The previous analysis process also mentioned that the parent component cannot access the variables of the sub-environment. The core reason is: all content in the parent template is compiled in the parent scope; all content in the sub-template is compiled in the sub-environment compiled in scope. Then we have reason to guess, does the inline template violate this principle, let the parent's content be put into the child component creation process to compile? Let's look down:
Going back to the ast parsing stage, we analyzed earlier that the key to parsing dynamic components lies in the processing of the is attribute by the processComponent function. Another key is the processing of the inline-template, which adds the inlineTemplate attribute to the ast tree.
copy// Analysis for dynamic components function processComponent (el) { var binding; // Get the value corresponding to the is attribute if ((binding = getBindingAttr(el, 'is'))) { // There are more component attributes on the ast tree el.component = binding; } // Add inlineTemplate attribute if (getAndRemoveAttr(el, 'inline-template') != null) { el.inlineTemplate = true; } }
In the render function generation stage, due to the existence of inlineTemplate, the child node of the parent's render function is null. This step also determines that the template under the inline-template is not compiled in the parent component stage. How is the template passed to the compilation process of the child component? Woolen cloth? The answer is that the template exists in the form of attributes, and the attribute values are obtained when the sub-instance is reached
copyfunction genComponent (componentName,el,state) { // children is null when having inlineTemplate attribute var children = el.inlineTemplate ? null : genChildren(el, state, true); return ("_c(" + componentName + "," + (genData$2(el, state)) + (children ? ("," + children) : '') + ")") }
Let's look at the result of the final render function, where the template exists in the inlineTemplate property of the parent component in the form of {render: function(){···}}.
"_c('div',{attrs:{"id":"app"}},[_c(chooseTabs,{tag:"component",inlineTemplate:{render:function(){with(this){return _c('span',[_v(_s(test))])}},staticRenderFns:[]}})],1)"
The final vnode result also shows that the inlineTemplate object will remain in the data property of the parent component.
copy// vnode result { data: { inlineTemplate: { render: function() {} }, tag: 'component' }, tag: "vue-component-1-child1" }
After having vnode, we come to the critical last step, the process of generating real nodes according to vnode. Starting from the root node, when you encounter vue-component-1-child1, you will go through the process of instantiating and creating subcomponents. Before instantiating the subcomponents, you will first process the inlineTemplate attribute.
copyfunction createComponentInstanceForVnode (vnode,parent) { // Default options for subcomponents var options = { _isComponent: true, _parentVnode: vnode, parent: parent }; var inlineTemplate = vnode.data.inlineTemplate; // Inline template processing, get the render function and staticRenderFns respectively if (isDef(inlineTemplate)) { options.render = inlineTemplate.render; options.staticRenderFns = inlineTemplate.staticRenderFns; } // Execute vue subcomponent instantiation return new vnode.componentOptions.Ctor(options) }
The default option configuration of the subcomponent will get the render function of the template according to the inlineTemplate attribute on the vnode. The conclusion of the analysis at this point is already very clear. The content of the inline template will eventually be resolved in the child component, so it is not surprising that the scope of the child component can be obtained in the template.
12.3 Built-in components
Finally, let’s talk about another concept in Vue’s thinking, built-in components. In fact, the official document of vue has listed the built-in components, which are component, transition, transition-group, keep-alive, slot, among which <slot> we are inserting The section on slots has already been introduced in detail, and the section on the use of component s also spends a lot of space on analysis from usage to principle. However, after learning about slots and components, I began to realize that slots and components are not real built-in components. Built-in components are components that have been registered globally during the source code initialization phase. However, <slot> and <component> are not treated as a component, so there is no component life cycle. The slot will only be converted to the renderSlot function for processing in the render function stage, and the component will only use the is attribute to convert the first parameter of createElement from a string to a variable, nothing more. So returning to the understanding of the concept, built-in components are components provided by the source code itself, so the focus of this part of the content will be on when the built-in components are registered and what are the differences when compiling. This part is just an introduction. Next, there will be two articles dedicated to detailing the implementation principles of keep-alive, transition, and transition-group.
12.3.1 Constructor-defined components
In the initialization phase of Vue, three component objects will be added to the components property of the constructor. The writing method of each component object is consistent with our writing method in the custom component process. It has a render function, a life cycle, and various data definitions.
copy// keep-alive component options var KeepAlive = { render: function() {} } // transition component options var Transition = { render: function() {} } // transition-group component options var TransitionGroup = { render: function() {}, methods: {}, ··· } var builtInComponents = { KeepAlive: KeepAlive }; var platformComponents = { Transition: Transition, TransitionGroup: TransitionGroup }; // Option configuration of Vue constructor, compoents option merge extend(Vue.options.components, builtInComponents); extend(Vue.options.components, platformComponents);
The extend method we said at the beginning of the series, when analyzing the option merge, merges the attributes on the object into the source object, and overwrites if the attributes are the same.
copy// Merge the _from object into the to object, and when the attributes are the same, overwrite the attributes of the to object function extend (to, _from) { for (var key in _from) { to[key] = _from[key]; } return to }
Finally, the Vue constructor has configuration options for three components.
copyVue.components = { keepAlive: {}, transition: {}, transition-group: {}, }
12.3.2 Registering built-in components
Just having a definition is not enough. Components need to be used globally and registered globally. During the initialization process of a Vue instance, the most important first step is to merge options, and resource class options like built-in components will have a special option merge strategy, and finally the component options on the constructor will be registered in the form of a prototype chain. In the components option of the instance (the same is true for directives and filters).
copy// resource options var ASSET_TYPES = [ 'component', 'directive', 'filter' ]; // Define a strategy for resource consolidation ASSET_TYPES.forEach(function (type) { strats[type + 's'] = mergeAssets; // Define default policy }); function mergeAssets (parentVal,childVal,vm,key) { var res = Object.create(parentVal || null); // Create an empty object based on parentVal if (childVal) { assertObjectType(key, childVal, vm); // The components, filters, and directives options must be objects return extend(res, childVal) // Subclass options are assigned to an empty object } else { return res } }
One of the two key steps is var res = Object.create(parentVal || null);, it will create an empty object based on parentVal as the prototype, and finally copy the user-defined component options to the empty object through extend. After the options are merged, the built-in components are thus registered globally.
copy{ components: { child1, __proto__: { keepAlive: {}, transition: {}, transitionGroup: {} } } }
Finally, let's see that there is no template template in the built-in component object, but a render function. In addition to reducing the performance-consuming template parsing process, I think the important reason is that the built-in component does not have a rendered entity. Finally, let us look forward to the follow-up analysis of the principles of keep-alive and transition, so stay tuned.