DDR爱好者之家 Design By 杰米

组件机制的设计,可以让开发者把一个复杂的应用分割成一个个功能独立组件,降低开发的难度的同时,也提供了极好的复用性和可维护性。本文我们一起从源码的角度,了解一下组件的底层实现原理。

组件注册时做了什么?

在Vue中使用组件,要做的第一步就是注册。Vue提供了全局注册和局部注册两种方式。

全局注册方式如下:

Vue.component('my-component-name', { /* ... */ })

局部注册方式如下:

var ComponentA = { /* ... */ }

new Vue({
 el: '#app',
 components: {
  'component-a': ComponentA
 }
})

全局注册的组件,会在任何Vue实例中使用。局部注册的组件,只能在该组件的注册地,也就是注册该组件的Vue实例中使用,甚至Vue实例的子组件中也不能使用。

有一定Vue使用经验的小伙伴都了解上面的差异,但是为啥会有这样的差异呢?我们从组件注册的代码实现上进行解释。

// Vue.component的核心代码
// ASSET_TYPES = ['component', 'directive', 'filter']
ASSET_TYPES.forEach(type => {
  Vue[type] = function (id, definition
  ){
   if (!definition) {
    return this.options[type + 's'][id]
   } else {
    // 组件注册
    if (type === 'component' && isPlainObject(definition)) {
     definition.name = definition.name || id
     // 如果definition是一个对象,需要调用Vue.extend()转换成函数。Vue.extend会创建一个Vue的子类(组件类),并返回子类的构造函数。
     definition = this.options._base.extend(definition)
    }
    
    // ...省略其他代码
    // 这里很关键,将组件添加到构造函数的选项对象中Vue.options上。
    this.options[type + 's'][id] = definition
    return definition
   }
  }
 })
// Vue的构造函数
function Vue(options){
 if (process.env.NODE_ENV !== 'production' &&
  !(this instanceof Vue)
 ) {
  warn('Vue is a constructor and should be called with the `new` keyword')
 }
 this._init(options)
  
}

// Vue的初始化中进行选项对象的合并
Vue.prototype._init = function (options) {
  const vm = this
  vm._uid = uid++
  vm._isVue = true
  // ...省略其他代码
  if (options && options._isComponent) {
   initInternalComponent(vm, options)
  } else {
   // 合并vue选项对象,合并构造函数的选项对象和实例中的选项对象
   vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
   )
  }
  // ...省略其他代码
 }

以上摘取了组件注册的主要代码。可以看到Vue实例的选项对象由Vue的构造函数选项对象和Vue实例的选项对象两部分组成。

全局注册的组件,实际上通过Vue.component添加到了Vue构造函数的选项对象 Vue.options.components 上了。

Vue 在实例化时(new Vue(options))所指定的选项对象会与构造函数的选项对象合并作为Vue实例最终的选项对象。因此,全局注册的组件在所有的Vue实例中都可以使用,而在Vue实例中局部注册的组件只会影响Vue实例本身。

为啥在HTML模板中可以正常使用组件标签?

我们知道组件可以跟普通的HTML一样在模板中直接使用。例如:

<div id="app">
 <!--使用组件button-counter-->
 <button-counter></button-counter>
</div>
// 全局注册一个名为 button-counter 的组件
Vue.component('button-counter', {
 data: function () {
  return {
   count: 0
  }
 },
 template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})

// 创建Vue实例
new Vue({
  el: '#app'
})

那么,当Vue解析到自定义的组件标签时是如何处理的呢?

Vue 对组件标签的解析与普通HTML标签的解析一样,不会因为是非 HTML标准的标签而特殊处理。处理过程中第一个不同的地方出现在vnode节点创建时。vue 内部通过_createElement函数实现vnode的创建。

export function _createElement (
 context: Component,
 tag"htmlcode">
export function resolveAsset (
 options: Object,
 type: string,
 id: string,
 warnMissing"htmlcode">
// createComponent
export function createComponent (
 Ctor: Class<Component> | Function | Object | void,
 data: "htmlcode">
/**
  * Vue类继承,以Vue的原型为原型创建Vue组件子类。继承实现方式是采用Object.create(),在内部实现中,加入了缓存的机制,避免重复创建子类。
  */
 Vue.extend = function (extendOptions: Object): Function {
  // extendOptions 是组件的选项对象,与vue所接收的一样
  extendOptions = extendOptions || {}
  // Super变量保存对父类Vue的引用
  const Super = this
  // SuperId 保存父类的cid
  const SuperId = Super.cid
  // 缓存构造函数
  const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
  if (cachedCtors[SuperId]) {
   return cachedCtors[SuperId]
  }

  // 获取组件的名字
  const name = extendOptions.name || Super.options.name
  if (process.env.NODE_ENV !== 'production' && name) {
   validateComponentName(name)
  }

  // 定义组件的构造函数
  const Sub = function VueComponent (options) {
   this._init(options)
  }

  // 组件的原型对象指向Vue的选项对象
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub

  // 为组件分配一个cid
  Sub.cid = cid++

  // 将组件的选项对象与Vue的选项合并
  Sub.options = mergeOptions(
   Super.options,
   extendOptions
  )
  // 通过super属性指向父类
  Sub['super'] = Super
  
  // 将组件实例的props和computed属代理到组件原型对象上,避免每个实例创建的时候重复调用Object.defineProperty。
  if (Sub.options.props) {
   initProps(Sub)
  }

  if (Sub.options.computed) {
   initComputed(Sub)
  }

  // 复制父类Vue上的extend/mixin/use等全局方法
  Sub.extend = Super.extend
  Sub.mixin = Super.mixin
  Sub.use = Super.use

  // 复制父类Vue上的component、directive、filter等资源注册方法
  ASSET_TYPES.forEach(function (type) {
   Sub[type] = Super[type]
  })

  // enable recursive self-lookup
  if (name) {
   Sub.options.components[name] = Sub
  }

  // 保存父类Vue的选项对象
  Sub.superOptions = Super.options
  // 保存组件的选项对象
  Sub.extendOptions = extendOptions
  // 保存最终的选项对象
  Sub.sealedOptions = extend({}, Sub.options)

  // 缓存组件的构造函数
  cachedCtors[SuperId] = Sub
  return Sub
 }
}

还有一处重要的代码是installComponentHooks(data)。该方法会给组件vnode的data添加组件钩子,这些钩子在组件的不同阶段被调用,例如init钩子在组件patch时会调用。

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]
  // 内置的组件vnode钩子
  const toMerge = componentVNodeHooks[key]
  // 合并钩子
  if (existing !== toMerge && !(existing && existing._merged)) {
   hooks[key] = existing "htmlcode">
// 创建 vnode
 const vnode = new VNode(
  `vue-component-${Ctor.cid}${name "htmlcode">
 function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
   const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
   // 执行组件钩子中的init钩子,创建组件实例
   if (isDef(i = i.hook) && isDef(i = i.init)) {
    i(vnode, false /* hydrating */)
   }
   
   // init钩子执行后,如果vnode是个子组件,该组件应该创建一个vue子实例,并挂载到DOM元素上。子组件的vnode.elm也设置完成。然后我们只需要返回该DOM元素。
   if (isDef(vnode.componentInstance)) {
    // 设置vnode.elm
    initComponent(vnode, insertedVnodeQueue)
    // 将组件的elm插入到父组件的dom节点上
    insert(parentElm, vnode.elm, refElm)
    if (isTrue(isReactivated)) {
     reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
    }
    return true
   }
  }
 }

createComponent会调用组件vnode的data对象上定义的init钩子方法,创建组件实例。现在我们回过头来看下init钩子的代码:

// ... 省略其他代码
 init (vnode: VNodeWithData, hydrating: boolean): "htmlcode">
export function createComponentInstanceForVnode (
 vnode: any, 
 parent: any,
): Component {
 // 内部组件选项
 const options: InternalComponentOptions = {
  // 标记是否是组件
  _isComponent: true,
  // 父Vnode
  _parentVnode: vnode,
  // 父Vue实例
  parent
 }
 // check inline-template render functions
 const inlineTemplate = vnode.data.inlineTemplate
 if (isDef(inlineTemplate)) {
  options.render = inlineTemplate.render
  options.staticRenderFns = inlineTemplate.staticRenderFns
 }
 // new 一个组件实例。组件实例化 与 new Vue() 执行的过程相同。
 return new vnode.componentOptions.Ctor(options)
}

createComponentInstanceForVnode 中会执行 new vnode.componentOptions.Ctor(options)。由前面我们在创建组件vnode时可知,vnode.componentOptions的值是一个对象:{ Ctor, propsData, listeners, tag, children },其中包含了组件的构造函数Ctor。因此 new vnode.componentOptions.Ctor(options)等价于new VueComponent(options)。

// 生成组件实例
const child = vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance)
// 挂载组件,与vue的$mount一样
child.$mount(hydrating "htmlcode">
new VueComponent(options).$mount(hydrating ? vnode.elm : undefined, hydrating)

这段代码想必大家都很熟悉了,是组件初始化和挂载的过程。组件的初始化和挂载与在前文中所介绍Vue初始化和挂载过程相同,因此不再展开说明。大致的过程就是创建了一个组件实例并挂载后。使用initComponent将组件实例的$el设置为vnode.elm的值。最后,调用insert将组件实例的DOM根节点插入其父节点。然后就完成了组件的处理。

总结

通过对组件底层实现的分析,我们可以知道,每个组件都是一个VueComponent实例,而VueComponent又是继承自Vue。每个组件实例独立维护自己的状态、模板的解析、DOM的创建和更新。篇幅有限,文中只分析了基本的组件的注册解析过程,未对异步组件、keep-alive等做分析。等后面再慢慢补上。

以上就是详解vue 组件的实现原理的详细内容,更多关于vue组件的资料请关注其它相关文章!

DDR爱好者之家 Design By 杰米
广告合作:本站广告合作请联系QQ:858582 申请时备注:广告合作(否则不回)
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!
DDR爱好者之家 Design By 杰米

稳了!魔兽国服回归的3条重磅消息!官宣时间再确认!

昨天有一位朋友在大神群里分享,自己亚服账号被封号之后居然弹出了国服的封号信息对话框。

这里面让他访问的是一个国服的战网网址,com.cn和后面的zh都非常明白地表明这就是国服战网。

而他在复制这个网址并且进行登录之后,确实是网易的网址,也就是我们熟悉的停服之后国服发布的暴雪游戏产品运营到期开放退款的说明。这是一件比较奇怪的事情,因为以前都没有出现这样的情况,现在突然提示跳转到国服战网的网址,是不是说明了简体中文客户端已经开始进行更新了呢?