Writing in the front

The article for reading notes to clone down Vue source code and debugging take ~~

A most basic component example, the following around this example, the source analysis:

Create a component called App
const App = Vue.component('App', {
  name: 'App'.template: `<h1>My App</h1>`.props: {},
  data() {
    return {};
  },
  methods: {},});new Vue({
  el: '#app'.render: (h) = > h(App)
});
Copy the code

createComponent

In the rendering section, we look at the implementation of the _createElement method. There is a section of code that analyzes the tag and generates the vNode object of the tag if an HTML tag is passed in, otherwise we call the createComponent method to create a component vNode:

  // Start creating a vnode
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      if(process.env.NODE_ENV ! = ='production' && isDef(data) && isDef(data.nativeOn)) {
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>. `,
          context
        )
      }
      // tag Creates a vNode instance directly if it is a built-in node (div, span, etc.)
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined.undefined, context
      )
    } else if((! data || ! data.pre) && isDef(Ctor = resolveAsset(context.$options,'components', tag))) {
      // component
      // tag Creates a vNode of component type if it is a registered component name
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children

      // Otherwise create a vnode with an unknown label
      vnode = new VNode(
        tag, data, children,
        undefined.undefined, context
      )
    }
  } else {
    // direct component options / constructor
    // Tag passes a component directly, creating a vNode of the component type
    vnode = createComponent(tag, data, context, children)
  }
Copy the code

CreateComponent is defined in SRC \core\vdom\create-component.js. It does three main things in our example: construct the subclass constructor, install the component function hook, and instantiate vNode. Here’s how it works:

// vnode = createComponent(tag, data, context, children)
// Create a component vNode
export function createComponent (
  Ctor: Class<Component> | Function | Object | void, data: ? VNodeData, context: Component, children: ?Array<VNode>, tag? : string) :VNode | Array<VNode> | void {
  // debugger
  if (isUndef(Ctor)) {
    return
  }

  /* context = current Vue instance vue. option in SRC \core\global-api\index.js defines and assigns the Vue constructor to _base: /* context = current Vue instance vue. option in SRC \core\global-api\index.js and assigns the Vue constructor to _base: Vue. Options. _base = Vue has a merged option operation when initializing _init. ResolveConstructorOptions (vm) constructor), the options | | {}, vm) baseCtor is the constructor * / the current context
  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    // If Ctor is a normal object, the call to vue.extend returns
    // In this example, Vue.component() is used to create the component and returns a component constructor that extends
    Ctor = baseCtor.extend(Ctor)
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeofCtor ! = ='function') {
    if(process.env.NODE_ENV ! = ='production') {
      warn(`Invalid Component definition: The ${String(Ctor)}`, context)
    }
    return
  }

  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  data = data || {}

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor)

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn

  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners & slot

    // work around flow
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // install component management hooks onto the placeholder node
  // Mount init, prepatch, INSERT, destroy hook functions to the component, as described below
  installComponentHooks(data)

  // return a placeholder vnode
  const name = Ctor.options.name || tag
  // Create a vNode for the component
  // Component names are defined by Vue based on component CID and name/tag
  // Store properties such as props, events, tag, and children in the VNode context
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? ` -${name}` : ' '}`,
    data, undefined.undefined.undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  // Weex specific: invoke recycle-list optimized @render function for
  // extracting cell-slot template.
  // https://github.com/Hanks10100/weex-native-directive/tree/master/component
  /* istanbul ignore if */
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode)
  }

  // Finally return the component's vNode
  return vnode
}
Copy the code

Construct the subclass constructor

  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }
Copy the code

When we import Vue from ‘Vue’, we execute an initGlobalAPI method, which is defined in SRC \core\ global-api-index.js. What we do is to mount some static properties and methods to the Vue constructor:

export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () = > config
  if(process.env.NODE_ENV ! = ='production') {
    configDef.set = () = > {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.')}}Object.defineProperty(Vue, 'config', configDef)

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  // 2.6 explicit observable API
  Vue.observable = <T>(obj: T): T= > {
    observe(obj)
    return obj
  }

  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type= > {
    Vue.options[type + 's'] = Object.create(null)})// this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue

  extend(Vue.options.components, builtInComponents)

  initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)  
  initAssetRegisters(Vue)
}
Copy the code

In this, we see the _base definition, which points to the Vue constructor:

Vue.options._base = Vue
Copy the code

In createComponent, if the ctor passed in is an object, baseCtor’s extend method is called, which returns a subclass that extends from the Vue constructor, generally a component constructor. It is defined in the initExtend method above:

/** * Class inheritance * vue. extend static method that returns a component constructor that inherits the Vue instance * defined by initExtend in the initGlobalAPI run */
  Vue.extend = function (extendOptions: Object) :Function {
    extendOptions = extendOptions || {}
    const Super = this
    const SuperId = Super.cid
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      // returns cached if the same component constructor is called multiple times
      return cachedCtors[SuperId]
    }

    const name = extendOptions.name || Super.options.name
    if(process.env.NODE_ENV ! = ='production' && name) {
      // Check whether the component name is valid or uses native tags
      validateComponentName(name)
    }

    // Define a subclass that inherits the Vue constructor
    const Sub = function VueComponent (options) {
      this._init(options)
    }
    // Prototype inheritance
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super

    // For props and computed properties, we define the proxy getters on
    // the Vue instances at extension time, on the extended prototype. This
    // avoids Object.defineProperty calls for each instance created.
    if (Sub.options.props) {
      initProps(Sub)
    }
    if (Sub.options.computed) {
      initComputed(Sub)
    }

    // allow further extension/mixin/plugin usage
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use

    // create asset registers, so extended classes
    // can have their private assets too.
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    // enable recursive self-lookup
    if (name) {
      Sub.options.components[name] = Sub
    }

    // keep a reference to the super options at extension time.
    // later at instantiation we can check if Super's options have
    // been updated.
    // Cache some attributes of the parent class
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // cache constructor
    // Cache constructor
    cachedCtors[SuperId] = Sub
    return Sub
  }
}
Copy the code

Install component hook functions

// install component management hooks onto the placeholder node
installComponentHooks(data)
Copy the code

Data. hook: componentVNodeHooks (); data.hook: componentVNodeHooks (); data.hook: componentVNodeHooks ();

const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ? boolean {if( vnode.componentInstance && ! vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) {// kept-alive components, treat as a patch
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },

  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions
    const child = vnode.componentInstance = oldVnode.componentInstance
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },

  insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if(! componentInstance._isMounted) { componentInstance._isMounted =true
      callHook(componentInstance, 'mounted')}if (vnode.data.keepAlive) {
      if (context._isMounted) {
        // vue-router#1212
        // During updates, a kept-alive component's child components may
        // change, so directly walking the tree here may call activated hooks
        // on incorrect children. Instead we push them into a queue which will
        // be processed after the whole patch process ended.
        queueActivatedComponent(componentInstance)
      } else {
        activateChildComponent(componentInstance, true /* direct */)
      }
    }
  },

  destroy (vnode: MountedComponentVNode) {
    const { componentInstance } = vnode
    if(! componentInstance._isDestroyed) {if(! vnode.data.keepAlive) { componentInstance.$destroy() }else {
        deactivateChildComponent(componentInstance, true /* direct */)}}}}const hooksToMerge = Object.keys(componentVNodeHooks)

function installComponentHooks (data: VNodeData) {
  const hooks = data.hook || (data.hook = {})
  for (let i = 0; i < hooksToMerge.length; i++) {
    const key = hooksToMerge[i]
    const existing = hooks[key]
    const toMerge = componentVNodeHooks[key]
    if(existing ! == toMerge && ! (existing && existing._merged)) {// During the merge process, if the hook already exists in data.hook at some point, the mergeHook function is executed to merge the hook
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
    }
  }
}

function mergeHook (f1: any, f2: any) :Function {
  const merged = (a, b) = > {
    // flow complains about extra args which is why we use any
    f1(a, b)
    f2(a, b)
  }
  merged._merged = true
  return merged
}
Copy the code

Instantiation vNode

  // return a placeholder vnode
  const name = Ctor.options.name || tag
  // Create a vNode for the component
  // Component names are defined by Vue based on component CID and name/tag
  // Store properties such as props, events, tag, and children in the VNode context
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? ` -${name}` : ' '}`,
    data, undefined.undefined.undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
Copy the code

After createComponent returns the component’s VNode, it also goes to the _update method and executes the patch function.


patch

The createElm method in _update is used for patch vNode, which generates a real DOM and inserts it. Here’s an overview of the createElm process:

Create a tag element and call the createChildren loop to createElm to render the DOM:

vnode.elm = vnode.ns
	? nodeOps.createElementNS(vnode.ns, tag)
	: nodeOps.createElement(tag, vnode)
// ...
createChildren(vnode, children, insertedVnodeQueue)
Copy the code

When vNode is a component, the init hook function of the component is called to create and insert the DOM of the component, and terminates:

// createElm
// If createComponent returns true, the component DOM has been patched
// Then abort the DOM logic behind createElm
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
}
Copy the code

Here’s a step-by-step look at the processes that are executed inside createComponent:

1. Call the init hook function, which is equivalent to the entry point of the component flow:

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
      // Is a component vNode
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      if (isDef(i = i.hook) && isDef(i = i.init)) {
        // The createComponent in SRC core vdom create-component.js mounts the component hook function
        // then I becomes the init hook function
        i(vnode, false /* hydrating */)}}// ...
  }
Copy the code

2, the init function call createComponentInstanceForVnode, get a component instance objects and execute component mounting method:

// Component instance
const child = vnode.componentInstance = createComponentInstanceForVnode(
    vnode,
    // activeInstance is assigned in the _update call and is the parent constructor of the current instance
    activeInstance
)
// Call the component's $mount method
$mount(undefined, false) $mount(undefined, false)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
Copy the code

3, _init createComponentInstanceForVnode execution component, _init performed initLifecycle insert current component instance to the parent instance $in children. InitInternalComponent is then executed, incorporating the options of some components. Finally, return an instance of the component:

export function createComponentInstanceForVnode (
  // we know it's MountedComponentVNode but flow doesn't
  vnode: any,
  // activeInstance in lifecycle state
  parent: any
) :Component {
  const options: InternalComponentOptions = {
    _isComponent: true.// Indicates a component
    _parentVnode: vnode,  // Represents the currently active component instance
    parent
  }
  // check inline-template render functions
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  // Call Vue's subclass constructor to return the component instance
  // The component constructor calls this._init(options)
  // The component also goes through the initialization of new Vue(options)
  return new vnode.componentOptions.Ctor(options)
}
Copy the code

$mount(vnode.elm); $mount(vnode.elm); Execute the render method on the template passed in by the user in $mount and call mountComponent. Calling mountComponent is equivalent to entering the _update(_render()) flow of a normal element. Since ELM is undefined, it will not be inserted into the DOM during the patch process, but the generated DOM element will be assigned to the $EL attribute under the placeholder VNode’s componentInstance attribute. :

// start calling $mount
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
Copy the code

After createComponent completes I, execute initComponent () and assign vnode.elm to vnode.elm:

// createComponent
if (isDef(vnode.componentInstance)) {
    initComponent(vnode, insertedVnodeQueue)
    insert(parentElm, vnode.elm, refElm)
    // Execute insert(parentElm, vnode.elm, refElm) to insert the DOM of the component after completing the patch process
    // The init hook recurses child components, so the DOM is inserted in the order of child before parent
    // If a child component is created during the patch process, then the DOM insertion sequence is child before parent.
    if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
    }
    return true
}

// initComponent
vnode.elm = vnode.componentInstance.$el
Copy the code

$el = vnode. Elm; $el = vnode. Elm; $el = vnode. At this point, createComponent has patched the component and returns the main process to createElm. CreateComponent returns True, and the createElm process is complete. Vue is a means of deep traversal:

if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    // If createComponent always returns true, indicating that it is still a component, continue into the component flow
    return
}
Copy the code

7. Finally, delete old nodes in DOM by patch function to complete the whole patch process:

if (isDef(parentElm)) {
    removeVnodes([oldVnode], 0.0)}Copy the code

PS: Record some important traversal and positioning in the whole patch process:

// Defined in initLifecycle, initLifecycle inserts the current instance VM into its parent (if the parent exists)
$parent, $root, $children 

// In mountComponent, the first patch is #app root
vm.$el = el

// createComponent will mount the component hook function and define the component placeholder vnode (tag = vue-component-cid..).

// _render() returns the vNode object
vm._vnode

/ / defined in createComponentInstanceForVnode, said the father vnode vnode and current components
options.parent, _parentVnode

/ * summary: CreateComponent in createElm of patch returns true as long as the passed VNode belongs to the component vNode, terminating createElm (Patch). When the vNode of createElm is a component, _init() is executed from the beginning. After the patch (insert) is complete, the parent patch is executed. * /
Copy the code

Merge configuration

Common instance merge

A basic example of instance configuration where the merge stores all configuration into the $options property of the instance:

let app = new Vue({
  // mixins: [mixin],
  el: '#app'.data: {
    msg: 'Hello'
  },
  created(){}})$options: {} created: [ƒ] data: ƒ mergedInstanceDataFn() directives: {} el: "# APP "filters: ƒ anonymous() : [] _base: ƒ Vue(options)} */
Copy the code

In the invocation of initGlobalAPI, some default configurations are first defined, including components, directives, filters, We then merged some of Vue’s built-in components keep-alive, transition, and transitionGroup into the components configuration, which is why Vue can use these components without registration:

{/* Default options definition */}
Vue.options = Object.create(null)
{/* Add components, directives, filters properties */}
ASSET_TYPES.forEach(type= > {
    Vue.options[type + 's'] = Object.create(null) {})/* Assign the builtInComponents property */ to components}
{/* This is where Vue's built-in components are first incorporated into the default components */}
extend(Vue.options.components, builtInComponents)
Copy the code

The _init method is executed when the instance is created. For this example, it calls mergeOptions to merge the default and incoming configuration:

// Merge new Vue configuration parameters
vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),  / / the default options
    options || {},  // Options passed by the user
    vm
)
Copy the code

In your case, ordinary instance resolveConstructorOptions direct return to the default options:

export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  
  if (Ctor.super) {
      / /...
  }

  // The current Vue root constructor returns the default options
  return options
}
Copy the code

MergeOptions merges the default generated configuration with the incoming configuration and returns an assignment to the $options property. It iterates through the default and incoming configurations, internally sets separate merge policy functions for each configured item, calls them to merge, stores them using options, and returns:

const options = {}
let key
for (key in parent) {
    // console.log('parentKey :>> ', key);
    mergeField(key)
}
for (key in child) {
    if(! hasOwn(parent, key)) {// Child does not exist in parent
        mergeField(key)
    }
}
// console.log('strats :>> ', strats);
function mergeField (key) {
    const strat = strats[key] || defaultStrat
    // Strats defines its own merge strategy function for all options
    // The merge strategy function assigns the combined attributes of parent and child to options[key]
    options[key] = strat(parent[key], child[key], vm, key)
}

return options
Copy the code

Here’s a look at the merging strategy for lifecycle functions, which are eventually merged into an array for sequential execution of lifecycle functions for mixins and other attributes:

/** * Hooks and props are merged as arrays. */
function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function|?Array<Function>
): ?Array<Function> {
  const res = childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
  return res
    ? dedupeHooks(res)
    : res
}

function dedupeHooks (hooks) {
  const res = []
  for (let i = 0; i < hooks.length; i++) {
    if (res.indexOf(hooks[i]) === -1) {
      res.push(hooks[i])
    }
  }
  return res
}

LIFECYCLE_HOOKS.forEach(hook= > {
  strats[hook] = mergeHook
})
Copy the code

withmixinsThe scene of

Next add a mixin to the configuration to see how it merges:

const mixin = Vue.mixin({
  created() {
    console.log('mixin created')}})let app = new Vue({
  mixins: [mixin],
  el: '#app'.data: {
    msg: 'Hello'
  },
  created(){}})$options: {} created: (2) [ƒ, ƒ] data: ƒ mergedInstanceDataFn() directives: {} el: "# APP "filters: ƒ : ƒ anonymous() staticRenderFns: [] _base: ƒ Vue(options)} */
Copy the code

MergeOptions iterates through incoming configurations with mixins, recursively merges the configuration of each mixin into the parent default configuration, and finally merges the other configurations passed in:

if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
    }
}
Copy the code

Note that in initMixin, which is called by the initGlobalAPI, the vue. mixin static method is defined. It will merge the current mixin with the default options of the Vue when it is called, and then merge the combined mixin with the options of the instance when it is instantiated:

export function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin)
    return this}}Copy the code

Component Merge Configuration

Then transform the example into a usage scenario for the component:

const childComponent = {
  template: '<div>{{msg}}</div>'.data() {
    return {
      msg: 'Hello Vue'
    };
  },
  created() {
    console.log('component created');
  },
  mounted() {
    console.log('component mounted'); }};const mixin = Vue.mixin({
  created() {
    console.log('mixin created'); }})let app = new Vue({
  mixins: [mixin],
  el: '#app'.data: {
    msg: 'Hello'
  },
  created() {},
  render: (h) = > h(childComponent)
})
Copy the code

Component link, the render will be called createComponentInstanceForVnode unique options to generate the component and the component constructor. In the initExtend method where the initGlobalAPI generates the child component constructor, merge the options of the parent constructor with the options passed in by the current component:

// Merge the options of the parent constructor with the options passed in by the current component
Sub.options = mergeOptions(
    Super.options,
    extendOptions
)
Copy the code

CreateComponentInstanceForVnode instantiate the component constructor, perform _init method, the combination of configuration is called initInternalComponent:

if (options && options._isComponent) {
    // The component constructor _init() goes here
    initInternalComponent(vm, options)
}

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  // vm.constructive. options gets the previous parent constructor merged with the configuration passed in by the current component
  $options = object.create (sub.options)
  // object.create Creates an Object specifying the prototype, that is, putting vm.constructive. options into the opts.__proto__ property
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  / / _parentVnode in createComponentInstanceForVnode assigned values
  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
  }
}
Copy the code

In the end, the merged components save the result in $options. The child component initialization via initInternalComponent is faster than the external Vue initialization via mergeOptions because there is no recursion, merge strategy, etc. $options for the final component looks like this:

/* vm.$options: {parent: Vue {_uid: 0, _isVue: true, $options: {... }, _renderProxy: Proxy, _self: Vue,... ƒ anonymous() : [] _componentTag: undefined constant Paren_parentvnode: VNode {tag: "vue-component-1", data: {... }, children: undefined, text: undefined, elm: div,... } _renderChildren: undefined __proto__: components: {} created: (2) [ƒ, ƒ] data: ƒ data() directives: {} filters: {} mounted: [ƒ] template: "< div > {{MSG}} < / div >" _Ctor: {0: ƒ} _base: ƒ Vue (options) __proto__ : Object} * /
Copy the code

The life cycle

Different lifecycles are called at each stage of component rendering, which uses a bus callHook function to call lifecycle hooks. It calls the incoming hook internally with error handling:

export function callHook (vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget()
  / / hook function
  const handlers = vm.$options[hook]
  const info = `${hook} hook`
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      // Execute the hook function
      invokeWithErrorHandling(handlers[i], vm, null, vm, info)
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}
Copy the code

beforeCreate & created

These two hooks are called when the _init method is initialized:

// Initialize the lifecycle, render, etc
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
// Initialize props, data, methods, watch, computed, etc
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
Copy the code

Because beforeCreate is called before initState, beforeCreate cannot access the properties of data and methods in the instance. Created does not call $mount when the instance is initialized, so Created does not have access to element attributes such as refs, DOM, etc.

beforeMount & mounted

BeforeMount is called at the start of the mountComponent method, with access to properties such as data, methods, and so on. Mounted is invoked after vm._update(vm._render(), hydrating) has mounted the DOM.

xport function mountComponent (vm: Component, el: ? Element, hydrating? : boolean) :Component {
  // ...

  // Run beforeMount life cycle
  callHook(vm, 'beforeMount')

  let updateComponent
  // ...
  updateComponent = () = > {
      vm._update(vm._render(), hydrating /* false */)}// ...
    
  new Watcher(vm, updateComponent, noop /* Empty function */, {
    before () {
      if(vm._isMounted && ! vm._isDestroyed) {// Components are mounted and not destroyed, execute beforeUpdate life cycle
        callHook(vm, 'beforeUpdate')}}},true) 
  
  // ...
  
  if (vm.$vnode == null) {
    // Render complete
    vm._isMounted = true
    // The mounted life cycle is executed
    callHook(vm, 'mounted')}// ...
}
Copy the code

Vm. $vnode = _parentVnode is defined in the _render method. If vm.$vnode is null, this is not a component mount, but a new Vue() mount. Now look at the mount of the component. The patch method defines an insertedVnodeQueue = [] queue. When a vnode is a component, the component vnodeQueue is pushed to the queue in the initComponent method before the component is inserted into the DOM:

// patch 
const insertedVnodeQueue = []

// initComponent
function initComponent (vnode, insertedVnodeQueue) {
    if (isDef(vnode.data.pendingInsert)) {
        insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
        vnode.data.pendingInsert = null
    }
    vnode.elm = vnode.componentInstance.$el
    if (isPatchable(vnode)) {
        invokeCreateHooks(vnode, insertedVnodeQueue)
        setScope(vnode)
    } else {
        // empty component root.
        // skip all element-related modules except for ref (#3455)
        registerRef(vnode)
        // make sure to invoke the insert hook
        // Component vNode push to queue
        insertedVnodeQueue.push(vnode)
    }
}
Copy the code

$vnode already assigns _parentVnode when _render is executed during component rendering. So the component’s init hook does not execute mounted when it calls child.$mount. At the end of the patch method, after the component has rendered, the invokeInsertHook method is called, which calls the insert hook function of all the components in the queue:

function invokeInsertHook (vnode, queue, initial) {
    // delay insert hooks for component root nodes, invoke them after the
    // element is really inserted
    if (isTrue(initial) && isDef(vnode.parent)) {
        vnode.parent.data.pendingInsert = queue
    } else {
        for (let i = 0; i < queue.length; ++i) {
            // Execute the component's insert hook
            queue[i].data.hook.insert(queue[i])
        }
    }
}
Copy the code

InsertedVnodeQueue insertedVnodeQueue insertedVnodeQueue insertedVnodeQueue insertedVnodeQueue insertedVnodeQueue insertedVnodeQueue insertedVnodeQueue insertedVnodeQueue

insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if(! componentInstance._isMounted) { componentInstance._isMounted =true
        // Mounted invocation of the component
        callHook(componentInstance, 'mounted')}if (vnode.data.keepAlive) {
        if (context._isMounted) {
            // vue-router#1212
            // During updates, a kept-alive component's child components may
            // change, so directly walking the tree here may call activated hooks
            // on incorrect children. Instead we push them into a queue which will
            // be processed after the whole patch process ended.
            queueActivatedComponent(componentInstance)
        } else {
            activateChildComponent(componentInstance, true /* direct */)}}},Copy the code

beforeUpdate & updated

BeforeUpdate is executed in the before function of mountComponent rendering Watcher, which is executed after component Mounted:

// Add a 'render watcher' at mount time
// For the first rendering of the element and subsequent updates
new Watcher(vm, updateComponent, noop /* Empty function */, {
    before () {
        if(vm._isMounted && ! vm._isDestroyed) {// Components are mounted and not destroyed, execute beforeUpdate life cycle
            callHook(vm, 'beforeUpdate')}}},true /* isRenderWatcher */)
Copy the code

Updated…

beforeDestroy & destroyed

The process of component destruction is not yet understood, but the $destroy global method is always called to destroy the component. BeforeDestroy is executed when $destroy is called. Implement destroyed from parent $children, uninstall Watcher, and remove DOM after completing a series of destroyed operations:

  Vue.prototype.$destroy = function () {
    const vm: Component = this
    if (vm._isBeingDestroyed) {
      return
    }
    callHook(vm, 'beforeDestroy')
    vm._isBeingDestroyed = true
    // remove self from parent
    const parent = vm.$parent
    if(parent && ! parent._isBeingDestroyed && ! vm.$options.abstract) {// Delete itself from parent's $children tree
      remove(parent.$children, vm)
    }
    // teardown watchers
    if (vm._watcher) {
      / / uninstall watcher
      vm._watcher.teardown()
    }
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }
    // remove reference from data ob
    // frozen object may not have observer.
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--
    }
    // call the last hook...
    vm._isDestroyed = true
    // invoke destroy hooks on current rendered tree
    // Destruct child components recursively
    vm.__patch__(vm._vnode, null)
    // fire destroyed hook
    callHook(vm, 'destroyed')
    // turn off all instance listeners.
    vm.$off()
    // remove __vue__ reference
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    // release circular reference (#6759)
    if (vm.$vnode) {
      vm.$vnode.parent = null}}}Copy the code

The destroyed component’s children are destroyed recursively during the destruction process, so they are also destroyed first.


The component registration

Global registration

Let’s look at an example of a global registry component:

const MyComp = Vue.component('MyComp', {
  template: ` 
      
{{msg}}
`
.data() { return { msg: 'Hello MyComp'}; }});new Vue({ el: '#app'.render: (h) = > h(MyComp) }); Copy the code

The initAssetRegisters method, which adds the component, directive, and filter global methods to the Vue constructor, registers a global component:

  Vue.component.directive, vue.filter, vue.component.directive, vue.filter
  // These three constant values also become options within the component
  ASSET_TYPES.forEach(type= > {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ) :Function | Object | void {
      if(! definition) {return this.options[type + 's'][id]
      } else {
        /* istanbul ignore if */
        if(process.env.NODE_ENV ! = ='production' && type === 'component') {
          // Check the component name specification
          validateComponentName(id)
        }
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id
          // Call the exntend method on the instance to create the component
          Vue. Extend (definition /* options */)
          definition = this.options._base.extend(definition)
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
        }
        // Save the component to the Components option of options
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
Copy the code

When Vue.component is called, it actually calls this.options._base.extend, equivalent to vue.extend, which returns a component constructor:

definition = this.options._base.extend(definition)
Copy the code

At this point MyComp is a component constructor. A subsequent execution to the component’s initInternalComponent merges the component’s options to the vm.$options prototype:

var opts = vm.$options = Object.create(vm.constructor.options);
Copy the code

Note that the component incorporates the options of the root Vue constructor when initializing the constructor:

Sub.options = mergeOptions(
    Super.options,
    extendOptions
)
Copy the code

After merging, the component’s own components will also have the component’s own constructor to call, and according to the component’s merge strategy, the Vue constructor’s components will be merged as a prototype object under the component’s component.__proto__ :

function mergeAssets (
  parentVal: ?Object,
  childVal: ?Object, vm? : Component, key: string) :Object {
  const res = Object.create(parentVal || null)
  if(childVal) { process.env.NODE_ENV ! = ='production' && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}
Copy the code

The component’s component.__proto__ option now contains all Vue.com components, under which transition and other global components can be used.

Modify the example:

const MyComp = Vue.component('MyComp', {
  template: ` 
      
MyComp
`
.data() { return { msg: 'Hello MyComp'}; }});const App = Vue.component('App', { template: `
`
.data() { return { msg: 'Hello App'}; }});new Vue({ el: '#app'.render: (h) = > h(App) }); Copy the code

When the App component executes _createElement, its tag is MyComp, so this logic follows. Calling resolveAsset returns the component constructor to Ctor when creating a component vNode:

if((! data || ! data.pre) && isDef(Ctor = resolveAsset(context.$options,'components', tag))) {
    // component
    // tag Creates a vNode of component type if it is a registered component name
    vnode = createComponent(Ctor, data, context, children, tag)
}
Copy the code

ResolveAsset looks through a series of conditions and prototype chains (as mentioned earlier, component constructors incorporate parent instance options at initialization, and the options of component constructors are defined on __proto__ in initInternalComponent). Find the required component constructor:

export function resolveAsset (
  options: Object, type: string, id: string, warnMissing? : boolean) :any {
  /* istanbul ignore if */
  if (typeofid ! = ='string') {
    return
  }
  const assets = options[type]
  
  // check local registration variations first
  // Returns the component constructor from components
  if (hasOwn(assets, id)) return assets[id]
  const camelizedId = camelize(id)
  // Hump the component name
  if (hasOwn(assets, camelizedId)) return assets[camelizedId]
  const PascalCaseId = capitalize(camelizedId)
  // Uppercase component name
  if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
  // fallback to prototype chain
  const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
  if(process.env.NODE_ENV ! = ='production'&& warnMissing && ! res) { warn('Failed to resolve ' + type.slice(0, -1) + ':' + id,
      options
    )
  }
  return res
}
Copy the code

Local registration

Example:

const localComp = Vue.extend({
  template: '<h1>Local component</h1>'
});

new Vue({
  el: '#app'.components: {
    localComp
  },
});
Copy the code

Component registration, written directly in options, goes directly to mergeOptions’ component merge strategy. Merge the passed components directly into VM. code.components.ponents so that the component’s constructor can be retrieved from resolveAsset and used as a hook argument to createComponent:

function mergeAssets (
  parentVal: ?Object,
  childVal: ?Object, vm? : Component, key: string) :Object {
  const res = Object.create(parentVal || null)
  if(childVal) { process.env.NODE_ENV ! = ='production' && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}
Copy the code

Differences between the two:

Vue.com Ponent globally created components are added directly to the Options.com ponents option of the Vue constructor. The child components are created by merging Vue options and concatenating the use stereotype chain, so globally registered components can be used within each child component.

Locally created components are only stored in the current $Options.component. other components do not incorporate the current $options, so registered components can only be accessed within the current component.

The most important places to understand component registration are component options merge, component merge policy, and initInternalComponent.


Asynchronous components

Asynchronous components create a component only when render is needed, which is an optimization. As a basic example, AsyncComp creates component instances only with render:

const AsyncComp = Vue.component('AsyncComp'.(resolve, reject) = > {
  // require(MyComp, resolve);
  setTimeout(() = > {
    resolve({
      template: '<h1>My component</h1>'
    });
  }, 1000);
});

new Vue({
  render: (h) = > h(AsyncComp)
}).$mount('#app');
Copy the code

The asynchronous component’s second argument receives a factory function that passes the component options to the resolve function after the asynchronous operation. The vue.componentmethod does not change the factory function to a component constructor, but simply saves it in vue.options:

/ / call Vue.com ponent
if (type === 'component' && isPlainObject(definition)) {
    definition.name = definition.name || id
    // Call the exntend method on the instance to create the component
    Vue. Extend (definition /* options */)
    definition = this.options._base.extend(definition)
}

// Store the factory function into the Components option of options
this.options[type + 's'][id] = definition
return definition
Copy the code

Without render on the component, there are no options, CID, or other methods on the component, just a function body. The extend method is not implemented in the above code. CreateElement >createComponent when the component is rendered, the createComponent logic will be executed:

// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
    // This is an asynchronous component
    // Save the factory function
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
        // return a placeholder node for async component, which is rendered
        // as a comment node but preserves all the raw information for the node.
        // the information will be used for async server-rendering and hydration.
        return createAsyncPlaceholder(
            asyncFactory,
            data,
            context,
            children,
            tag
        )
    }
}
Copy the code

ResolveAsyncComponent resolves asynchronous components and handles factory functions, promises, and advanced asynchronous components.

The factory function

This is the example above. The resolve and reject methods are defined in the factory function flow, wrapped in the once method to ensure that the same component is executed only once:

/** * Ensure a function is called only once. */
export function once (fn: Function) :Function {
  let called = false
  return function () {
    if(! called) { called =true
      fn.apply(this.arguments)}}}const resolve = once((res: Object | Class<Component>) = > {
    // cache resolved
    // ensureCtor is actually a closure that returns the component constructor
    // This calls the component's constructor, vm._init
    The // _init method returns no value, so factory.resolved is a undefined, indicating that the component constructor has been called
    factory.resolved = ensureCtor(res, baseCtor)
    factory.resolved = ensureCtor(res, baseCtor)
    // invoke callbacks only if this is not a synchronous resolve
    // (async resolves are shimmed as synchronous during SSR)
    if(! sync) { forceRender(true)}else {
        owners.length = 0}})const reject = once(reason= >{ process.env.NODE_ENV ! = ='production' && warn(
        `Failed to resolve async component: The ${String(factory)}` +
        (reason ? `\nReason: ${reason}` : ' '))if (isDef(factory.errorComp)) {
        factory.error = true
        forceRender(true)}})Copy the code

The resolve method uses the ensureCtor method to convert the passed component into a constructor. In ensureCtor, the component options are actually called Vue. Extend to generate the extend logic for the component constructor, which is the essence of importing components on demand:

function ensureCtor (comp: any, base) {
  if (
    comp.__esModule ||
    (hasSymbol && comp[Symbol.toStringTag] === 'Module')
  ) {
    comp = comp.default
  }
  return isObject(comp)
    ? base.extend(comp)
    : comp
}
Copy the code

In the asynchronous component flow, Resolve next executes the forceRender method, which executes the $forceUpdate method once for each instance currently rendered. $forceUpdate invokes the render Watcher’s update method on the instance to trigger the component to rerender. The reason for doing this is that Vue is usually data-driven view re-rendering, but no data changes during the entire asynchronous component load, so you can force the component to re-render by executing $forceUpdate:

const forceRender = (renderCompleted: boolean) = > {
    for (let i = 0, l = owners.length; i < l; i++) {
        (owners[i]: any).$forceUpdate()
    }

    if (renderCompleted) {
        owners.length = 0
        if(timerLoading ! = =null) {
            clearTimeout(timerLoading)
            timerLoading = null
        }
        if(timerTimeout ! = =null) {
            clearTimeout(timerTimeout)
            timerTimeout = null
        }
    }
}

Vue.prototype.$forceUpdate = function () {
    const vm: Component = this
    if (vm._watcher) {
        vm._watcher.update()
    }
}
Copy the code

Finally, we call the factory function, which returns an asynchronous component placeholder vnode in createComponent:

const res = factory(resolve, reject)

// createElement
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
    // return a placeholder node for async component, which is rendered
    // as a comment node but preserves all the raw information for the node.
    // the information will be used for async server-rendering and hydration.
    return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
    )
}
Copy the code

Promise asynchronous components

Writing:

Vue.component(
  'AsyncComp'.() = > import('SomeComp'));Copy the code

When the parse PROMISE asynchronous component executes () => import(), res returns a Promise object. Call resolve and reject as res.then parameters to complete the Promise asynchronous component:

const res = factory(resolve, reject)  // Promise

if (isObject(res)) {
    if (isPromise(res)) {
        // () => Promise
        if (isUndef(factory.resolved)) {
            res.then(resolve, reject)
        }
    }
    / /...
}
Copy the code

Advanced asynchronous components (rarely used, see source code for details)

Asynchronous components are different from normal components

Common components in the definition and the implementation of the component’s constructor, complete the integration of components configuration and some initialization work; Asynchronous components do not perform component initialization until render is performed, and extend generates the component’s constructor and initialization. The logical level of code is the difference between when extend is executed.