详解vue 组件的实现原理


Posted in Javascript onNovember 12, 2020

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

组件注册时做了什么?

在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?: 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标签
  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))) {
   // 如果是组件标签,e.g. my-custom-tag
   vnode = createComponent(Ctor, data, context, children, tag)
  } else {
   vnode = new VNode(
    tag, data, children,
    undefined, undefined, context
   )
  }
 } else {
  // direct component options / constructor
  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()
 }
}

以文中的button-counter组件为例,由于button-counter标签不是合法的HTML标签,不能直接new VNode()创建vnode。Vue 会通过resolveAsset函数检查该标签是否为自定义组件的标签。

export function resolveAsset (
 options: Object,
 type: string,
 id: string,
 warnMissing?: boolean
): any {
 /* istanbul ignore if */
 if (typeof id !== 'string') {
  return
 }
 const assets = options[type]

 // 首先检查vue实例本身有无该组件
 if (hasOwn(assets, id)) return assets[id]
 const camelizedId = camelize(id)
 if (hasOwn(assets, camelizedId)) return assets[camelizedId]
 const PascalCaseId = capitalize(camelizedId)
 if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]

 // 如果实例上没有找到,去查找原型链
 const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
 if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
  warn(
   'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
   options
  )
 }
 return res
}

button-counter是我们全局注册的组件,显然可以在this.$options.components找到其定义。因此,Vue会执行createComponent函数来生成组件的vnode。

// createComponent
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
 }
 
 // 获取Vue的构造函数
 const baseCtor = context.$options._base

 // 如果Ctor是一个选项对象,需要使用Vue.extend使用选项对象,创建将组件选项对象转换成一个Vue的子类
 if (isObject(Ctor)) {
  Ctor = baseCtor.extend(Ctor)
 }

 // 如果Ctor还不是一个构造函数或者异步组件工厂函数,不再往下执行。
 if (typeof Ctor !== 'function') {
  if (process.env.NODE_ENV !== 'production') {
   warn(`Invalid Component definition: ${String(Ctor)}`, context)
  }
  return
 }

 // 异步组件
 let asyncFactory
 if (isUndef(Ctor.cid)) {
  asyncFactory = Ctor
  Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
  if (Ctor === undefined) {
   // return a placeholder node for async component, which is rendered
   // as a comment node but preserves all the raw information for the node.
   // the information will be used for async server-rendering and hydration.
   return createAsyncPlaceholder(
    asyncFactory,
    data,
    context,
    children,
    tag
   )
  }
 }

 data = data || {}

 // 重新解析构造函数的选项对象,在组件构造函数创建后,Vue可能会使用全局混入造成构造函数选项对象改变。
 resolveConstructorOptions(Ctor)

 // 处理组件的v-model
 if (isDef(data.model)) {
  transformModel(Ctor.options, data)
 }

 // 提取props
 const propsData = extractPropsFromVNodeData(data, Ctor, tag)

 // 函数式组件
 if (isTrue(Ctor.options.functional)) {
  return createFunctionalComponent(Ctor, propsData, data, context, children)
 }

 const listeners = data.on
 data.on = data.nativeOn

 if (isTrue(Ctor.options.abstract)) {
  const slot = data.slot
  data = {}
  if (slot) {
   data.slot = slot
  }
 }

 // 安装组件hooks
 installComponentHooks(data)

 // 创建 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
}

由于Vue允许通过一个选项对象定义组件,Vue需要使用Vue.extend将组件的选项对象转换成一个构造函数。

/**
  * 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 ? mergeHook(toMerge, existing) : toMerge
  }
 }
}

// 组件vnode的钩子。
const componentVNodeHooks = {
 // 实例化组件
 init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
  if (
   vnode.componentInstance &&
   !vnode.componentInstance._isDestroyed &&
   vnode.data.keepAlive
  ) {
   // kept-alive components, treat as a patch
   const mountedNode: any = vnode // work around flow
   componentVNodeHooks.prepatch(mountedNode, mountedNode)
  } else {
   // 生成组件实例
   const child = vnode.componentInstance = createComponentInstanceForVnode(
    vnode,
    activeInstance
   )
   // 挂载组件,与vue的$mount一样
   child.$mount(hydrating ? vnode.elm : undefined, hydrating)
  }
 },

 prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
  const options = vnode.componentOptions
  const child = vnode.componentInstance = oldVnode.componentInstance
  updateChildComponent(
   child,
   options.propsData, // updated props
   options.listeners, // updated listeners
   vnode, // new parent vnode
   options.children // new children
  )
 },

 insert (vnode: MountedComponentVNode) {
  const { context, componentInstance } = vnode
  if (!componentInstance._isMounted) {
   componentInstance._isMounted = true
   // 触发组件的mounted钩子
   callHook(componentInstance, 'mounted')
  }
  if (vnode.data.keepAlive) {
   if (context._isMounted) {
    queueActivatedComponent(componentInstance)
   } else {
    activateChildComponent(componentInstance, true /* direct */)
   }
  }
 },

 destroy (vnode: MountedComponentVNode) {
  const { componentInstance } = vnode
  if (!componentInstance._isDestroyed) {
   if (!vnode.data.keepAlive) {
    componentInstance.$destroy()
   } else {
    deactivateChildComponent(componentInstance, true /* direct */)
   }
  }
 }
}

const hooksToMerge = Object.keys(componentVNodeHooks)

最后,与普通HTML标签一样,为组件生成vnode节点:

// 创建 vnode
 const vnode = new VNode(
  `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
  data, undefined, undefined, undefined, context,
  { Ctor, propsData, listeners, tag, children },
  asyncFactory
 )

组件在patch时对vnode的处理与普通标签有所不同。

Vue 如果发现正在patch的vnode是组件,那么调用createComponent方法。

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): ?boolean {
  if (
   vnode.componentInstance &&
   !vnode.componentInstance._isDestroyed &&
   vnode.data.keepAlive
  ) {
   // kept-alive components, treat as a patch
   const mountedNode: any = vnode // work around flow
   componentVNodeHooks.prepatch(mountedNode, mountedNode)
  } else {
   // 生成组件实例
   const child = vnode.componentInstance = createComponentInstanceForVnode(
    vnode,
    activeInstance
   )
   // 挂载组件,与vue的$mount一样
   child.$mount(hydrating ? vnode.elm : undefined, hydrating)
  }
 }
 // ...省略其他代码

由于组件是初次创建,因此init钩子会调用createComponentInstanceForVnode创建一个组件实例,并赋值给vnode.componentInstance。

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 ? vnode.elm : undefined, hydrating)

等价于:

new VueComponent(options).$mount(hydrating ? vnode.elm : undefined, hydrating)

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

总结

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

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

Javascript 相关文章推荐
jQuery 前的按键判断代码
Mar 19 Javascript
自定义ExtJS控件之下拉树和下拉表格附源码
Oct 15 Javascript
Node.js中使用Buffer编码、解码二进制数据详解
Aug 16 Javascript
JavaScript实现为input与textarea自定义hover,focus效果的方法
Aug 21 Javascript
javascript简单比较日期大小的方法
Jan 05 Javascript
JS实现改变HTML上文字颜色和内容的方法
Dec 30 Javascript
javascript 使用正则test( )第一次是 true,第二次是false
Feb 22 Javascript
JavaScript实现简单精致的图片左右无缝滚动效果
Mar 16 Javascript
webpack dll打包重复问题优化的解决
Oct 10 Javascript
webpack 代码分离优化快速指北
May 18 Javascript
layui自定义插件citySelect实现省市区三级联动选择
Jul 26 Javascript
javascript设计模式 ? 模板方法模式原理与用法实例分析
Apr 23 Javascript
Vant Weapp组件踩坑:picker的初始赋值解决
Nov 12 #Javascript
vue 图片裁剪上传组件的实现
Nov 12 #Javascript
js前端传json后台接收‘‘被转为quot的问题解决
Nov 12 #Javascript
使用Vant完成DatetimePicker 日期的选择器操作
Nov 12 #Javascript
JavaScript 实现拖拽效果组件功能(兼容移动端)
Nov 11 #Javascript
vant 中van-list的用法说明
Nov 11 #Javascript
让Vue响应Map或Set的变化操作
Nov 11 #Javascript
You might like
初品cakephp 入门基础
2012/02/16 PHP
2014年10个最佳的PHP图像操作库
2014/07/14 PHP
php使用MySQL保存session会话的方法
2015/06/26 PHP
Yii框架弹出窗口组件CJuiDialog用法分析
2017/01/07 PHP
phpstudy默认不支持64位php的解决方法
2017/02/20 PHP
php调用云片网接口发送短信的实现方法
2017/10/25 PHP
收集的网上用的ajax之chat.js文件
2007/04/08 Javascript
奇妙的js
2007/09/24 Javascript
基于jquery的finkyUI插件与Ajax实现页面数据加载功能
2010/12/03 Javascript
cookie在javascript中的使用技巧以及隐私在服务器端的设置
2012/12/03 Javascript
jquery实现点击文字可编辑并修改保存至数据库
2014/04/15 Javascript
理解javascript中Map代替循环
2016/02/26 Javascript
在React框架中实现一些AngularJS中ng指令的例子
2016/03/06 Javascript
jquery制做精致的倒计时特效
2016/06/13 Javascript
深入理解bootstrap框架之入门准备
2016/10/09 Javascript
JS填写银行卡号每隔4位数字加一个空格
2016/12/19 Javascript
完美解决UI-Grid表格元素中多个空格显示为一个空格的问题
2017/04/25 Javascript
JavaScript中三个等号和两个等号你了解多少
2017/07/04 Javascript
Vue实现active点击切换方法
2018/03/16 Javascript
如何将HTML字符转换为DOM节点并动态添加到文档中详解
2018/08/19 Javascript
[01:07:21]NAVI vs VG Supermajor 败者组 BO3 第二场 6.5
2018/06/06 DOTA
Python map和reduce函数用法示例
2015/02/26 Python
使用pycharm生成代码模板的实例
2018/05/23 Python
python3实现SMTP发送邮件详细教程
2018/06/19 Python
python异步编程 使用yield from过程解析
2019/09/25 Python
Python查找不限层级Json数据中某个key或者value的路径方式
2020/02/27 Python
利用三角函数在canvas上画虚线的方法
2018/01/11 HTML / CSS
AJax面试题
2014/11/25 面试题
Java如何支持I18N?
2016/10/31 面试题
十佳护士获奖感言
2014/02/18 职场文书
研究生毕业自我鉴定范文
2014/03/27 职场文书
终止合同协议书
2014/04/17 职场文书
小学运动会演讲稿
2014/08/25 职场文书
学习党的群众路线对照检查材料
2014/09/29 职场文书
酒店保洁员岗位职责
2015/02/26 职场文书
关于Python中*args和**kwargs的深入理解
2021/08/07 Python