Vue2.x source code - initialization: initMixin(Vue), initProxy(vm), initInjections(vm), initProvide(vm)

Keywords: Vue

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)

Posted by MrTL on Mon, 25 Oct 2021 00:42:52 -0700