详解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 相关文章推荐
JavaScript 对象模型 执行模型
Dec 06 Javascript
JS简单实现元素复制示例附图
Nov 19 Javascript
js 设置缓存及获取设置的缓存
May 08 Javascript
深入了解Node.js中的一些特性
Sep 25 Javascript
JavaScript中Function()函数的使用教程
Jun 04 Javascript
Node.js制作简单聊天室
Jan 12 Javascript
浅谈事件冒泡、事件委托、jQuery元素节点操作、滚轮事件与函数节流
Jul 22 jQuery
Js面试算法详解
Apr 08 Javascript
使用Angular-CLI构建NPM包的方法
Sep 07 Javascript
Node使用koa2实现一个简单JWT鉴权的方法
Jan 26 Javascript
Vue 事件的$event参数=事件的值案例
Jan 29 Vue.js
详解javascript脚本何时会被执行
Feb 05 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
php escape URL编码
2008/12/10 PHP
用PHP读取和编写XML DOM的实现代码
2011/02/03 PHP
使用ThinkPHP+Uploadify实现图片上传功能
2014/06/26 PHP
PHP 使用 Imagick 裁切/生成缩略图/添加水印自动检测和处理 GIF
2016/02/19 PHP
ext 同步和异步示例代码
2009/09/18 Javascript
两个比较有用的Javascript工具函数代码
2010/02/17 Javascript
WEB 浏览器兼容 推荐收藏
2010/05/14 Javascript
IE6、IE7中setAttribute不支持class/for/rowspan/colspan等属性
2011/08/28 Javascript
js判断是否为数组的函数: isArray()
2011/10/30 Javascript
每天一篇javascript学习小结(Function对象)
2015/11/16 Javascript
Javascript闭包实例详解
2015/11/29 Javascript
jQuery 利用$.ajax 时获取原生XMLHttpRequest 对象的方法
2016/08/25 Javascript
jQuery实现的简单拖拽功能示例
2016/09/13 Javascript
easyui datagrid 大数据加载效率慢,优化解决方法(推荐)
2016/11/09 Javascript
jquery对象与DOM对象转化
2017/02/08 Javascript
JavaScript实现短暂提示框功能
2018/04/04 Javascript
浅谈layui里的上传控件问题
2019/09/26 Javascript
JQuery绑定事件四种实现方法解析
2020/12/02 jQuery
Vue中强制组件重新渲染的正确方法
2021/01/03 Vue.js
Python抓取京东图书评论数据
2014/08/31 Python
python根据路径导入模块的方法
2014/09/30 Python
python读取文本绘制动态速度曲线
2018/06/21 Python
使用pandas把某一列的字符值转换为数字的实例
2019/01/29 Python
python matplotlib画图库学习绘制常用的图
2019/03/19 Python
pandas DataFrame 行列索引及值的获取的方法
2019/07/02 Python
pandas条件组合筛选和按范围筛选的示例代码
2019/08/26 Python
django创建超级用户时指定添加其它字段方式
2020/05/14 Python
在Keras中CNN联合LSTM进行分类实例
2020/06/29 Python
用python查找统一局域网下ip对应的mac地址
2021/01/13 Python
The Body Shop美体小铺西班牙官网:天然化妆品
2019/06/21 全球购物
.NET现在共支持多少种语言
2014/02/26 面试题
通信工程专业女生个人求职信
2013/09/21 职场文书
公司聘任书模板
2014/03/29 职场文书
党支部反对四风思想汇报
2014/10/10 职场文书
年会邀请函范文
2015/01/30 职场文书
2019年销售部季度工作计划3篇
2019/10/09 职场文书