Interpretation of Vue source code -- Vue initialization process

Posted by JeanieTallis on Tue, 22 Feb 2022 01:43:36 +0100

When learning becomes a habit, knowledge becomes common sense. Thank you for your likes, collections and comments.

The new video and articles will be sent to WeChat official account for the first time. Li Yongning

The article has been included in github warehouse liyongning/blog , welcome to Watch and Star.

target

Deeply understand the initialization process of Vue, and no longer afraid of the interviewer's interview question: what happened to new Vue(options)?

Find the entrance

If you want to know what new Vue(options) does, you must first find out where Vue's constructor is declared. There are two ways:

  • Find the compiled entry from the rollup configuration file, and then find the Vue constructor step by step. This method is laborious
  • By writing sample code and then breaking points, it is simple and in place in one step

Let's adopt the second method, write examples, break points and reach the goal in one step.

  • Add a sample file in the / examples Directory - test HTML, add the following contents to the file:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Vue Source code interpretation</title>
</head>
<body>
  <div id="app">
    {{ msg }}
  </div>
  <script src="../dist/vue.js"></script>
  <script>
    debugger
    new Vue({
      el: '#app',
      data: {
        msg: 'hello vue'
      }
    })
  </script>
</body>
</html>
  • Open the console in the browser, and then open test HTML, you will enter breakpoint debugging, and then find the file where the Vue constructor is located

‚Äč Click to view the dynamic diagram of the demonstration , dynamic diagram address: https://p1-juejin.byteimg.com...

Get the Vue constructor in / SRC / core / instance / index JS file, then officially start reading the source code and read it with the target.

In the process of reading, if you encounter something you can't understand, you can write sample code, and then use the debugging function of the browser to debug step by step to cooperate with understanding. If you still can't understand, make a note and continue to look back. Maybe you see other places and suddenly understand what this place is doing, or look back and you will understand the source code, Be sure to read more. If you want to master it, it's not enough to read it again and again

Interpretation of source code -- Vue initialization process

Vue

/src/core/instance/index.js

import { initMixin } from './init'

// Vue constructor
function Vue (options) {
  // Call Vue prototype._ Init method, which is defined in initMixin
  this._init(options)
}

// Define Vue prototype._ Init method
initMixin(Vue)

export default Vue

Vue.prototype._init

/src/core/instance/init.js

/**
 * Define Vue prototype._ Init method 
 * @param {*} Vue Vue Constructor
 */
export function initMixin (Vue: Class<Component>) {
  // Responsible for Vue initialization process
  Vue.prototype._init = function (options?: Object) {
    // vue instance
    const vm: Component = this
    // Each vue instance has one_ uid, and it increases in turn
    vm._uid = uid++

    // a flag to avoid this being observed
    vm._isVue = true
    // Processing component configuration items
    if (options && options._isComponent) {
      /**
       * When initializing each sub component, we only do some performance optimization here
       * Put some deep-seated attributes on the component configuration object into VM$ Options option to improve the efficiency of code execution
       */
      initInternalComponent(vm, options)
    } else {
      /**
       * When initializing the root component, go here to merge the global configuration of Vue into the local configuration of the root component, such as Vue The global components registered by component are merged into the components option of the root instance
       * As for the option combination of each sub component, it occurs in two places:
       *   1,Vue.component The global components registered by the method are merged when registering
       *   2,{ components: { xx } } When executing the render function generated by the compiler, the local components registered in the mode are combined with options, including the configuration of components in the root component
       */
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      // Set the proxy to proxy the properties on the vm instance to the vm_ renderProxy
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    // Initialize component instance relationship attributes, such as $parent, $children, $root, $refs, etc
    initLifecycle(vm)
    /**
     * We need to pay attention to this point when initializing custom events, so the listener of events registered on < comp @ Click = "handleclick" / > is not the parent component,
     * It is the sub component itself, that is, the event dispatcher and listener are the sub component itself, which has nothing to do with the parent component
     */
    initEvents(vm)
    // Parse the slot information of the component to get the VM$ Slot, process the rendering function to get VM$ CreateElement method, i.e. h function
    initRender(vm)
    // Call the beforeCreate hook function
    callHook(vm, 'beforeCreate')
    // Initialize the inject configuration item of the component to get the configuration object in the form of result[key] = val, then process the result data in a responsive manner and proxy each key to the vm instance
    initInjections(vm) // resolve injections before data/props
    // The focus of data response is to deal with props, methods, data, calculated and watch
    initState(vm)
    // Resolve the provide object on the component configuration item and mount it to VM_ On the provided property
    initProvide(vm) // resolve provide after data/props
    // Call the created hook function
    callHook(vm, 'created')

    // If the El option is found on the configuration item, the $mount method will be called automatically, that is, with the El option, you don't need to call $mount manually. On the contrary, if there is no el, you must call $mount manually
    if (vm.$options.el) {
      // Call the $mount method to enter the mount phase
      vm.$mount(vm.$options.el)
    }
  }
}

resolveConstructorOptions

/src/core/instance/init.js

/**
 * Resolve the configuration object options from the component constructor and merge the base class options
 * @param {*} Ctor 
 * @returns 
 */
export function resolveConstructorOptions (Ctor: Class<Component>) {
  // Configuration item
  let options = Ctor.options
  if (Ctor.super) {
    // If there is a base class, recursively resolve the options of the base class constructor
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    if (superOptions !== cachedSuperOptions) {
      // Description the base class constructor option has changed and needs to be reset
      Ctor.superOptions = superOptions
      // Check ctor Are there any later modified / additional options on options (4976)
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // If there are modified or added options, merge the two options
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      // Select merge and assign the merge result to ctor options
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}

resolveModifiedOptions

/src/core/instance/init.js

/**
 * Resolve the options that are subsequently modified or added in the constructor options
 */
function resolveModifiedOptions (Ctor: Class<Component>): ?Object {
  let modified
  // Constructor options
  const latest = Ctor.options
  // Sealed constructor option, backup
  const sealed = Ctor.sealedOptions
  // Compare the two options and record the inconsistent options
  for (const key in latest) {
    if (latest[key] !== sealed[key]) {
      if (!modified) modified = {}
      modified[key] = latest[key]
    }
  }
  return modified
}

mergeOptions

/src/core/util/options.js

/**
 * Merge two options. When the same configuration item appears, the child option will overwrite the configuration of the parent option
 */
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }

  // Standardize props, inject and directive options to facilitate the processing of subsequent programs
  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  // Process the extensions and mixins on the original child object, execute mergeOptions respectively, and merge these inherited options into the parent
  // The objects processed by mergeOptions will contain_ base attribute
  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
  // Traverse parent options
  for (key in parent) {
    mergeField(key)
  }

  // Traverse the sub options. If the parent option does not have the configuration, merge it. Otherwise, skip it. Because the parent and child have the same attribute, it has been processed when processing the parent option above, and the value of the sub option is used
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }

  // For merge options, childVal takes precedence over parentVal
  function mergeField (key) {
    // strats = Object.create(null)
    const strat = strats[key] || defaultStrat
    // If childVal exists, childVal is preferred; otherwise, parentVal is used
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

initInjections

/src/core/instance/inject.js

/**
 * Initialize inject configuration item
 *   1,Get result[key] = val
 *   2,Process the result data in a responsive manner, and proxy each key to the vm instance
 */
export function initInjections (vm: Component) {
  // Parse the inject configuration item, then find the Val corresponding to each key in the configuration item from the configuration of the ancestral component, and finally get the result of result[key] = val
  const result = resolveInject(vm.$options.inject, vm)
  // The result is processed in a data responsive manner, which can also be used to proxy each key in the inject configuration to the vm instance.
  // It is not recommended to change these data in the sub component, because once the provide r injected in the ancestral component changes, the changes you make in the component will be overwritten
  if (result) {
    toggleObserving(false)
    Object.keys(result).forEach(key => {
      /* istanbul ignore else */
      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 {
        defineReactive(vm, key, result[key])
      }
    })
    toggleObserving(true)
  }
}

resolveInject

/src/core/instance/inject.js

/**
 * Resolve the inject configuration item and find the value corresponding to the key from the provide configuration of the ancestral component. Otherwise, use the default value, and finally get the result[key] = val
 * inject The object must be the following structure, because the component configuration object is standardized when merging options
 * @param {*} inject = {
 *  key: {
 *    from: provideKey,
 *    default: xx
 *  }
 * }
 */
export function resolveInject (inject: any, vm: Component): ?Object {
  if (inject) {
    // inject is :any because flow is not smart enough to figure out cached
    const result = Object.create(null)
    // All key s of inject configuration item
    const keys = hasSymbol
      ? Reflect.ownKeys(inject)
      : Object.keys(inject)

    // Traversal key
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      // Skip__ ob__  object
      // #6574 in case the inject object is observed...
      if (key === '__ob__') continue
      // Get the corresponding key in the provide
      const provideKey = inject[key].from
      let source = vm
      // Traverse all ancestral components until the root component, find the value of the corresponding key in provide, and finally get result[key] = provide[provideKey]
      while (source) {
        if (source._provided && hasOwn(source._provided, provideKey)) {
          result[key] = source._provided[provideKey]
          break
        }
        source = source.$parent
      }
      // If the previous loop is not found, use inject [key] Default. If the default value is not set, an error will be thrown
      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
  }
}

initProvide

/src/core/instance/inject.js

/**
 * Resolve the provide object on the component configuration item and mount it to VM_ On the provided property 
 */
export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}

summary

What does Vue's initialization process (new Vue(options)) do?

  • Processing component configuration items

    • When initializing the root component, the options are merged to merge the global configuration into the local configuration of the root component
    • When initializing each sub component, some performance optimizations are made, and some deep-seated attributes on the component configuration object are put into VM$ Options option to improve the efficiency of code execution
  • Initialize the Relationship Attribute of the component instance, such as & dollar; parent,&dollar; children,&dollar; root,&dollar; Refs et al
  • Handling custom events
  • Call the beforeCreate hook function
  • Initialize the inject configuration item of the component to obtain the configuration object in the form of ret[key] = val, then perform shallow response processing on the configuration object (only the first layer data of the object is processed), and proxy each key to the vm instance
  • Data response, processing props, methods, data, computed, watch and other options
  • Resolve the provide object on the component configuration item and mount it to VM_ On the provided property
  • Call the created hook function
  • If the el option is found on the configuration item, call & dollar; Mount method, that is, with the el option, you don't need to call & dollar; Mount method, on the contrary, if the el option is not provided, you must call & dollar; mount
  • Then enter the mount phase

link

Thank you for your likes, collections and comments. I'll see you next time.

When learning becomes a habit, knowledge becomes common sense. Thank you for your likes, collections and comments.

The new video and articles will be sent to WeChat official account for the first time. Li Yongning

The article has been included in github warehouse liyongning/blog , welcome to Watch and Star.

Topics: Javascript node.js Front-end ECMAScript Vue.js