详解vue 组件的实现原理
组件机制的设计,可以让开发者把一个复杂的应用分割成一个个功能独立组件,降低开发的难度的,也提供了极好的复用性和可维护性。本文我们一起从源码的角度,了解一下组件的底层实现原理。
组件注册时做了什么?
在Vue中使用组件,要做的第一步就是注册。Vue提供了全局注册和局部注册两种方式。
全局注册方式如下
Vue.ponent('my-ponent-name', { / ... / })
局部注册方式如下
var ComponentA = { / ... / } new Vue({ el: '#app', ponents: { 'ponent-a': ComponentA } })
全局注册的组件,会在任何Vue实例中使用。局部注册的组件,只能在该组件的注册地,也就是注册该组件的Vue实例中使用,甚至Vue实例的子组件中也不能使用。
有一定Vue使用经验的小伙伴都了解上面的差异,为啥会有这样的差异呢?我们从组件注册的代码实现上进行解释。
// Vue.ponent的核心代码 // ASSET_TYPES = ['ponent', 'directive', 'filter'] ASSET_TYPES.forEach(type => { Vue[type] = function (id, definition ){ if (!definition) { return this.options[type + 's'][id] } else { // 组件注册 if (type === 'ponent' && 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.ponent添加到了Vue构造函数的选项对象 Vue.options.ponents 上了。
Vue 在实例化时(new Vue(options))所指定的选项对象会与构造函数的选项对象合并作为Vue实例最终的选项对象。,全局注册的组件在所有的Vue实例中都可以使用,而在Vue实例中局部注册的组件只会影响Vue实例本身。
为啥在HTML模板中可以正常使用组件标签?
我们知道组件可以跟普通的HTML一样在模板中直接使用。例如
<div id="app"> <!--使用组件button-counter--> <button-counter></button-counter> </div>
// 全局注册一个名为 button-counter 的组件 Vue.ponent('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, 'ponents', 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 ponent 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.ponents找到其定义。,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 ponent, which is rendered // as a ment 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-ponent-${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和puted属代理到组件原型对象上,避免每个实例创建的时候重复调用Object.defineProperty。 if (Sub.options.props) { initProps(Sub) } if (Sub.options.puted) { initComputed(Sub) } // 复制父类Vue上的extend/mixin/use等全局方法 Sub.extend = Super.extend Sub.mixin = Super.mixin Sub.use = Super.use // 复制父类Vue上的ponent、directive、filter等资源注册方法 ASSET_TYPES.forEach(function (type) { Sub[type] = Super[type] }) // enable recursive self-lookup if (name) { Sub.options.ponents[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 = ponentVNodeHooks[key] // 合并钩子 if (existing !== toMerge && !(existing && existing._merged)) { hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge } } } // 组件vnode的钩子。 const ponentVNodeHooks = { // 实例化组件 init (vnode: VNodeWithData, hydrating: boolean): ?boolean { if ( vnode.ponentInstance && !vnode.ponentInstance._isDestroyed && vnode.data.keepAlive ) { // kept-alive ponents, treat as a patch const mountedNode: any = vnode // work around flow ponentVNodeHooks.prepatch(mountedNode, mountedNode) } else { // 生成组件实例 const child = vnode.ponentInstance = createComponentInstanceForVnode( vnode, activeInstance ) // 挂载组件,与vue的$mount一样 child.$mount(hydrating ? vnode.elm : undefined, hydrating) } }, prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) { const options = vnode.ponentOptions const child = vnode.ponentInstance = oldVnode.ponentInstance updateChildComponent( child, options.propsData, // updated props options.listeners, // updated listeners vnode, // new parent vnode options.children // new children ) }, insert (vnode: MountedComponentVNode) { const { context, ponentInstance } = vnode if (!ponentInstance._isMounted) { ponentInstance._isMounted = true // 触发组件的mounted钩子 callHook(ponentInstance, 'mounted') } if (vnode.data.keepAlive) { if (context._isMounted) { queueActivatedComponent(ponentInstance) } else { activateChildComponent(ponentInstance, true / direct /) } } }, destroy (vnode: MountedComponentVNode) { const { ponentInstance } = vnode if (!ponentInstance._isDestroyed) { if (!vnode.data.keepAlive) { ponentInstance.$destroy() } else { deactivateChildComponent(ponentInstance, true / direct /) } } } } const hooksToMerge = Object.keys(ponentVNodeHooks)
,与普通HTML标签一样,为组件生成vnode节点
// 创建 vnode const vnode = new VNode( `vue-ponent-${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.ponentInstance) && 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.ponentInstance)) { // 设置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.ponentInstance && !vnode.ponentInstance._isDestroyed && vnode.data.keepAlive ) { // kept-alive ponents, treat as a patch const mountedNode: any = vnode // work around flow ponentVNodeHooks.prepatch(mountedNode, mountedNode) } else { // 生成组件实例 const child = vnode.ponentInstance = createComponentInstanceForVnode( vnode, activeInstance ) // 挂载组件,与vue的$mount一样 child.$mount(hydrating ? vnode.elm : undefined, hydrating) } } // ...省略其他代码
由于组件是初次创建,init钩子会调用createComponentInstanceForVnode创建一个组件实例,并赋值给vnode.ponentInstance。
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.ponentOptions.Ctor(options) }
createComponentInstanceForVnode 中会执行 new vnode.ponentOptions.Ctor(options)。由前面我们在创建组件vnode时可知,vnode.ponentOptions的值是一个对象{ Ctor, propsData, listeners, tag, children },其中包含了组件的构造函数Ctor。 new vnode.ponentOptions.Ctor(options)等价于new VueComponent(options)。
// 生成组件实例 const child = vnode.ponentInstance = 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组件的资料请关注狼蚁SEO其它相关文章!
编程语言
- 如何快速学会编程 如何快速学会ug编程
- 免费学编程的app 推荐12个免费学编程的好网站
- 电脑怎么编程:电脑怎么编程网咯游戏菜单图标
- 如何写代码新手教学 如何写代码新手教学手机
- 基础编程入门教程视频 基础编程入门教程视频华
- 编程演示:编程演示浦丰投针过程
- 乐高编程加盟 乐高积木编程加盟
- 跟我学plc编程 plc编程自学入门视频教程
- ug编程成航林总 ug编程实战视频
- 孩子学编程的好处和坏处
- 初学者学编程该从哪里开始 新手学编程从哪里入
- 慢走丝编程 慢走丝编程难学吗
- 国内十强少儿编程机构 中国少儿编程机构十强有
- 成人计算机速成培训班 成人计算机速成培训班办
- 孩子学编程网上课程哪家好 儿童学编程比较好的
- 代码编程教学入门软件 代码编程教程