Vue2.0 source code reading notes (7): components

Vue2.0 source code reading notes (7): components

Traditional page development advocates separating content, style, and behavior for easy development and maintenance. When MVVM front-end frameworks such as React and Vue become popular, people tend to use html, css, and js to aggregate together to create components, and build large-scale applications by writing small, independent and usually reusable components.
Components are the cornerstone of modern development frameworks. The following describes the implementation principles of Vue components in detail.

1. register components

There are two types of component registration in Vue: local registration and global registration. The global registration is done through the Vue.component method, and the local registration is done by adding the components option when instantiating the component .
The following is a detailed introduction to component registration and related content.

1. Vue.options.components

Vue.options of components property is /src/core/global-api/index.js called file initGlobalAPI function defined.

initGlobalAPI(Vue)

//initGlobalAPI  
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
  Vue.options[type + 's'] = Object.create(null)
})
Vue.options._base = Vue
extend(Vue.options.components, builtInComponents)

//ASSET_TYPES
export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

//builtInComponents
import KeepAlive from './keep-alive'
export default { KeepAlive }
 

In /src/platforms/web/runtime/index.js will file Vue.options.components further assignment.

import platformComponents from './components/index'

extend(Vue.options.components, platformComponents)

//platformComponents
import Transition from './transition'
import TransitionGroup from './transition-group'

export default {
  Transition,
  TransitionGroup
}
 

Finally, Vue.options.components will contain three built-in components:

Vue.options.components = {
  KeepAlive: {/* ... */},
  Transition: {/* ... */},
  TransitionGroup: {/* ... */}
}
 

As mentioned in the article "Options Merging" , the merging of resource options is carried out through the mergeAssets function. The merge strategy is based on the parent option object, so:

//vm  Vue Vue 
vm.$options.components.prototype = Vue.options.components = {
  KeepAlive: {/* ... */},
  Transition: {/* ... */},
  TransitionGroup: {/* ... */}
}
 

2. Vue.extends

Vue.extends is used to create a "subclass" of the Vue constructor based on the configuration options passed in. The simplified code is as follows:

Vue.extend = function (extendOptions) {
  /* ... */
  const Sub = function VueComponent (options) {
    this._init(options)
  }
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub
  Sub.cid = cid++
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
  /* ... */
  ASSET_TYPES.forEach(function (type) {
    Sub[type] = Super[type]
  })
  if (name) {
    Sub.options.components[name] = Sub
  }
  /* ... */
  return Sub
}
 

You can see that the function VueComponent returned by Vue.extend is the same as the Vue constructor, which is initialized by calling the _init method. The VueComponent function itself also adds the same static properties and methods as Vue. The main difference between
VueComponent and Vue is the static property options . VueComponent.options is obtained by merging the Vue.extend parameter and the options parameter of the original constructor through the mergeOptions function.
In addition, the constructor will be added to its own options.components object property, that is to say , the property $options.components.prototype on the object instantiated by VueComponent will have a custom component constructor in addition to the built-in components.

3. Vue.component global registration

Vue's static methods on resources (Vue.component, Vue.directive, Vue.filter) are defined as follows:

initAssetRegisters(Vue)

function initAssetRegisters (Vue) {
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (id,definition ){
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          validateComponentName(id)
        }
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id
          definition = this.options._base.extend(definition)
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
        }
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
}
 

Just look at the definition of the Vue.component method as follows:

Vue.component = function (id, definition){
  if (!definition) {
    return this.options.components[id]
  } else {
      // 
      validateComponentName(id)

      if (isPlainObject(definition)) {
        definition.name = definition.name || id
        definition = Vue.extend(definition)
      }
      Vue.options.components[id] = definition
      return definition
  }
}
 

As can be seen from the above code, the essence of global registration is to generate a Vue sub-constructor function based on the global registration component options, and then add the sub-constructor to the Vue.options.components object .

4. Partial registration of component options

Using the components option to register components will store the component information to be registered in the $options.components object of the current component instance .
When the local component generates the corresponding VNode according to the rendering function, the VNode is finally generated by the createComponent function.

function createComponent (Ctor,data,context,children,tag) {
  /*...*/
  var baseCtor = context.$options._base;
  //Ctor  
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor);
  }
  /*...*/
}
 

It can be seen from the above code that in the process of generating VNode, the extend method of the component instance is called to generate the corresponding sub-constructor according to the registration information.

2. component analysis

The parsing process of components is the same as that of ordinary tags:

1. Generate a rendering function based on the template.
2. Generate a virtual DOM according to the rendering function.
3. Generate a real DOM based on the virtual DOM.

Here is a simple example to illustrate the parsing process of the component:

<body>
  <div id="app"></div>
</body>
<script>
  var ComponentA = {
    template: '<div> A</div>'
  }
  var vue = new Vue({
    el: '#app',
    template: `<div id="app" class="home"><component-a></component-a></div>`,
    components: {
      "component-a": ComponentA
    }
  })
</script>
 

1. Rendering function

The rendering function generated by the components in the template is relatively simple, and is wrapped by the _c() function just like the label. The first parameter of _c() is the component name, the second parameter is the component attribute object, and the third parameter is the content received using <slot>.

function anonymous() {
  with(this){
    return _c(
      'div',
      {staticClass:"home",attrs:{"id":"app"}},
      [_c('component-a')],
      1
    )
  }
}
 

The specific configuration parameter information of the component is stored in vm.$options.components :

vm.$options.components = {
  "component-a" : {
    template: "<div> A</div>"
  }
}
 

2. VNode

The component generates VNode by calling _c() in the rendering function, and _c() will eventually call _createElement to generate VNode. The code for component processing in _createElement is as follows:

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

(1) resolveAsset to obtain component registration information

The resolveAsset processing code for component type resources is as follows:

function resolveAsset (options,type,id,warnMissing) {
  if (typeof id !== 'string') { return }
    var assets = options[type];
    if (hasOwn(assets, id)) { return assets[id] }

    var camelizedId = camelize(id);
    if (hasOwn(assets, camelizedId)) { return assets[camelizedId] }
    
    var PascalCaseId = capitalize(camelizedId);
    if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] }

    var res = assets[id] || assets[camelizedId] || assets[PascalCaseId];
    if (warnMissing && !res) {
      warn('Failed to resolve ' + type.slice(0, -1) + ': ' + id,options);
    }
    return res
  }
 

This function is more interesting for the processing of components. The first is about the component name: the component name used in the template can have three forms when the component is registered . When registering, you can keep the same as when you use it, or you can use the camel case name or the camel case name with the first letter capitalized.
The second is about the local registration and global registration of components: locally registered components will be stored in vm.$options.components , globally registered components will be stored in Vue.options.components , and Vue.options.components will be stored in vm.$ options.components on the prototype chain.
The resolveAsset function to query the component registration information will first check the registered local variables, and if it can't be found, it will query along the prototype chain. This is why local components can only be used by themselves, and globally registered components can be used globally .

(Two) createComponent

The condensed code of the component VNode generation function createComponent is as follows:

function createComponent (Ctor,data,context,children,tag){
  if (isUndef(Ctor)) { return }

  const baseCtor = context.$options._base
  if (isObject(Ctor)) {Ctor = baseCtor.extend(Ctor)}
  /*   */
  data = data || {}
  resolveConstructorOptions(Ctor)
  /*  v-model  */
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)
  /*   */
  const listeners = data.on
  data.on = data.nativeOn
  /*   */
  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
  )
  /*  WEEX  */
  return vnode
}
 

The first is to call the extend method to generate the sub-constructor according to the registration information of the local component , and then call the resolveConstructorOptions function to update the options attribute of the sub-constructor . There will be a question here: In the extend method, the mergeOptions method has been used to complete the merge and update of the sub-constructor options properties. Why do we need to call the resolveConstructorOptions function to process the options?

function resolveConstructorOptions (Ctor) {
  let options = Ctor.options
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    if (superOptions !== cachedSuperOptions) {
      Ctor.superOptions = superOptions
      const modifiedOptions = resolveModifiedOptions(Ctor)
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}
 

This is to prevent the use of global mixins to change the options of the parent constructor after the component constructor is created. The function of the resolveConstructorOptions function is to update the options of the child constructor according to the options value of the object on the prototype chain . Then call the extractPropsFromVNodeData function to extract the value of the partial component props from the current instance, and call installComponentHooks to install the hook function of the component on the data attribute. Finally, use new VNode() to generate the component type VNode. The first parameter passed in is spliced and processed according to the component name; the third parameter is not passed, which means that the component VNode has no children attribute; it is different from generating other types of VNode. The seventh parameter will be passed into the component options object componentOptions ; the eighth parameter will be passed in different values according to whether it is an asynchronous component.


(Three) installComponentHooks

The component hook installation function installComponentHooks and related code are as follows:

const componentVNodeHooks = {
  init (vnode, hydrating) {/*   */},
  prepatch (oldVnode, vnode) {/*   */},
  insert (vnode) {/*   */},
  destroy (vnode) {/*   */}
}

const hooksToMerge = Object.keys(componentVNodeHooks)

function installComponentHooks (data) {
  var hooks = data.hook || (data.hook = {});
  for (var i = 0; i < hooksToMerge.length; i++) {
    var key = hooksToMerge[i];
    var existing = hooks[key];
    var toMerge = componentVNodeHooks[key];
    if (existing !== toMerge && !(existing && existing._merged)) {
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge;
    }
  }
}
function mergeHook (f1, f2) {
  const merged = (a, b) => {
    f1(a, b)
    f2(a, b)
  }
  merged._merged = true
  return merged
}
 

The component hook function generation logic is relatively simple: merge the functions in data.hook and componentVNodeHooks , and the merge strategy is to merge the functions of the same name into the same function.
If there is no hook function in the original data.hook , the final value of data.hook is as follows:

data.hook = componentVNodeHooks = {
  init (vnode, hydrating) {/*   */},
  prepatch (oldVnode, vnode) {/*   */},
  insert (vnode) {/*   */},
  destroy (vnode) {/*   */}
}
 

Finally, there are four hook functions: init, prepatch, insert, and destroy. The specific function of the hook function will be explained in detail later when it is used.

(4) Use new VNode() to generate component VNode

The code for generating component instances in the constructor VNode() is as follows:

export default class VNode {
  constructor (tag,data,children,text,elm,
    context,componentOptions,asyncFactory) {
    this.tag = tag
    this.data = data
    this.context = context
    this.componentOptions = componentOptions
    this.asyncFactory = asyncFactory
    /* ...*/
  }
  get child (){
    return this.componentInstance
  }
}
 

The component VNode in the example finally looks like this:

vnode = {
  tag: 'vue-component-1-component-a',
  data:{
    on: undefined,
    hook:{
      init (vnode, hydrating) {/*   */},
      prepatch (oldVnode, vnode) {/*   */},
      insert (vnode) {/*   */},
      destroy (vnode) {/*   */}
    }
  },
  componentOptions:{
    Ctor: function VueComponent(options){/* */}
    tag: "component-a"
    children: undefined
    listeners: undefined
    propsData: undefined
  },
  asyncFactory: undefined,
  componentInstance: undefined
  /* ...*/
}
 

3. Patch

In the patch process, the component type VNode generates the real DOM by calling the internal function createComponent in the function createPatchFunction .

(1) createComponent

The function createComponent code is as follows:

function createComponent(vnode,insertedVnodeQueue,parentElm,refElm){
  var i = vnode.data
  if (isDef(i)) {
    var isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false)
    }
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}
 

Without considering keepAlive, the process of generating DOM with component type VNode is:

1. Call data.hook.init method to generate the componentInstance property of the component instance , and complete the mounting of the component.
2. Call the initComponent function and use the hook function to complete the component initialization.
3. Call the insert method to insert the generated DOM.

(Two) init hook function

The hook function init function code is as follows:

init (vnode, hydrating) {
  if (
    vnode.componentInstance &&
    !vnode.componentInstance._isDestroyed &&
    vnode.data.keepAlive
  ) {
    const mountedNode = vnode
    componentVNodeHooks.prepatch(mountedNode, mountedNode)
  } else {
    const child = 
    vnode.componentInstance = 
    createComponentInstanceForVnode(vnode,activeInstance)

    child.$mount(hydrating ? vnode.elm : undefined, hydrating)
  }
}
 

The situation of keepAlive will be explained in the subsequent explanation of the built-in components. Under normal circumstances, it will take the else branch, use createComponentInstanceForVnode function to create the component instance properties of VNode. Finally, call the $mount method of the component instance to mount the instance.

function createComponentInstanceForVnode (vnode,parent) {
  var options = {
    _isComponent: true,
    _parentVnode: vnode,
    parent: parent
  };
  var inlineTemplate = vnode.data.inlineTemplate;
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render;
    options.staticRenderFns = inlineTemplate.staticRenderFns;
  }
  return new vnode.componentOptions.Ctor(options)
}
 

The main function of the createComponentInstanceForVnode function is to call the component's constructor to generate a component construction instance.

(Three) initialization component function initComponent

When the component tag exists, the initComponent function is mainly used to call various hook functions in the local variable cbs.create to complete the initialization. cbs.create is in Virtual DOM is detailed in an article. Then use setScope to set the style scope.

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

3. functional components

Functional components are very similar to stateless components in react. Functional components are stateless (no response data) and no instances (no this context). Because it is just a function and no instance, the rendering overhead of functional components is lower than that of ordinary components.
The following first briefly introduces the use of functional components, and then explains its source code implementation.

1. Use of functional components

There are generally two ways to use functional components:

1. When using Vue.component to declare a component, set the functional property to true in the options and implement the render function manually.
2. In the single-file component, use <template functional> instead of <template> to declare the template.

In addition to the first createElement parameter, the render function in the functional component also adds a second parameter context object. Everything the component needs is passed through the context parameter.
The context object contains attributes as follows:

context = {
  props { /*  prop   */ },
  children: [ /*VNode  */ ],
  slots: () => {},/* */
  scopedSlots: { /* */ },
  data: { /* createElement */ },
  parent: { /* */ },
  listeners: { /* data.on  */ },
  injections: { /*  inject  */ },
}
 

The following is an example of a simple functional component, which will be used as an example to illustrate the principle of functional components.

<body>
  <div id="app"></div>
</body>
<script>
  var ComponentA = {
    functional: true,
    render: function(createElement,context) {
      return createElement('div',context.props.name)
    }
  }
  var vue = new Vue({
    el: '#app',
    template: `<div id="app" class="home">
        <component-a name=' A'></component-a>
      </div>`,
    components: {
      "component-a": ComponentA
    }
  })
</script>
 

2. The realization principle of functional components

Still in accordance with the compilation order of the components to explore its implementation principles.

(1) Generate rendering function

The rendering function generated by the template in the above example is shown below. It can be seen that the rendering function generated by the functional component is no different from the ordinary component.

with(this){
  return _c(
    'div',
    {staticClass:"home",attrs:{"id":"app"}},
    [
      _c('component-a',{attrs:{"name":" A"}})
    ],
    1
  )
}
 

(2) Generate VNode

In the process of generating VNode from the rendering function, the function createComponent of generating component VNode will be called , and there is special processing for functional components in this function .

function createComponent(Ctor,data,context,children,tag){
  /*  ... */
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor,propsData,data,context,children)
  }
  /*  ... */
}
 

As can be seen from the above code, the VNode of the functional component is the return value of the createFunctionalComponent function.

function createFunctionalComponent(Ctor,propsData,data,contextVm,children){
  var options = Ctor.options;
  var props = {};
  var propOptions = options.props;
  if (isDef(propOptions)) {
    for (var key in propOptions) {
      props[key] = validateProp(key, propOptions, propsData || emptyObject);
    }
  } else {
    if (isDef(data.attrs)) { mergeProps(props, data.attrs); }
    if (isDef(data.props)) { mergeProps(props, data.props); }
  }

  var renderContext = new FunctionalRenderContext(
    data,
    props,
    children,
    contextVm,
    Ctor
  );

  var vnode = options.render.call(null, renderContext._c, renderContext);

  if (vnode instanceof VNode) {
    return cloneAndMarkFunctionalResult(vnode, data, renderContext.parent, options, renderContext)
  } else if (Array.isArray(vnode)) {
    var vnodes = normalizeChildren(vnode) || [];
    var res = new Array(vnodes.length);
    for (var i = 0; i < vnodes.length; i++) {
      res[i] = cloneAndMarkFunctionalResult(vnodes[i], data, renderContext.parent, options, renderContext);
    }
    return res
  }
}
 

createFunctionalComponent function has four main functions:

1. Combine the values on attrs and props into props.
2. According to the incoming context value, merge and generate the context parameter object renderContext.
3. Generate VNode from the handwritten render function.
4. Clone the VNode and add attributes such as fnContext and fnOptions.

Here you can see the biggest difference between functional components and ordinary components: ordinary components generate component VNode, and the VNode pair has a componentInstance attribute pointing to the component instance. The functional component generates VNode according to the render function, and there is no corresponding component instance. .
The VNode generated according to the functional component is as follows:

VNode = {
  /*  ... */
  tag: "div",
  children: [{/* VNode*/}],
  devtoolsMeta: {renderContext: {/*createElement */}},
  fnContext: {/* */},
  fnOptions: {/* */},
  isCloned: true,
  isRootInsert: true,
  componentInstance: undefined,
  componentOptions: undefined,
  data: undefined
  /*  ... */
}
 

(Three) patch

In the patch phase, because the VNode generated from the functional component does not have the componentOptions property, the process of generating the real DOM based on the VNode is the same as the ordinary component.
In fact, the functional component only generates the VNode corresponding to the package content. When generating the real DOM, the functional component is completely transparent, and the generated DOM is determined by the package content.

4. summary

There are two ways of component registration: local registration and global registration. The essence of component registration is to generate Vue sub-constructors based on the options passed in, and use sub-constructors to generate component instances when using components. The information of the global registered component is on the prototype of the local component registration object, so the globally registered component can be used globally without repeated registration.
The rendering function generated according to the component is the same as the ordinary label except for the slot. The VNode generated by the component rendering function has the component option information property componentOptions. In the process of patching, the component instance is first generated, and then the real DOM is generated and mounted according to the component instance.
Ordinary components will generate corresponding component instance objects, which is relatively expensive. The functional component does not generate a special VNode and instance object. The functional component is equivalent to a container and directly renders the contents of the package when the component is generated.

Welcome to pay attention to the official account : the front-end Taohuayuan , exchange and learn from each other!