【vue2.x原理剖析七】组件原理

86 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第7天,点击查看活动详情

前言

源码分析文章看了很多,也阅读了至少两遍源码。终归还是想自己写写,作为自己的一种记录和学习。重点看注释部分和总结,其余不用太关心,通过总结对照源码回看过程和注释收获更大

组件的定义

组件定义有两种方式,一种是局部注册,另一种是全局注册

// 局部注册
var Dialog = {...}
new Vue({
  el: '#app',
  components: {
    'Dialog': Dialog
  }
})
// 全局注册
Vue.component('Dialog': {
  ...
})

全局注册

初始化全局 api

// /src/core/index.js
import { initGlobalAPI } from './global-api/index'
...
initGlobalAPI(Vue)
...

全局组件函数,初始化了三个全局函数,将全局注册的组件放在Vue.options.components对象中,利用Vue.extends进行继承时,会调用_init方法对全局组件和局部组件进行一个合并,也就是当全局组件和局部组件同名且都存在时,优先使用局部组件。

// src/shared/constants.js
export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]
// core/global-api/assets.js
export function initAssetRegisters (Vue: GlobalAPI) {
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        // 定义是一个component并且是一个普通对象
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id
          // 将定义的对象转化成构造器(保证组件之间的隔离)  this.options._base是Vue
          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
      }
    }
  })
}
// src/core/global-api/extend.js
Vue.extend = function (extendOptions: Object): Function {
  ...
  const Sub = function VueComponent (options) {
    this._init(options)
  }
  ...
}
// /src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
  ...
  vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor),options || {},vm)
  ...
}

局部注册

在 new 过程中会将template先转化成 ast,在生成render函数过程中会判断是否是 html 标签,如果不是就会创建组件 vnode

// 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 && context.$vnode.ns) || config.getTagNamespace(tag)
    // 判断是否是html标签 Ctor是组件的内容
    if (config.isReservedTag(tag)) {
      vnode = new VNode(config.parsePlatformTagName(tag), data, children,undefined, undefined, context)
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
    // 创建组件vnode
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      vnode = new VNode(tag, data, children,undefined, undefined, context)
    }
  } else {
    vnode = createComponent(tag, data, context, children)
  }
  ...
}
// 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 {
  const baseCtor = context.$options._base
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }
  ...
  // 如果是局部组件,会调用vue.extend转化成构造函数,并会定义init(创建组件实例并进行挂载)等方法放在data.hook上
  // init方法会执行类似 new Ctor().$mount()操作,所以会调用_init方法将组件options进行合并,组件里没有$el,所以得手动调用$mount方法,之后会返回组件的真实节点并赋值再组件的vm.$el上同时放在(vnode是当前父组件的虚拟节点)vnode.componentInstance上
  // 所以再创建过程中,子组件也会生成自己的watcher
  installComponentHooks(data)

  const name = Ctor.options.name || tag
  // 组件vnode Ctor是组件的构造函数
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
  return vnode
}

渲染

在生成 render 函数后,就会调用 patch 方法进行真实节点的渲染

// src/core/vdom/patch.js
//创建真实节点
function createElm(vnode, insertedVnodeQueue, parentElm, refElm) {
  //如果是组件就创建组件真实节点
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return;
  }
}
// 创建组件的真实节点 如果data是hook属性,就证明他是组件,将组件的真实节点进行插入
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data;
  if (isDef(i)) {
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
    // 先取data上的hook,再取hook上的init方法,此时i是init
    if (isDef((i = i.hook)) && isDef((i = i.init))) {
      // 调用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;
    }
  }
}

总结

组件有全局组件和局部组件两种方式,定义全局组件时,初始化时会利用Vue.extend方法将组件转化成构造函数储存在Vue.options.components对象中。如果定义了局部组件,在初始化时会将全局options和当前实例的options进行合并,也就是说全局组件和局部组件同时存在且同名时,优先使用局部组件。在生成虚拟节点时,如果不是html标签,说明是组件,会对组件进行特殊处理,会定义init方法(创建组件实例并进行挂载)存放在当前虚拟节点的data.hook上,生成组件的虚拟节点。在创建真实节点时,如果data上有hook属性,就证明是组件,此时就会去调用hook里的init方法,init方法会对子组件进行实例化(基于Vue,所以有_init方法),实例化时就会调用_init方法,会将父组件options和当前组件options进行合并,在初始化完数据等后调用$mount方法生成真实节点同时会存放在父组件vnode的componentInstance上,此时代表子组件已经创建完成。之后会将子组件的$el插入到父组件中。由于子组件调用了$mount方法,因此它有了自己的watcher,所以子组件也是响应式。

系列链接

【Vue2.x原理剖析一】响应式原理
【Vue2.x原理剖析二】计算属性原理
【Vue2.x原理剖析三】侦听属性原理
【Vue2.x原理剖析四】模板编译原理
【Vue2.x原理剖析五】初始渲染及更新原理
【Vue2.x原理剖析六】diff算法原理
【Vue2.x原理剖析七】组件原理