preface

The core idea of Vue is componentization. When we develop a page, we take a bunch of components and build them, like a stack of wood. So today we will analyze how Vue components are created and how they work inside.

case

import Vue from 'vue'
import App from './App.vue'

var app = new Vue({
  el: '#app'Render: h => h(App)})Copy the code

H is the createElement method, and it receives a component App. OK, let’s analyze each one.

Antecedents to review

We are in the source code series one (juejin.cn/post/684490…) / SRC /core/vdom/create-element.js/SRC /core/vdom/create-element.js/SRC /core/vdom/create-element.js/SRC /core/vdom/create-element.js

if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    // ...
    if((! data || ! data.pre) && isDef(Ctor = resolveAsset(context.$options.'components', tag)) {/* If it is a registered component name, create a component class VNode */ VNode = createComponent(Ctor, data, context, children, tag)}else{/* If it is a normal string, create a normal VNode */ VNode = new VNode(tag, data, children, undefined, context)}}else {
    vnode = createComponent(tag, data, context, children)
  }
Copy the code

The createElement method takes a tag argument and internally determines the type of tag to create a normal VNode or a component class VNode. Let’s look at the createComponent method implementation:

The body of the

createComponent

Source: the SRC/core/vdom/create – component. Js

export functioncreateComponent ( Ctor: Class<Component> | Function | Object | void, data: ? VNodeData, context: Component, children: ? Array<VNode>, tag? : : string) VNode | Array < VNode > | void {/ * Vue, at the time of Vue initialize such a logic Vue. Options. The _base = Vue * / const baseCtor = context.$options._base /* Constructs the subclass constructor */if(isObject (Ctor)) {Ctor = baseCtor. The extend (Ctor)} data = data | | {} / * * / installed components hook installComponentHooks (data) const name = Ctor.options.name || tag const vnode = new VNode( `vue-component-${Ctor.cid}${name ? `-${name}` : ' '}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  return vnode
}
Copy the code

There are only 3 steps in the core process, which we will analyze separately:

Construct the subclass constructor

To start with an example, we usually create a component by creating an object:

import HelloWorld from './components/HelloWorld'

export default {
  name: 'app',
  components: {
    HelloWorld
  }
}
Copy the code

Because the component is an object, it walks

Ctor = baseCtor.extend(Ctor)
Copy the code

Extend () source: SRC /core/global-api/extend.js

  Vue.cid = 0
  letCid = 1 /* Construct a Vue subclass */ Vue. Extend =function(extendOptions: Object): The Function {extendOptions = extendOptions | | {} / * Vue assigned to Super * / const Super = this const SuperId = Super. Cid const CachedCtors = extendOptions. _Ctor | | (extendOptions _Ctor = {}) / * cache exists the subclasses, direct return * /if (cachedCtors[SuperId]) {
      returnCachedCtors component name [SuperId]} / * * / const name = extendOptions. Name | | Super. Options. The name const Sub =functionVueComponent (options) {/* Take Vue instance initialization logic */ this._init(options)} /* Inherit Vue */ sub.prototype = Object.create(Super.prototype) Sub.prototype.constructor = Sub Sub.cid = cid++ Sub.options = mergeOptions( Super.options, extendOptions ) Sub['super'] = Super /* Initialize props and computed properties */if (Sub.options.props) {
      initProps(Sub)
    }
    if (Sub.options.computed) {
      initComputed(Sub)
    }
    if(name) {Sub.options.components[name] = Sub} sub. superOptions = super. options // Parent options sub.extendOptions = ExtendOptions // component object /* cachedCtors[SuperId] = Sub */ cachedCtors[SuperId] = Subreturn Sub
  }
Copy the code

Vue.extend builds a subclass of Vue. It converts a pure object into a constructor Sub that extends from Vue and returns it, then extends some properties on the Sub object itself, Such as extending options, adding global API, etc. Initialize props and computed in the configuration. Finally, the Sub constructor is cached to avoid repeating the construction of the same subcomponent when vue.extend is executed multiple times.

Install component hook functions

installComponentHooks(data)
Copy the code

installComponentHooks:

functioninstallComponentHooks (data: VNodeData) {const hooks = data. The hook | | (data. Hook = {}) / * hooksToMerge is Object. The keys (componentVNodeHooks) * /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)) { hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge } } }Copy the code

Data. hook = data.hook; data.hook = data.hook; ComponentVNodeHooks () {componentVNodeHooks ();

const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    // ...
  },

  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    // ...
  },

  insert (vnode: MountedComponentVNode) {
    // ...
  },

  destroy (vnode: MountedComponentVNode) {
    // ...
  }
}
Copy the code

For installComponentHooks, incorporate the hooks of componentVNodeHooks into data.hook.

3. Instantiate VNode

  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ' '}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
  // ...
  return vnode
Copy the code

Instantiate VNode. If you don’t know what VNode is, you can see series 1 (juejin.cn/post/684490…). Note ⚠️ : The component Vnode is different from the normal element Vnode. The component Vnode does not have children;

Summary: We analyzed the createComponent implementation and learned that it renders a component with three key logic: construct subclass constructors, install component hook functions, and instantiate vNodes. CreateComponent returns the component VNode, which also goes to the vm._update method, which we covered in Series 1, and executes the patch function, which we’ll analyze next

patch

When we create the component VNode using createComponent, we go to vm._update and execute vm.__patch__ to convert the VNode into a real DOM node. Having examined this in Series 1, which was for a normal VNode, let’s take a look at how component VNodes differ.

CreateElm: SRC /core/vdom/patch.js

functionCreateElm (vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {/* Check whether it is a component */if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }
Copy the code

Take a look at createComponent’s implementation:

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      if (isDef(i = i.hook) && isDef(i = i.init)) {
        i(vnode, false /* hydrating */)
      }
      if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue)
        insert(parentElm, vnode.elm, refElm)
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true}}}Copy the code

As we mentioned earlier, vNode. data has a bunch of hook functions mixed in, and createComponent finally returns a Boolean that returns true when it is a component. We analyze it carefully

let i = vnode.data
if (isDef(i)) {
  if (isDef(i = i.hook) && isDef(i = i.init)) {
    i(vnode, false/* hydrating */) // ... }}Copy the code

Init: SRC /core/vdom/create-component.js; SRC /core/vdom/create-component.js

const componentVNodeHooks = { init (vnode: VNodeWithData, hydrating: boolean): ? boolean { const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance ) child.$mount(hydrating ? vnode.elm : undefined, hydrating)
  }
  //...
}
Copy the code

Init is through createComponentInstanceForVnode create a Vue instance, and then call $mount subcomponents mount method

export functionCreateComponentInstanceForVnode (vnode: any, / / Component vnode parent: any,) : Component {const options: InternalComponentOptions = { _isComponent:true,
    _parentVnode: vnode,
    parent
  }
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  return new vnode.componentOptions.Ctor(options)
}
Copy the code

Inside the options to build an internal parameters, _isComponent to true means it is a component, the parent said the active component instance The execution of new vnode.com ponentOptions. Ctor (options). Here vnode.com ponentOptions. Ctor corresponding is the child component constructor, we analyzed the above it is inherited in a constructor of Vue Sub, equivalent to a new Sub (options) so here is the timing of the child component instantiation, Then executes _init instantiation of the method, a part of the review series: source: SRC/core/instance/init. Js

Vue.prototype._init = function(options? : Object) { const vm: Component = thisif (options && options._isComponent) {
    initInternalComponent(vm, options)
  } else {
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  }
  // ...
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  } 
}
Copy the code

As you can see, the options integration process is different for components and regular elements. Look at initInternalComponent

/* Integrate options */ when rendering componentsexport function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  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 this process, we can mainly remember the following points: Opts.parent = opts.parent, opts._parentvNode = parentVnode, We by createComponentInstanceForVnode function before they are put into the merger of several parameters of internal options in the $options look at the _init function the last execution of code:

if (vm.$options.el) {
   vm.$mount(vm.$options.el)
}
Copy the code

Here is the mountComponent, which calls the mountComponent method and executes the vm._render() method:

Vue.prototype._render = function (): VNode {
  const vm: Component = this
  const { render, _parentVnode } = vm.$options/* The parent VNode of the current component */ VM.$vnode = _parentVnode
  letVnode try {/* Virtual node */ vnode = render. Call (vm._renderProxy, vm.$createElement} /* Sets the parent of the current component */ vnode.parent = _parentVnodereturn vnode
}
Copy the code

$VNode = VNode; $parentvNode = VNode; $VNode = VNode; $VNode = VNode; They are a father-son relationship. We know that after vm._render is executed to generate vNodes, vm._update is executed to render vNodes, as discussed in Series 1. Take a look at what need to pay attention to in the process of component rendering, vm. _update defined in SRC/core/instance/lifecycle. Js

Vue.prototype._update = function(vnode: VNode, hydrating? : boolean) { const vm: Component = this const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(VM) /* Current component virtual dom */ vm._vnode = vnodeif(! PrevVnode) {/* Initialize render */ VM.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else{/* Update phase */ VM.$el= vm.__patch__(prevVnode, vnode)}$vnodeIs the parent component of vm._vnode */if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el}}Copy the code

Calling vm. __Patch__ generates the real node, which actually calls patch

function patch (oldVnode, vnode, hydrating, removeOnly) {
  // ...
  let isInitialPatch = false
  const insertedVnodeQueue = []

  if (isUndef(oldVnode)) {
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
  }
}
Copy the code

We’re back to where we started. The function that renders to the DOM is createElm; Source: the SRC/core/vdom/patch. Js

/* Create a virtual node as a real node and insert it into its parent */functionCreateElm (vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {/* createComponent creates a child component, the current return value isfalse* /if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }

    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    if(isDef(tag)) {/* Call the platform DOM operation to create a placeholder element */ vnode.elm = vnode.ns? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode)setScope(vnode)
    } else if (isTrue(vnode.isComment)) {
      vnode.elm = nodeOps.createComment(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    } else {
      vnode.elm = nodeOps.createTextNode(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    }
  }
Copy the code

Note that the vnode we pass in here is the component rendered vnode (vm._vnode). If the root node of the component is a normal element, then vm._vnode is also a normal vnode. CreateComponent (vNode, insertedVnodeQueue, parentElm, refElm) returns false. Create a parent node placeholder and recursively call createElm through all the child VNodes. If a child VNode is a component VNode, repeat the procedure from the beginning of this article. In this way, the entire component tree can be completely built in a recursive manner