Skip to content

component

javascript
// 注册全局组件,挂载在 Vue.options.components
Vue.component({ // Vue.options.components
	template: '<div>hello world</div>'
})

// 注册局部组件,挂载在 vm.$options.components
new Vue({
  components: { // vm.$options.components
    'my-component': {
      template: '<div>hello world</div>'
    }
  }
})
javascript
// 对于全局组件、全局指令、全局过滤器来说,优先级都是局部 > 全局,
// vm.$options 中没有才会去 Vue.options 中找,因此对于
// components、directives、filters 使用同一个合并策略 mergeAssets。
// 以 components 属性为例,当合并配置项时发现属性是 components 时触发 mergeAssets
// 合并策略。内部以 Vue.components 对象的原型创建一个空对象 options,这个空对象用来
// 保存局部组件,之后如果遍历 vm.$options.components 每个属性并保存到 options 中,
// 最后返回 options。当使用组件时,先在自身找,没找到才沿着原型链去找全局的。
const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

ASSET_TYPES.forEach(function (type) {
	strats[type + 's'] = mergeAssets
});

function mergeAssets (parentVal, childVal) {
  const options = Object.create(parentVal || null) // 自己没有时就去原型链上找
  
  if (childVal) {
    for (let key in childVal) {
      options[key] = childVal[key] // 局部组件优先
    }
  }
  
  return options
}
javascript
Vue.component = function (id, definition) { 
	definition = this.options._base.extend(definition) // Vue.extend
}
javascript
// 在 render => vnode 时通过 isReservedTag(tag) 检测标签是否属于原生标签来判断是否
// 是组件,如果不是原生标签那么通过 vm.$options.components[tag] 获取Ctor,最后通过
// createComponent 生成组件 vnode。
function createElement (context, tag, data, children) {
	// 不是原生标签生成组件 vnode
  let Ctor = context.$options.components[tag] // 配置项

  return createComponent(Ctor, data || {}, context, children, tag) 
}
javascript
// Ctor 可能是配置项,即一个对象,也可能是继承了 Vue 的子类,即即 VueComonent 
// 构造函数,取决于我们有没有调用 Vue.extend。
// createComponent 内部先判断 Ctor 是不是对象,是对象就手动调用 Vue.extend(Ctor) 
// 生成子类,即 VueComonent 构造函数,之后会在 vnode 的 data 属性中添加一个 hook 
// 对象,对象内部定义一个 init 方法,init 方法会 new Ctor 生成组件实例,再手动调用
// vc.$mount 渲染成 DOM,因为组件没有 $el,_init 时不会自动挂载,最后返回 vnode。
createComponentfunction createComponent () {
    if (isObject(Ctor)) { // 没有使用Vue.extend时则手动调用
      Ctor = context.$options._base.extend(Ctor) // 创建VueComponent, 即Sub
    }
    
    data.hook = { 
      init (vnode) { // 初始化init钩子函数
        // new Sub, 生成vc
        let vc = vnode.componentInstance = new Ctor({ _isComponent: true }) 
        vc.$mount() // 手动挂载, 因为没有$el, _init时不会自动挂载
      }
    }
    
    return VNode(
      context, 
      `vue-component-${tag}`, // tag
      data,
      undefined,
      undefined, 
      undefined, 
      undefined, 
      { Ctor, children} // componentOptions
    )
}
javascript
// 在 patch 时,因为组件没有 $el,所以 oldVnode 未定义,说明是初始化渲染组件
// 调用 createElm 生成 DOM
function patch (oldVnode, vnode) {
  if (isUndef(oldVnode)) { // 组件没有$el
    return createElm(vnode)
  }
}

// 渲染原生标签和渲染组件标签都是走 createElm 方法,createElm 中会先尝试调用
// createComponent(vnode) 尝试渲染组件,如果返回值为真直接 return,否则接下来就执行
// 渲染原生标签的逻辑
function createElm (vnode) {
  let { tag, data, children, text, context } = vnode
  if (typeof tag === 'string') {
    // 尝试找vnode.data.hook.init并调用, 渲染成组件对应的真实DOM并返回true
    // 否则不是组件, 渲染成元素节点
    if (createComponent(vnode)) {
      return vnode.componentInstance.$el
    }
    // 渲染成元素节点
    vnode.elm = document.createElement(tag)
    updateProps({}, vnode) // 初始化时更新样式、属性
    if (children) {
      for (let i = 0, l = children.length; i < l; i++) {
        vnode.elm.appendChild(createElm(children[i]))
      }
    }
  } else {
    // 渲染成文本节点
    vnode.elm = document.createTextNode(text)
  }

  return vnode.elm
}

// createComponent 内部直接看能不能从 vnode 中找到 vnode.data.hook.init,
// 如果找得到直接把init方法调用,最后返回 true。
// 调用vnode.data.hook.init => new Ctor => _init => HTML => render => AST 
// => VNode => DOM
function createComponent(vnode) {
  let i = vnode.data
  if (i) {
    if ((i = i.hook) && (i = i.init)) {
      i(vnode) // vnode.data.hook.init
      return true
    }
  }
}