Vue2 source code analysis (componentized)

One of the main ideas of Vue is componentization. In normal development, a complete Vue project is usually composed of one Vue component module after another. Let’s first look at how Vue initializes a component:

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

var app = new Vue({
  el: '#app'.// h is the createElement method
  render: h= > h(App)
})
Copy the code

Here the component calls the Render method, passing the App argument into the createElement function:

export function createElement (context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean) :VNode | Array<VNode> {
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}
Copy the code

The createElement function will eventually call _createElement.

export function _createElement (context: Component, tag? : string | Class<Component> |Function | Object, data? : VNodeData, children? : any, normalizationType? : number) :VNode | Array<VNode> {
  if(isDef(data) && isDef((data: any).__ob__)) { process.env.NODE_ENV ! = ='production' && warn(
      `Avoid using observed data object as vnode data: The ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render! ',
      context
    )
    return createEmptyVNode()
  }
  // Get the component is property to get the corresponding component
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if(! tag) {// Create a comment node in tag if not pure
    return createEmptyVNode()
  }
  // Process component slot slots
  if (Array.isArray(children) &&
    typeof children[0= = ='function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  // The children vNode array is flattened, if it is a template-generated render, the flattening scheme is selected according to the parameter, and the user-built render array is recursively flattened
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children) // Deep recursively flattens the array
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children) // From 2d to 1d
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    // Check whether it is a platform standard tag
    if (config.isReservedTag(tag)) {
      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
        )
      }
      // Create the label vnode
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined.undefined, context
      )
    } else if((! data || ! data.pre) && isDef(Ctor = resolveAsset(context.$options,'components', tag))) {
      // Create component vNode
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // Create vNodes with tag names
      vnode = new VNode(
        tag, data, children,
        undefined.undefined, context
      )
    }
  } else {
    // Create component vNode
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}
Copy the code

Vnode = createComponent(Tag, data, context, children);

export function createComponent (
  Ctor: Class<Component> | Function | Object | void, data: ? VNodeData, context: Component, children: ?Array<VNode>, tag? : string) :VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }

  const baseCtor = context.$options._base
  // Create a Vue subclass constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  ....

  // Handle asynchronous components
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  data = data || {}

  ....
  // Install the component hook function
  installComponentHooks(data)

  const name = Ctor.options.name || tag
  // Create a component Vnode
  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

The createComponent method does three things:

  1. Pass the component objectCtor = baseCtor.extend(Ctor)To build aVueThe subclass constructor of
  2. Pass the component’s hook functioninstallComponentHooks(data)Method will becomponentVNodeHooksMerges the hook function todata.hookIn the.
  3. Create the component Vnode and return the Vnode.

After creating the component Vnode, the vm._update function executes the vm. __Patch__ (prevVnode, Vnode) method to convert the Vnode to the real DOM. This method is defined in SRC /core/vdom/patch.js:

return function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {  // Delete the old node
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }
    let isInitialPatch = false
    const insertedVnodeQueue = []
    if (isUndef(oldVnode)) {  // Create a new node if the old one does not exist
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else{... } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)return vnode.elm
  }
Copy the code

Here, since we are a new component Vnode, we call createElm(Vnode, insertedVnodeQueue) :

function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {...if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return}... }Copy the code

Here because we are component vNodes, createComponent(Vnode, insertedVnodeQueue, parentElm, refElm) returns true:

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      // Execute component Vnode hook function init
      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

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

init (vnode: VNodeWithData, hydrating: boolean): ? boolean {if( vnode.componentInstance && ! vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) {// If the component is keepAlive
    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)
  }
},
Copy the code

If the component is keepAlive is executed componentVNodeHooks. Prepatch (mountedNode mountedNode) method, if not create a Vue createComponentInstanceForVnode instance, Then call the $mount method to mount the child component:

export function createComponentInstanceForVnode (vnode: any, 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

Function to build a component parameters, and then execute the new vnode.com ponentOptions. Ctor (options), the vnode.com ponentOptions. Ctor is inherited the son of the Vue constructor before, The _isComponent argument is true to indicate that it is a component, and parent indicates the current component instance, which is the parent component. So the instantiation of the child component is performed at this point, and then the _init method in the constructor is executed:

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options? :Object) {
    const vm: Component = this
    vm._uid = uid++

    let startTag, endTag
    vm._isVue = true
    if (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

Component instance passes _isComponent true and executes initInternalComponent(VM, options) :

export 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

This function basically merges the parameters we passed into the option $options. The _init function finally executes:

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

ComponentVNodeHooks init ($mount) {child.$mount(hydrating? Elm: undefined, hydrating), so it calls the mountComponent method and finally executes the vm._render() method:

Vue.prototype._render = function () :VNode {
  const vm: Component = this
  const { render, _parentVnode } = vm.$options

  vm.$vnode = _parentVnode
  let vnode
  try {
    vnode = render.call(vm._renderProxy, vm.$createElement)
  } catch (e) {
    // ...
  }
  // set parent
  vnode.parent = _parentVnode
  return vnode
}
Copy the code

The parent of the render Vnode refers to _parentVnode (vm.$Vnode). The parent of the render Vnode refers to _parentVnode (vm.$Vnode). Execute vm._update to render the corresponding vnode:

Vue.prototype._update = function (vnode: VNode, hydrating? : boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    
    if(! prevVnode) { vm.$el = vm.__patch__(vm.$el, vnode, hydrating,false /* removeOnly */)}else {
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
  }

Copy the code

Const restoreActiveInstance = setActiveInstance(VM) holds the Vue instance of the current context:

export let activeInstance: any = null

export function setActiveInstance(vm: Component) {
  const prevActiveInstance = activeInstance
  activeInstance = vm
  return () = > {
    activeInstance = prevActiveInstance
  }
}
Copy the code

PrevActiveInstance holds the parent Vue instance of the current VM instance. ActiveInstance saves the current Vue instance and returns a function to refer the activeInstance back to the parent Vue instance of the current instance after the patch of the child component is completed. Because Vue initialization is a deep traversal process, the current Vue instance needs to be known when instantiating the child component. Make it the parent Vue instance of the child component and the initLifecycle(VM) method is called before the child component $mount is mounted:

export function initLifecycle (vm: Component) {
  const options = vm.$options

  let parent = options.parent
  if(parent && ! options.abstract) {while(parent.$options.abstract && parent.$parent) { parent = parent.$parent } parent.$children.push(vm) } vm.$parent = parent . }Copy the code

Here the current VM is stored in the parent instance’s $children, thus guaranteeing the parent-child relationship between the VM instance and all of its subtrees. $el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);

function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
  // ...
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }

  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag
  if (isDef(tag)) {
    // ...

    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)
    setScope(vnode)

    /* istanbul ignore if */
    if (__WEEX__) {
      // ...
    } else {
      createChildren(vnode, children, insertedVnodeQueue)
      if (isDef(data)) {
        invokeCreateHooks(vnode, insertedVnodeQueue)
      }
      insert(parentElm, vnode.elm, refElm)
    }
    
    // ...
  } 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

The difference here is that the vnode we pass in here is a component render VNode. Because our render vNode root element is a normal element, it is a normal Vnode, not the previous component vNode, so instead of using the previous logic to create a component instance, we use the following normal VNode logic. Check whether vNode contains a tag parameter. If yes, check whether vNode contains a tag parameter. Then call platform DOM operation to create a placeholder element:

vnode.elm = vnode.ns
  ? nodeOps.createElementNS(vnode.ns, tag)
  : nodeOps.createElement(tag, vnode)
setScope(vnode)
Copy the code

Then call the createChildren method to create the child elements, and then use invokeCreateHooks(vnode, InsertedVnodeQueue) triggers all create hook functions and pushes vnodequeue to insertedVnodeQueue:

if (isDef(data)) {
  invokeCreateHooks(vnode, insertedVnodeQueue)
}

function invokeCreateHooks (vnode, insertedVnodeQueue) {
  for (let i = 0; i < cbs.create.length; ++i) {
    cbs.create[i](emptyNode, vnode)
  }
  i = vnode.data.hook
  if (isDef(i)) {
    if (isDef(i.create)) i.create(emptyNode, vnode)
    if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
  }
}
Copy the code

Finally, we call insert(parentElm, vnode.elm, refElm), because parentElm we passed is empty, so we don’t insert here, Instead, execute the initComponent function after component initialization in createComponent to assign the component rendering vNode’s $EL to component VNode’s elm:

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    / /...
    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

Finally, execute insert(parentElm, vnode.elm, refElm) to insert component DOM.

conclusion

When Vue renders the whole project according to the VNode tree, if it is a normal Vnode, it will directly create element inserts, but if it is a component Vnode, it will first initialize the component Vue instance and point its parent to the parent instance, and then patch the component to insert the generated real DOM into the location of the component. This is how the component initializes rendering.