Previous: Vue2.x source code learning preparation
This article mainly looks at the mixing of initMixin and the initProxy(vm), initInjections(vm) and initProvide(vm) methods involved;
preparation
When we use Vue, we initialize it through new Vue(). Where does this Vue come from?
1. Vue introduced in main.js is exposed in the entry file src/platforms/web/entry-runtimes.js;
2. Vue in the entry file is imported from src/platforms/web/runtime/index.js;
3. Vue in src/platforms/web/runtime/index.js is imported from src/core/index.js;
4. Vue in src/core/index.js is imported from src/core/instance/index.js;
In this way, the ontology of Vue is found; So what exactly is Vue?
In the src/core/instance/index.js file
//vue ontology is a method or a class function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) } //Mix some custom prototype methods into the prototype of vue initMixin(Vue) // Definition_ init method stateMixin(Vue) // Define data related methods $set, $delete, $watch eventsMixin(Vue) // Define event related methods $on, $once, $off, $emit lifecycleMixin(Vue) // Definition_ update, $forceUpdate, and lifecycle method $destroy renderMixin(Vue) // Define method $nextTick_ Render (convert render function to vnode) export default Vue
The warn information here directly tells us the essence of Vue: Vue is a constructor and should be called with the "new" keyword; Then execute this. In the Vue constructor_ Init (options) method, pass in initialization parameters; At the end of the code, several methods are executed to mix some custom prototype methods into the Vue prototype, which will be described below.
1, Initialize mixin (Vue)
In the src/core/instance/init.js file
let uid = 0 export function initMixin (Vue: Class<Component>) { Vue.prototype._init = function (options?: Object) { const vm: Component = this // Each component is initialized with a unique identifier vm._uid = uid++ let startTag, endTag // The related methods of the performance attribute of window record the performance through the related values passed in if (process.env.NODE_ENV !== 'production' && config.performance && mark) { startTag = `vue-perf-start:${vm._uid}` endTag = `vue-perf-end:${vm._uid}` mark(startTag) } // After initialization, the vm instance is marked as true, which will be used in other scenarios, such as observer vm._isVue = true // Merge options if (options && options._isComponent) { // Instance in component form // Add some attributes to vm.$options initInternalComponent(vm, options) } else { // Non component instances vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } //Agent initialization, different initialization methods in different environments if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else {// If it is not a development environment, the name of the vue instance_ The renderProxy property points to the vue instance itself vm._renderProxy = vm } // expose real self vm._self = vm initLifecycle(vm) // Initialize the component instance related attributes $parent, $children, $root, $refs, etc initEvents(vm) // Initialize custom events initRender(vm) // Mount the method that can convert the render function to vnode callHook(vm, 'beforeCreate') //Call the beforeCreate hook function initInjections(vm) // Initialize inject initState(vm) // Initialize data/props initProvide(vm) // Initialize provide callHook(vm, 'created') //Call the created hook function // The related methods of the performance attribute of window record the performance through the related values passed in if (process.env.NODE_ENV !== 'production' && config.performance && mark) { vm._name = formatComponentName(vm, false) mark(endTag) measure(`vue ${vm._name} init`, startTag, endTag) } //The configuration item has el, and the $mount method is automatically called to mount. The (flag of mounting is to render the template into the final DOM if (vm.$options.el) { vm.$mount(vm.$options.el) } } }
This part mainly includes the following operations:
1. Cache the current context into the vm variable;
2. Add a unique identifier for the vm;
3. Marking is used to test performance;
4. Use_ isVue value identifies the current instance;
5. Merge options to distinguish between component instances and non component instances;
6. Initialize the agent;
7. The following is the initialization of some components, events, render, inject, data/props and provide;
8. Mark again to test performance;
9. el mount.
Summary: first merge options = > initialize agent = > initialize component instance related attributes, determine the parent-child relationship of component (Vue instance) = > initialize events, pass parent component custom events to child components = > bind the method to convert render function into vnode = > call beforeCreate lifecycle hook = > initialize inject, Enable the sub component to access the corresponding dependency = > mount the state (props, methods, data, computed, watch) defined by the component under this = > initialize provide to provide dependency for the sub component = > call the created life cycle hook = > execute $mount mount.
1. initInternalComponent: parameters for merging component instances
// Optimize internal component instantiation because dynamic option merging is very slow and internal component options do not require special processing. export function initInternalComponent (vm: Component, options: InternalComponentOptions) { const opts = vm.$options = Object.create(vm.constructor.options) //This is done because it is faster than dynamic enumeration const parentVnode = options._parentVnode opts.parent = options.parent opts._parentVnode = parentVnode const vnodeComponentOptions = parentVnode.componentOptions opts.propsData = vnodeComponentOptions.propsData opts._parentListeners = vnodeComponentOptions.listeners opts._renderChildren = vnodeComponentOptions.children opts._componentTag = vnodeComponentOptions.tag if (options.render) { opts.render = options.render opts.staticRenderFns = options.staticRenderFns } }
Use the Object.create function to mount the options of the component constructor to the options of vm.$options__ proto__ On, specify the prototype of vm.$options; Add some attributes to vm.$options through the passed in parameter options, and mount the props, listeners and other attributes of the component dependent on the parent component to vm.$options to facilitate the call of child components.
2. resolveConstructorOptions: merges the parent constructor parameters with the instance itself parameters
Distinguish between Vue constructor and Vue.extend extender. Ctor.super is the attribute defined in Vue.extend. If it is a constructor, it returns parameters directly. If it is an extender, it executes internal code.
export function resolveConstructorOptions (Ctor: Class<Component>) { //option on constructor let options = Ctor.options //If there is a super attribute, it means that Ctor is a subclass built by Vue.extend, and super points to the parent constructor if (Ctor.super) { //Recursively obtain the latest options on the parent (there may be more than one parent, so recursion is required) const superOptions = resolveConstructorOptions(Ctor.super) //The default options of the parent when extend const cachedSuperOptions = Ctor.superOptions //Some new attributes may be mixed by Vue.mixin, so here we need to judge whether the parent class has changed. If it has changed, the assignment will be updated if (superOptions !== cachedSuperOptions) { // The options input parameter has changed. Modify the default parameter Ctor.superOptions = superOptions //Check whether there are any options for later modification / addition (mixin) const modifiedOptions = resolveModifiedOptions(Ctor) // Update the basic extension options, which are usually used when mixing in new options if (modifiedOptions) { extend(Ctor.extendOptions, modifiedOptions) } options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions) if (options.name) { options.components[options.name] = Ctor } } } return options } //Gets the options item that changes after the extend is executed function resolveModifiedOptions (Ctor: Class<Component>): ?Object { let modified //Own options const latest = Ctor.options //options encapsulated when executing Vue.extend const sealed = Ctor.sealedOptions for (const key in latest) { if (latest[key] !== sealed[key]) { if (!modified) modified = {} modified[key] = latest[key] } } return modified }
When you execute extend, Vue.mixin will appear to mix some parameters into the parent and child. At this time, you need to judge whether the parameters of the parent and child have changed before and after the extender is executed. If so, update the latest parameters; Finally, the options of the combined constructor are returned.
3. mergeOptions: merge instance parameters and input parameters
In the src/core/util/options.js file, the purpose of this method is to combine the constructor options and the passed in options.
export function mergeOptions ( parent: Object, //Constructor parameters child: Object, //Pass in parameters when instantiating vm?: Component //Instance itself ): Object { //Check whether the component name is legal, and issue a warning message if it does not comply with the law. if (process.env.NODE_ENV !== 'production') { checkComponents(child) } //If the child is of type function, we take its options attribute as the child if (typeof child === 'function') { child = child.options } //Respectively, convert the props, inject and directives attributes in options into the form of objects normalizeProps(child, vm) normalizeInject(child, vm) normalizeDirectives(child) //Only merge options have_ base attribute //When there is a mixin or extends attribute in the passed in options, call the mergeOptions method again to merge the contents of mixins and extensions to the constructor options of the instance if (!child._base) { if (child.extends) { parent = mergeOptions(parent, child.extends, vm) } if (child.mixins) { for (let i = 0, l = child.mixins.length; i < l; i++) { parent = mergeOptions(parent, child.mixins[i], vm) } } } const options = {} let key for (key in parent) { mergeField(key) } for (key in child) { if (!hasOwn(parent, key)) { mergeField(key) } } //Core consolidation strategies strats, defaultStrat function mergeField (key) { const strat = strats[key] || defaultStrat options[key] = strat(parent[key], child[key], vm, key) } return options }
When you merge options, you will merge props, inject, and directives respectively; When there is a mixin or extends attribute in the passed in options, the mergeOptions method will be called again to merge the contents of mixins and extensions to the constructor options of the instance.
Then, the core part of this method is to circulate parent and child respectively. When child is recycled, additional judgement is made that the current attribute is not in the parent attribute, then the mergeField method is used to merge options through strats and defaultStrat merge strategy.
The logic of defaultstring is that if the attribute value on the child exists, the attribute value on the child is taken; if it does not exist, the attribute value on the parent is taken.
strats subdivides several strategies:
1. el, propsData: the defaultStrat strategy of going directly;
2. component, directive, filter: cache the parent first; If the child has any, it shall be merged, and the child shall prevail;
3. watch: the child attribute does not exist, and the parent is returned directly; If the child attribute exists, judge whether it is an object; If the parent attribute does not exist, the child attribute is returned directly; Merge if both exist;
4. props, methods, inject, computed: if the child attribute exists, judge whether it is an object. If there is no such attribute on the parent, directly return the attribute on the child; If both child and parent exist, they are merged, and the value of child shall prevail;
5. Hook function: if it does not exist on child but exists on parent, it returns the attribute on parent; If there are both child and parent, the attribute after concat is returned (the child with the same name overrides the parent); If there is a child but there is no child on the parent, the child attribute is returned (this attribute must be an array. If not, it will be converted to an array);
6. Data, provide: it will distinguish whether Vue instances are merged or not; It is a Vue instance. If options has the data attribute, it calls mergeData to merge child and parent. If not, it follows the defaultStrat policy; It is not a Vue instance. If there is no child, it will return parent; if there is no parent, it will return child; if there are both, it will call mergeData to merge child and parent;
In short, the child attribute will prevail.
Here, all business logic and some features of components are transformed into vm.$options. For later use, you only need to take values from vm.$options.
2, initProxy: initializes the proxy
let initProxy initProxy = function initProxy (vm) { // Judge whether the Proxy in the current environment is available if (hasProxy) { // Determine which proxy handler to use const options = vm.$options const handlers = options.render && options.render._withStripped ? getHandler : hasHandler vm._renderProxy = new Proxy(vm, handlers) } else { vm._renderProxy = vm } } const hasHandler = { has (target, key) { const has = key in target const isAllowed = allowedGlobals(key) || (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data)) if (!has && !isAllowed) { if (key in target.$data) warnReservedPrefix(target, key) else warnNonPresent(target, key) } return has || !isAllowed } } const getHandler = { get (target, key) { if (typeof key === 'string' && !(key in target)) { if (key in target.$data) warnReservedPrefix(target, key) else warnNonPresent(target, key) } return target[key] } } export { initProxy }
If the current environment proxy is not available, the Vue instance's_ The renderProxy attribute points to the Vue instance itself; Proxy is available. If render exists on the options of the instance, then render exists_ The withstriped property calls getHandler, otherwise it calls hasHandler.
getHandler: operates when reading the properties of the proxy object. If the property is not a string or does not exist, an error is reported; otherwise, the property value is returned;
hasHandler: used to prompt when calling vm attribute incorrectly during development;
Note: options. Render_ Withstriped is enabled only when with is not supported in strict mode and is manually set to true, so hasHandler is generally used
3, initInjections(vm), initProvide(vm)
In the src/core/instance/inject.js file
export function initInjections (vm: Component) { //Get the dependency corresponding to the inject option const result = resolveInject(vm.$options.inject, vm) if (result) { //Turn off responsive binding toggleObserving(false) //Traverse each attribute Object.keys(result).forEach(key => { if (process.env.NODE_ENV !== 'production') { defineReactive(vm, key, result[key], () => { warn( `Avoid mutating an injected value directly since the changes will be ` + `overwritten whenever the provided component re-renders. ` + `injection being mutated: "${key}"`, vm ) }) } else { //Define responsiveness properties on objects defineReactive(vm, key, result[key]) } }) //Open responsive binding toggleObserving(true) } } //Traverse the key of the inject. If there is a key with the same name as the from attribute of the inject in the provide, give the data to the result. If not, check whether there is a default value, give the default value to the result, and finally return export function resolveInject (inject: any, vm: Component): ?Object { if (inject) { // Create an empty object const result = Object.create(null) //If hasymbol is supported, use Reflect; otherwise, use Object const keys = hasSymbol ? Reflect.ownKeys(inject) : Object.keys(inject) //Traverse the inject property for (let i = 0; i < keys.length; i++) { // Gets the value of each property const key = keys[i] if (key === '__ob__') continue //Is ky of provide equal to from of inject const provideKey = inject[key].from let source = vm while (source) { if (source._provided && hasOwn(source._provided, provideKey)) { result[key] = source._provided[provideKey] break } source = source.$parent } if (!source) { if ('default' in inject[key]) { const provideDefault = inject[key].default result[key] = typeof provideDefault === 'function' ? provideDefault.call(vm) : provideDefault } else if (process.env.NODE_ENV !== 'production') { warn(`Injection "${key}" not found`, vm) } } } return result } }
First traverse each item, and then traverse whether the parent of each item provides the dependency of the item. If yes, return to the result, and if not, continue to find; After the result is obtained, toggleObserving will be called to close the responsive binding attribute. The specific implementation is in defineReactive. Whether to set the bound attribute as responsive data is determined by passing parameters true and false; Here, we deliberately close the responsive binding when binding the attribute on the inject.
export function initProvide (vm: Component) { const provide = vm.$options.provide if (provide) { vm._provided = typeof provide === 'function' ? provide.call(vm) : provide } }
Take out the provide from the options of the vm instance. If the provide is a function, bind this to the vm_ Provided private property. If it is not function, it is directly assigned to vm_ Provided private attribute; In this way, the child component can access the dependencies provided by the parent component.
Here, I want to talk about the two API s of inject and provide. They are used together. Provide provides dependency binding to vm instances in the parent component_ provided attribute, so that the dependencies of these bindings can be accessed globally. Inject obtains the corresponding dependencies on its parent chain through input parameters.
There is a problem here: when initializing an inject, you will go to the parent to find provide, but the initialization of provide is behind the inject. Is there a problem?
The answer is no question; These two API s are mainly used to handle the value transfer between parent and child components. During initialization, the parent component will be initialized first, and then the child component will be initialized. Therefore, the child component can get the provision in the parent component at this time; (parent beforecreate - > parent created - > parent beforemount - > child beforecreate - > child created - > child beforemount - > child mounted - > parent mounted)