The built version of Vuejs is divided into the full version and the run-time version. The difference between the full version and the run-time version is that the template compilation part is missing, so this note mainly introduces the learning process of the full version.
The overall architecture
The overall structure of VUejs is divided into three parts: core code, cross-platform related code, and common utility functions (that is, various helper functions in the code). The architecture is layered, with a common function at the bottom and an exit at the top that exports a complete constructor to the user for use. For example, to build a full version running on the Web platform, first create a Vue constructor, then add some methods to the prototype constructor, then add some global apis to the Vue constructor itself, then import web platform-specific code, and finally import the compiler. Eventually all the code was exported along with the entire Vue function, and now we can use Vuejs normally.
Take the vUE frame as an example:
You can see that the code instantiates a Vue and then mounts it to a DOM element (or passes in the option el argument), so it’s important to know what the Vue does when new Vue() to facilitate subsequent use.
To find out what Vue does when new Vue() is used, take a look at the Vue lifecycle.
The life cycle
The vuejs instance life cycle is divided into four phases: initialization phase -> template compilation phase -> mount phase -> Uninstall phase
- The initialization phase is between new Vue() and created lifecycle hooks, and both beforeCreate and Created lifecycle hooks are triggered. The main purpose of this phase is to initialize some properties, events, and reactive data on the VUejS instance. For example, props, methods, data, computed, watch, provide, and inject
- The template compilation phase is between created and beforeMount. During template compilation, the lifecycle hook function is not fired, it only determines whether there is an EL option and then the template option. After obtaining the template with both options, the template is compiled into a rendering function. (This process is only available in the full version)
- The mount phase is to render the template into the specified DOM node and turn on Watcher to track changes in dependencies. This phase fires two lifecycle hook functions, beforeMount and Mounted. Mounted components also perform operations such as updates, which also trigger two lifecycle hooks beforeUpdate and updated
- The uninstall phase offloads dependency tracing, child components, event listeners, and so on, triggering beforeDestroy and Destroy
So far, it has been concluded that when new Vue() is called, some initialization takes place, then template compilation, and finally mount. Once the VUE is mounted, we can use the methods and apis it provides for further development work such as updating content, uninstalling components, and so on.
- So how is vUE initialized?
- How is the template compiled after initialization?
- Once the template is compiled, how can it be mounted?
Initialization of vUE
Vue. Prototype. _init = function(options){// Initialize some code}Copy the code
Vue initializes the data by adding a _init method to the prototype, which is executed on new Vue() to complete the initialization.
Vue.prototype._init = function( options ){ vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor),options || {},vm) initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm,'beforeCreate') initInjections(vm) initState(vm) InitProvide (vm) callHook(vm,'created') // Then determine if the el option is passed when instantiating vuejs. If so, start template compilation and mount phases. $mounted () {// Start the template compiling and mounting phases.Copy the code
Including resolveConstructorOptions is to obtain the current instance constructor options in the options, and the parent’s constructor options (because vue is likely to be a child component). MergeOPtions merges these options into an object and assigns them to the $options property of the current vue instance. This allows the current component to see the options passed in, parent options, and its own options, and then use the various $options properties to aid in other process Settings.
Order of initialization: InitLifecycle ->initEvents->initRender and then trigger the beforeCreate lifecycle hook before initIjections->initState->initProvide Finally, the Created lifecycle hook is triggered and initialization is complete!
So what do these init functions do?
- InitLifecycle: Initializes instance properties. The first step in the initialization process is to instantiate the attributes needed internally by the VUejs and the external attributes provided to the user. The internal implementation is not complicated, just setting some properties and giving an initial value on the VUE instance. (Add attributes and default values to the object)
- InitEvents: initializes events. Initializing an event means adding an event registered by the parent component in the template using V-ON to the child component’s event system. This does not include binding events on HTML tags (because events are bound to templates). During template compilation, if the tag is a normal tag, the event bound to the tag will be registered with browser events. If the tag is a component, the component is instantiated and properties and events bound to the component are passed to it as parameters. So we just need to initialize the event passed by the parent component.)
- The first step in initialization is to initialize a _events property (an empty object with an event name and a value of event function) on the current VUE instance to hold events passed from the parent of the current instance
- $options._parentListeners are retrieved from the vm.$options._parentListeners, and if the component is being instantiated for the first time, they are iterated (using this.$on) to register the events into the _events of the current instance. If it is not the first time you are instantiating, compare the original event list with the one you are instantiating this time. If the old list does and the new list does not, then you need to remove the event from _events (using this.$off) or add it to _events instead. If both the old and new lists have this event attribute, but the event function changes, replace the current attribute with the event function in the new list in _events.
- InitInjections: Initializes Inject. The use of inject is unnecessary. The content injected by provide can be injected by all descendant components via Inject. So inject is read by the current component via the configured key (_provide property of the current component or parent component, because when providing inject content, it actually registers the content in the _provide property of the current component instance). If it fails to read, it will be read by its parent component ($parent, Initializing instance properties is handled), and so on until read. After reading it, we save it on the current instance, so we can access the injected content by providing the key in the inject of this point.
- InitState: indicates the initialization state. When we write code, we add data, methods, computed, and so on to our components. How do they get initialized? First, the sequence goes like this: if your component (an instance of Vue) uses props, initialize props first, then methods, then data, then computed, then watch, if you write methods. Obviously, there are some simple components that will not participate in the process of initializing a state if it is not used. The order of initialization is also determined but we can observe the previous state in the latter state. For example, we can use properties in props in data.
- In this step of the initialization state, we first initialize a _Watchers attribute to the Vue instance (the currently initialized component). It can save all watcher instances in the current component, whether registered with this.$watcher or with watcher().
- Initialize props: We set props to select the data needed by the current component from the data passed in (either by the parent component or by the options passed in at instantiation) and store it in the current instance. The first step to normalizing props is to format the props as an object or an array, so any way you want to write it or pass it to the parent component, there is an object structure that uses a small hump as the key.
- After normalizing props data, data selection and detail processing are performed. During the initialization process, there are two props data, one is set by the component itself, and the other is passed in by the parent or instantiation of the template. In this case, we need to compare the two props data. Get the keys of the props for the current component and store them in the _props property of the current component.X = this._props. We can then access the props data later with this.XXX. There are all sorts of details:
- The props of the root instance needs to be set reactive
- If the parent does not pass the props value, the default value is used
- Various types of processing of props values
- Save the key of the current props to the _propKeys of the current instance for subsequent updates
- Initialize Methods: Initialize methods by looping over the methods object in the options and mounting each property in turn to the current component. (Check if the properties in methods are identical to those in props, or if the properties start with $or _ will be warned on the console)
- _data, and then set an agent on the VM that can access this._data.X(same as the initialization props). However, in the meantime, you need to make some detailed decisions, such as whether the data option is valid. Whether the value is a function that needs to be executed (if so, then the result is assigned to data). Whether it is identical to the props and methods property names (it is not set on the agent if it is identical to the props and it is still set on the agent if it is identical to the methods property names), both cases are warned in the console. Whether it starts with $or _. If it starts with either, the property is not set on the agent and a warning is issued on the console. Finally, the data in vm._data is converted to reactive. Response principle
- Initialize computed: Computed attributes are cached and recalculated only if the reactive attributes on which the computed attributes depend or the return value of the computed attributes changes.
- When the calculated attributes are initialized, a _computedWatchers computedWatcher is added to the VM to hold the computedWatcher on the current instance.
- Whether or not the results of the calculated properties have changed is determined by watcher’s dirty property. When dirty is true, a recalculation is required; otherwise, the cached result is returned.
- The template uses watcher to read calculated properties. The calculated properties read the data in the property function and add their own watcher and component watcher to the dependency list of these properties. When one of the calculated properties changes, the watcher of the calculated properties is notified of the change. The watcher who calculated the property gets the notification, recalculates, compares the value of the calculated property to see if it has changed, and notifts the component to rerender if it has.
- If we set the watch for calculating the attributes ourselves, the watch will be notified when the data changes. If not, the Watch instance used to evaluate the property is notified.
- When initializing a compute property, a warning is issued to the console if the compute property has the same name as data or props. If the name is the same as methods, the calculated property is not warned and will fail.
- $watch(expOrFn,handler,options); initialize watch: Initialize watch logic by calling this.$watch(expOrFn,handler,options) method. The watch with different writing methods will be processed in a unified manner.
- Initialize provide: To initialize provide, simply add the provide option to the Vm. _provided. (Provide is either an object or a function that returns an object; in the case of an object, it assigns the object directly to the Vm. _provided; in the case of a function, So execute and assign to vm._provided)
At this point, the initialization of the Vue is complete, and it’s time to move on to template compilation.
Vue template compilation
Once the initialization is complete, it’s time to start compiling the template. First, vue needs to determine whether we passed the EL option, and if not, whether we passed the template option when vm.$mount is called. The template option is used if there is a template, or the EL option if there is no template. The obtained template is then compiled into a rendering function.
So how does a template compile into a render function? Compiling a template into a render function has two steps:
- 1. Parse templates into AST(Abstract Syntax tree)
- 2. Use AST to generate rendering functions
But since static nodes don’t need to be iterated all the time, we add an operation that iterates the AST to mark the static node so that when updating the node in the virtual DOM, if it is found to have this mark, it won’t be re-rendered. These three steps are realized by three modules:
- Parsing templates into AST(parsers)
- Traversing AST tagged static nodes (optimizer)
- Use AST to generate rendering functions
The parser
What the parser does is parse the template into an AST(an object that describes a node and contains information about the node, such as node type, node name, node parent, node children). The parser is divided into several child parsers, and the main one for template compilation is the HTML parser. It implements a number of hook functions: start, end, chars, comment, etc. These hook functions are triggered when parsing an HTML template. For example, parsing to the start tag triggers the start hook function.
A template is a string, and template parsing is about parsing a template string into an AST syntax tree (parsing into a large object). When a start tag is encountered, the start hook function is fired, the AST is built and the current node is pushed onto the stack, and parsing continues. When a text node is encountered, the chars hook function is fired and the AST is written to the children property (because the text node has no children, it will not be pushed onto the stack), until an end tag is encountered. Trigger the end hook function and pop it off the stack, and a small AST object is built. (Pushed on the stack to build a hierarchy between the parent and child, and popped on an end tag to ensure that the node in the stack is the parent of the node being built). The AST builds until the HTML parser hook function is not fired.
The optimizer
The optimizer does two main things:
- Find all static nodes and mark them
- Find all static root nodes and mark them
Determine whether the current node is a static node, using recursion to determine child nodes. Static node concept:
- The current node cannot be a text node with variables
- The current node is not dynamically bound
- No V-if, V-for, v-else
- Not built-in labels
- Is not a component
- There are no properties in a node that only dynamic nodes have
Because the judgment is made from the root down, there is a conflict when the parent node is marked as static and the child node as dynamic, because all the children under a static node should be static, otherwise the node is a dynamic node. Therefore, when the recursion finds that the child node under this node is a dynamic node, the static node flag of the parent level is also removed. (The tag is simply adding a static attribute to the AST object, distinguished by true and false). After finding the static node, look for the static node, and once the node is marked as the static root node, stop looking because the children of the static root node must be static nodes.
Code generator
The code generator is the final step in template compilation and is used to translate the AST into the contents of the rendering function (generating code strings). The code string can be wrapped in a rendering function and executed, resulting in a VNode from which the virtual DOM can render the view.
Generating code strings is actually a process of traversing the AST to generate strings and stitching them together. The root node is generated first, and then after the child node strings are generated, the stitching is done layer by layer in the parameters of the root node until it is complete. The code string format is as follows:
_c('div',{attrs:{"id":"XXX"},[_c('div',[_c('p',[_v("hello"+_s(name))])])])
Copy the code
The _c function creates the element (createElement), _v creates the text node, and _s is the toString variable. They are wrapped in with, and when the render function is executed, the code string in with is executed to create a VNode for view rendering.
Now that template compilation is complete, how to mount compiled templates?
$mount
The template’s compiled render function is assigned to options.render.
Mount method :(comp is the component to mount)
- comp.$mount(“#app”)
- comp({el:”#app”})
- document.querySelector(“#app”).appendChild(comp.$el)
The first step in mounting is to get the element to which the mount is to be mounted, which is the el option we passed in (el:”#app”). If not, create a div and save the result of this step. (The result of the render function is mounted to this element)
Step 2: Check if options.render has a saved render function. If the compiled rendering function of the template is not found, vue will determine whether the user passed in the template, that is, whether this option was passed in when we instantiated. If we don’t pass in the template option, Vue gets the template from the EL option we pass in. If not, it creates a div that clones the result saved in the first step into the div created in this step and reassigns it to template. If found, (template can exist in three ways: as a template string, as a selector starting with #, or as a DOM element), process each case and reassign to template. We now have a template, whether or not we found it in the first place, internally recompile it, generate a render function, and assign it to options.render.
Step 3: After ensuring that there are rendering functions to perform rendering, start implementing the mount. Trigger the beforeMount lifecycle hook before mounting, then use new Watcher() to save the component as a Watcher in vm._watcher, The vm.render() function is then re-executed in watcher’s update function when the component state changes. Thus achieving mount and responsive. Then triggers the Mounted lifecycle hook. Response principle
Vm. _watcher = new Watcher(vm,()=>{// Render ()= new (vm,()=>{// Render ()= new (vm,()=>{// Watcher can be notified. Vm.update (vm.render())})Copy the code
At this point, vUE completes the process from creation to rendering.
This note is a reference to vue. js written by Liu Bowen