Processing components for vue2 source code analysis

Basic usage

Define components globally:

// Define a b component in the form of Vue.component()
Vue.component('b', {<!-- -->
      template: '<p>{<!-- -->{ a }}{<!-- -->{ name }}</p>',
      data () {<!-- -->
        return {<!-- -->
          a: 999
        }
      },
      created(){<!-- -->
        // ('created of child component')
      },
      beforeMount(){<!-- -->
        // ('beforeMount of subcomponent')
      }
})

Locally defined components:

new Vue({<!-- -->
     components: {<!-- -->
        b: {<!-- -->
          template: '<a>{<!-- -->{ name }}</a>',
          data(){<!-- -->
            return {<!-- -->
              name: 'I am a component'
            }
          }
        }
      },
})

Globally defined components are implemented through the component method on the constructor; this method has been analyzed in the global API chapter, and the following is a brief review:

component method

// source code
ASSET_TYPES.forEach(type => {<!-- -->
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {<!-- -->
      // There is no definition, which means acquisition, directly from the corresponding typs + 's' under options
      if (!definition) {<!-- -->
        return this. options[type + 's'][id]
      } else {<!-- -->
        // if it is a component
        if (type === 'component' & amp; & amp; isPlainObject(definition)) {<!-- -->
          // use id without name
          definition.name = definition.name || id
          // Make definition inherit vue
          definition = this.options._base.extend(definition)
        }
        // Settings
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
  
 // In order to facilitate understanding, modify according to the source code
  Vue. component = function (id, definition) {<!-- -->
    definition.name = definition.name || id
    // Create the class of the child component and inherit from the parent component
    definition = this.options._base.extend(definition)
    // record
    Vue.options.components[id] = definition
  }

As can be seen from the above code, the component only does two things. The first is to assign the name or id to the name attribute of the component configuration item, then execute the extend method to create the component, and finally place it in the components under options; extend also It has been analyzed in the global API chapter, and a brief review is also given below;

extend method

 Vue.extend = function (extendOptions: Object): Function {<!-- -->
    // Initialization parameters
    extendOptions = extendOptions || {<!-- -->}
    // Store the parent class
    const Super = this
    // Store the unique identifier of the parent class
    const SuperId = Super.cid
    // Get the cached data in the parameter
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {<!-- -->})
    // If it has been cached, it will be retrieved from the cache and returned
    if (cachedCtors[SuperId]) {<!-- -->
      return cachedCtors[SuperId]
    }
    // Get the name
    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' & amp; & amp; name) {<!-- -->
      validateComponentName(name)
    }
    // Create a subclass
    const Sub = function VueComponent (options) {<!-- -->
      this._init(options)
    }
    // prototypal inheritance
    Sub.prototype = Object.create(Super.prototype)
    // modify the constructor
    Sub.prototype.constructor = Sub
    // The unique identifier of the subclass
    Sub.cid = cid++
    // Merge the parent class and the parameters passed in
    Sub.options = mergeOptions(
      Super. options,
      extendOptions
    )
    // Store the parent class in the child class
    Sub['super'] = Super

    // If there are props in the subclass to initialize
    if (Sub. options. props) {<!-- -->
      initProps(Sub)
    }
    // If there is a computed property in the subclass, initialize it
    if (Sub. options. computed) {<!-- -->
      initComputed(Sub)
    }

    // Put the extend, mixin, and use of the parent class on the subclass
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use

    // Copy all the specified attributes from the parent class to the subclass
    ASSET_TYPES.forEach(function (type) {<!-- -->
      Sub[type] = Super[type]
    })
    if (name) {<!-- -->
      Sub.options.components[name] = Sub
    }
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({<!-- -->}, Sub.options)

    // cache constructor
    // Cache subclass
    cachedCtors[SuperId] = Sub
    return Sub
  }
}

Extend mainly defines a subclass, which inherits the parent class through prototype inheritance; the subclass mainly calls the _init method for initialization; then merges some attributes, and stores the subclass in the cache, in the function Get it from the cache at the beginning;

The above completes the initialization of the component; the following is the process of how to identify, create and mount the components used in the template by compiling the template;

Approximate process

  1. After the template is compiled, when the dom is mounted, the render function will be executed to generate a Vnode, and the render function will call different functions to generate different vnodes according to different types of nodes;
  2. When it is an element node, it will call the Vnode function that creates the element type. This function internally judges whether it is the original label through the label name of the element, and if it is not the original label, it is a component;
  3. If it is a component, the corresponding component data is obtained from the components of the option
  4. After the component data is obtained, it can be judged whether it is a globally added component or a locally added component
  5. Partially added components call the parent’s extend method to implement component inheritance and return the constructor of the child component
  6. Create the Vnode corresponding to the component
  7. When creating a real dom based on Vnode through patch, it will judge whether the current Vnode is a component. If it is a component, execute the constructor of the component, create an instance of the subcomponent, and then call the $mout of the subcomponent method to generate the real dom and mount it on $el;

The corresponding Vnode is generated in the render function. It has been analyzed in the template compilation chapter. The render function uses different functions to wrap and execute according to different node types. Element nodes are wrapped by _c, so see the _c function for details.

// source location ./src/core/instance/render.js

export function initRender (vm: Component) {<!-- -->
 ...
  // _c function to create a virtual node is used internally
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  ...
}

You can see that the createElement function is executed inside the _c function and returns its result;

// Source location ./src/core/vdom/create-element.js
export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {<!-- -->
  ...
  return _createElement(context, tag, data, children, normalizationType)
}

The _createElement function is called inside the createElement function;

// Source location ./src/core/vdom/create-element.js
export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {<!-- -->
  ...
  let vnode, ns
  if (typeof tag === 'string') {<!-- -->
    let Ctor
    ns = (context.$vnode & amp; & amp; context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {<!-- -->
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) & amp; & amp; isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {<!-- -->
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {<!-- -->
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {<!-- -->
    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()
  }
}

If it is an original tag, create the corresponding virtual dom

if (config.isReservedTag(tag)) {<!-- -->
  vnode = new VNode(
    config.parsePlatformTagName(tag), data, children,
    undefined, undefined, context
  )
}

If it is a component, call the createComponent method

else if ((!data || !data.pre) & amp; & amp; isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {<!-- -->
  // component
  vnode = createComponent(Ctor, data, context, children, tag)
}

createComponent function

// Source location ./src/core/vdom/create-component.js

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
  if (isObject(Ctor)) {<!-- -->
    Ctor = baseCtor. extend(Ctor)
  }
  ...
  // install component management hooks onto the placeholder node
  installComponentHooks(data)
  // return a placeholder 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
}

If the ctor does not exist, return it directly, get the Vue constructor through _base, and judge whether the Ctor is an object, if it is an object, it is an internal component, create the class of the Ctor component through extend, and inherit the child Vue;

if (isUndef(Ctor)) {<!-- -->
    return
}
const baseCtor = context. $options._base
if (isObject(Ctor)) {<!-- -->
    Ctor = baseCtor. extend(Ctor)
}

Call the installComponentHooks method, which is used to merge some methods of external and internal components and mount them under the data hook, and use them when creating the corresponding real dom in the future; mainly analyze the init method in the hook

const componentVNodeHooks = {<!-- -->
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {<!-- -->
     const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      child.$mount(hydrating? vnode.elm : undefined, hydrating)
  },
  ...
}
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 & amp; & amp; !(existing & amp; & amp; existing._merged)) {<!-- -->
      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
}

The init method internally calls the createComponentInstanceForVnode method to execute the constructor of the current component, returns the instance of the current component, and assigns it to componentInstance, and finally calls mount to create a real dom; (this method is used to create an instance of the component, so that it can be called Vue’s init initializes some properties and methods of the component itself. After the initialization, execute the mout method to create the real dom of the component and mount it on $el of the component instance)

export function createComponentInstanceForVnode (
  // we know it's MountedComponentVNode but flow doesn't
  vnode: any,
  // activeInstance in lifecycle state
  parent: any
): Component {<!-- -->
  return new vnode.componentOptions.Ctor(options)
}

The class of the new component inside the createComponentInstanceForVnode function returns an instance; the installComponentHooks method is executed at this point; then the virtual dom of the component is created;

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
)

The above is the Vnode of the component created; after the render function is executed, the corresponding Vnode is created; the next step is to mount and create a real dom; the whole process of mounting to creation can be seen in the overall process of the source code analysis chapter, and the specific dom creation You can read the diff algorithm and diff algorithm; the following is directly how to process components in the creation of real dom;

// source location ./src/core/vdom/patch.js

  function createElm (
    vnode,
    insertedVnodeQueue,
    parent Elm,
    refElm,
    nested,
    ownerArray,
    index
  ) {<!-- -->
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {<!-- -->
      return
    }
    ....
  }

The createElm function is used to create a real dom. First, the createComponent function is called to judge and process the Vnode of the component;

 function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {<!-- -->
    let i = vnode.data
    if (isDef(i)) {<!-- -->
      const isReactivated = isDef(vnode.componentInstance) & amp; & amp; i.keepAlive
      if (isDef(i = i.hook) & amp; & amp; 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
      }
    }
}

Get the data attribute of the component, if the data exists, continue to execute;

if (isDef(i = i.hook) & amp; & amp; isDef(i = i.init)) {<!-- -->
    i(vnode, false /* hydrating */)
}

Get the hook in the data, and then get the init in the hook. If init exists, execute the init function and pass the virtual node in. This step is connected with the init function under the componentVNodeHooks analyzed above, and the init function is created internally. An instance of the component is created to initialize the data of the component and create a real dom;

if (isDef(vnode.componentInstance)) {<!-- -->
        initComponent(vnode, insertedVnodeQueue)
        insert(parentElm, vnode.elm, refElm)

If the instance of the component already exists, then initialize the component, and then insert the component directly into the parent element;

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
      insertedVnodeQueue.push(vnode)
    }
  }


function insert (parent, elm, ref) {<!-- -->
    if (isDef(parent)) {<!-- -->
      if (isDef(ref)) {<!-- -->
        if (nodeOps. parentNode(ref) === parent) {<!-- -->
          nodeOps. insertBefore(parent, elm, ref)
        }
      } else {<!-- -->
        nodeOps.appendChild(parent, elm)
      }
    }
  }

If it is keepAlive, it will be processed by the reactivateComponent function

if (isTrue(isReactivated)) {<!-- -->
  reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}

function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {<!-- -->
    let i
    let innerNode = vnode
    while (innerNode. componentInstance) {<!-- -->
      innerNode = innerNode.componentInstance._vnode
      if (isDef(i = innerNode.data) & amp; & amp; isDef(i = i.transition)) {<!-- -->
        for (i = 0; i < cbs.activate.length; + + i) {<!-- -->
          cbs.activate[i](emptyNode, innerNode)
        }
        insertedVnodeQueue.push(innerNode)
        break
      }
    }
    insert(parentElm, vnode.elm, refElm)
}

Asynchronous components

basic use

// The first way callback function
Vue.component('async-webpack-example', function (resolve) {<!-- -->
  // This special `require` syntax will tell webpack
  // Automatically split your build code into multiple packages, these packages
  // will be loaded via an Ajax request
  require(['./my-async-component'], resolve)
})

// The second way promise
Vue. component(
  'async-webpack-example',
  // This `import` function returns a `Promise` object.
  () => import('./my-async-component')
)

// The third way advanced gameplay
const AsyncComponent = () => ({<!-- -->
  // Component to load (should be a `Promise` object)
  component: import('./MyComponent.vue'),
  // Components used when loading asynchronous components
  loading: LoadingComponent,
  // Component to use when loading fails
  error: ErrorComponent,
  // Display the delay time of the component when loading. Default value is 200 (milliseconds)
  delay: 200,
  // If a timeout is provided and component loading also times out,
  // Then use the component used when loading failed. The default value is: `Infinity`
  timeout: 3000
})

During the vnode creation of the component analyzed above, the asynchronous component is also processed inside the createComponent function;

// Source location ./src/core/vdom/create-component.js

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

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {<!-- -->
    Ctor = baseCtor. extend(Ctor)
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeof Ctor !== 'function') {<!-- -->
    return
  }

  // async component
  // asynchronous component
  let asyncFactory
  // If there is no cid to indicate an asynchronous component, because the extend method will not be called
  if (isUndef(Ctor. cid)) {<!-- -->
    asyncFactory = Ctor
    // Handle asynchronous components
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    // if there is no result define and return a placeholder component
    if (Ctor === undefined) {<!-- -->
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }
 ...
}

above code. First judge whether Ctor is an object, an object means it is a non-asynchronous component, because an asynchronous component is a function; judge whether it is an asynchronous component by cid, if cid does not exist, it means an asynchronous component, because it does not execute the extend method, through the resolveAsyncComponent function Process the asynchronous component function, if the returned demerit has no value, then create and return a placeholder component;

// Source location ./src/core/vdom/helpers/resolve-async-component.js
export function resolveAsyncComponent (
  factory: Function,
  baseCtor: Class<Component>
): Class<Component> | void {<!-- -->
    // force render function
    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
        }
      }
    }
    // resolve function
    const resolve = once((res: Object | Class<Component>) => {<!-- -->
      // Inside the ensureCtor function, the constructor of the component is created through the extend method
      factory.resolved = ensureCtor(res, baseCtor)
      // force rendering
      if (!sync) {<!-- -->
        forceRender(true)
      } else {<!-- -->
        owners. length = 0
      }
    })
    // reject function
    const reject = once(reason => {<!-- -->
      if (isDef(factory. errorComp)) {<!-- -->
        factory.error = true
        forceRender(true)
      }
    })
    // Execute the function of the asynchronous component and pass resolve and reject in for execution. Implement the first method of defining an asynchronous component
    const res = factory(resolve, reject)
    // Determine whether the returned result is an object
    if (isObject(res)) {<!-- -->
      // Is it a promise to implement the second method of defining asynchronous components
      if (isPromise(res)) {<!-- -->
        // If factory.resolved does not exist, it means that the resolve method has not been executed
        if (isUndef(factory.resolved)) {<!-- -->
          // pass resolve and reject through then
          res.then(resolve, reject)
        }
        // If there is a component attribute, implement the third method of defining asynchronous components
      } else if (isPromise(res. component)) {<!-- -->
      // Execute resolve and reject through then
        res.component.then(resolve, reject)
        // If there is an error attribute, define the error component
        if (isDef(res. error)) {<!-- -->
          factory.errorComp = ensureCtor(res.error, baseCtor)
        }
        // If there is laoding definition loading component
        if (isDef(res.loading)) {<!-- -->
          factory.loadingComp = ensureCtor(res.loading, baseCtor)
          if (res.delay === 0) {<!-- --> // start loading directly without delay attribute
            factory.loading = true
          } else {<!-- -->
            timerLoading = setTimeout(() => {<!-- -->
              timerLoading = null
              if (isUndef(factory.resolved) & amp; & amp; isUndef(factory.error)) {<!-- -->
                factory.loading = true
                forceRender(false)
              }
            }, res. delay || 200)
          }
        }
        
        if (isDef(res. timeout)) {<!-- -->
          timerTimeout = setTimeout(() => {<!-- -->
            timerTimeout = null
            if (isUndef(factory.resolved)) {<!-- -->
              reject(
                process.env.NODE_ENV !== 'production'
                  ? `timeout (${<!-- -->res.timeout}ms)`
                  : null
              )
            }
          }, res. timeout)
        }
      }
    }

    sync = false
    // If there is a loading attribute, return the loading component, otherwise it is an asynchronous component
    return factory.loading
      ?factory.loadingComp
      : factory.resolved
}

The resolveAsyncComponent method mainly defines the resolve and reject functions inside, and executes the functions of the asynchronous component, and passes these two methods into it. At this time, the first way of using the asynchronous component is realized; then by judging the type of the returned result To achieve the other two methods; if it is a promise, then pass the Resolve and reject methods through then, this step realizes the second method; if the result has a component attribute, pass resolve and reject through the then method of the component, and process loading , error and other attributes and create the corresponding components. This step implements the third type; finally, if there is a loading attribute, return the loading component, otherwise it is the current asynchronous component;

Approximate process:

  • Determine whether it is an asynchronous component by cid, and the asynchronous component creates resolve and reject methods, executes and passes resolve and reject to the asynchronous component function, and finally determines whether there is a loading attribute, and if so, returns the constructor of the loaded component, otherwise returns the current The constructor of the asynchronous component may not execute resolve or reject at this time, so the constructor of the returned component is undefined;
  • If the result returned by the external judgment of the function is undefined, then create an empty virtual node to represent the placeholder component;
  • Generate virtual nodes that load components
  • Patch creates a real node, and the loading component will be rendered on the page at this time
  • If the asynchronous component executes the resolve method at this time, the constructor of the asynchronous component will be created inside the Resolve function, and $forceUpdate will be used to force the update. At this time, the update method of the watcher will be executed to update the view; the asynchronous component will be rendered on the page