详解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 UI设置固定日期选择特效代码分享
Aug 27 Javascript
浏览器环境下JavaScript脚本加载与执行探析之defer与async特性
Jan 14 Javascript
JavaScript中输出信息的方法(信息确认框-提示输入框-文档流输出)
Jun 12 Javascript
Node连接mysql数据库方法介绍
Feb 07 Javascript
Vue获取DOM元素样式和样式更改示例
Mar 07 Javascript
Vue.js单向绑定和双向绑定实例分析
Aug 14 Javascript
webpack4 SCSS提取和懒加载的示例
Sep 03 Javascript
Vue动态组件和异步组件原理详解
May 06 Javascript
vue实现公告栏文字上下滚动效果的示例代码
Jun 16 Javascript
JavaScript的垃圾回收机制与内存管理
Aug 06 Javascript
vue 实现图片懒加载功能
Dec 31 Vue.js
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
php xml留言板 xml存储数据的简单例子
2009/08/24 PHP
PHP6 中可能会出现的新特性预览
2014/04/04 PHP
ThinkPHP使用getlist方法实现数据搜索功能示例
2017/05/08 PHP
php设计模式之备忘模式分析【星际争霸游戏案例】
2020/03/24 PHP
php查看一个变量的占用内存的实例代码
2020/03/29 PHP
jquery马赛克拼接翻转效果代码分享
2015/08/24 Javascript
JavaScript正则表达式匹配 div  style标签
2016/03/15 Javascript
需要牢记的JavaScript基础知识
2016/09/25 Javascript
浅析JavaScript动画模拟拖拽原理
2016/12/09 Javascript
nodejs基础应用
2017/02/03 NodeJs
requirejs按需加载angularjs文件实例
2017/06/08 Javascript
Angular2的管道Pipe的使用方法
2017/11/07 Javascript
bootstrap paginator分页插件的两种使用方式实例详解
2017/11/14 Javascript
Vue实现侧边菜单栏手风琴效果实例代码
2018/05/31 Javascript
jquery.param()实现数组或对象的序列化方法
2018/10/08 jQuery
jQuery简单实现根据日期计算星期几的方法
2019/01/09 jQuery
JavaScript中的&quot;=、==、===&quot;区别讲解
2019/01/22 Javascript
js模拟F11页面全屏显示
2019/09/17 Javascript
学前端,css与javascript重难点浅析
2020/06/11 Javascript
[10:14]2018DOTA2国际邀请赛寻真——paiN Gaming不仅为自己而战
2018/08/14 DOTA
[01:39:42]Fnatic vs Mineski 2018国际邀请赛小组赛BO2 第一场 8.17
2018/08/18 DOTA
Python中的Matplotlib模块入门教程
2015/04/15 Python
Python使用arrow库优雅地处理时间数据详解
2017/10/10 Python
python使用BeautifulSoup与正则表达式爬取时光网不同地区top100电影并对比
2019/04/15 Python
Python基于BeautifulSoup和requests实现的爬虫功能示例
2019/08/02 Python
利用django model save方法对未更改的字段依然进行了保存
2020/03/28 Python
Python自动重新加载模块详解(autoreload module)
2020/04/01 Python
Python yield生成器和return对比代码实例
2020/04/20 Python
Vince官网:全球著名设计师品牌,休闲而优雅的服饰
2017/01/15 全球购物
会计实习期自我鉴定
2013/10/06 职场文书
优秀党员主要事迹
2014/01/19 职场文书
计算机系本科生求职信
2014/05/31 职场文书
财务部副经理岗位职责范本
2014/06/17 职场文书
小学新课改心得体会
2016/01/22 职场文书
《初涉尘世》读后感3篇
2020/01/10 职场文书
七个Python必备的GUI库
2021/04/27 Python