詳解vue 組件的實(shí)現(xiàn)原理
組件機(jī)制的設(shè)計(jì),可以讓開(kāi)發(fā)者把一個(gè)復(fù)雜的應(yīng)用分割成一個(gè)個(gè)功能獨(dú)立組件,降低開(kāi)發(fā)的難度的同時(shí),也提供了極好的復(fù)用性和可維護(hù)性。本文我們一起從源碼的角度,了解一下組件的底層實(shí)現(xiàn)原理。
組件注冊(cè)時(shí)做了什么?
在Vue中使用組件,要做的第一步就是注冊(cè)。Vue提供了全局注冊(cè)和局部注冊(cè)兩種方式。
全局注冊(cè)方式如下:
Vue.component('my-component-name', { /* ... */ })
局部注冊(cè)方式如下:
var ComponentA = { /* ... */ }
new Vue({
el: '#app',
components: {
'component-a': ComponentA
}
})
全局注冊(cè)的組件,會(huì)在任何Vue實(shí)例中使用。局部注冊(cè)的組件,只能在該組件的注冊(cè)地,也就是注冊(cè)該組件的Vue實(shí)例中使用,甚至Vue實(shí)例的子組件中也不能使用。
有一定Vue使用經(jīng)驗(yàn)的小伙伴都了解上面的差異,但是為啥會(huì)有這樣的差異呢?我們從組件注冊(cè)的代碼實(shí)現(xiàn)上進(jìn)行解釋。
// 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 {
// 組件注冊(cè)
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
// 如果definition是一個(gè)對(duì)象,需要調(diào)用Vue.extend()轉(zhuǎn)換成函數(shù)。Vue.extend會(huì)創(chuàng)建一個(gè)Vue的子類(組件類),并返回子類的構(gòu)造函數(shù)。
definition = this.options._base.extend(definition)
}
// ...省略其他代碼
// 這里很關(guān)鍵,將組件添加到構(gòu)造函數(shù)的選項(xiàng)對(duì)象中Vue.options上。
this.options[type + 's'][id] = definition
return definition
}
}
})
// Vue的構(gòu)造函數(shù)
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的初始化中進(jìn)行選項(xiàng)對(duì)象的合并
Vue.prototype._init = function (options) {
const vm = this
vm._uid = uid++
vm._isVue = true
// ...省略其他代碼
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
// 合并vue選項(xiàng)對(duì)象,合并構(gòu)造函數(shù)的選項(xiàng)對(duì)象和實(shí)例中的選項(xiàng)對(duì)象
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// ...省略其他代碼
}
以上摘取了組件注冊(cè)的主要代碼。可以看到Vue實(shí)例的選項(xiàng)對(duì)象由Vue的構(gòu)造函數(shù)選項(xiàng)對(duì)象和Vue實(shí)例的選項(xiàng)對(duì)象兩部分組成。
全局注冊(cè)的組件,實(shí)際上通過(guò)Vue.component添加到了Vue構(gòu)造函數(shù)的選項(xiàng)對(duì)象 Vue.options.components 上了。
Vue 在實(shí)例化時(shí)(new Vue(options))所指定的選項(xiàng)對(duì)象會(huì)與構(gòu)造函數(shù)的選項(xiàng)對(duì)象合并作為Vue實(shí)例最終的選項(xiàng)對(duì)象。因此,全局注冊(cè)的組件在所有的Vue實(shí)例中都可以使用,而在Vue實(shí)例中局部注冊(cè)的組件只會(huì)影響Vue實(shí)例本身。
為啥在HTML模板中可以正常使用組件標(biāo)簽?
我們知道組件可以跟普通的HTML一樣在模板中直接使用。例如:
<div id="app"> <!--使用組件button-counter--> <button-counter></button-counter> </div>
// 全局注冊(cè)一個(gè)名為 button-counter 的組件
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
// 創(chuàng)建Vue實(shí)例
new Vue({
el: '#app'
})
那么,當(dāng)Vue解析到自定義的組件標(biāo)簽時(shí)是如何處理的呢?
Vue 對(duì)組件標(biāo)簽的解析與普通HTML標(biāo)簽的解析一樣,不會(huì)因?yàn)槭欠?HTML標(biāo)準(zhǔn)的標(biāo)簽而特殊處理。處理過(guò)程中第一個(gè)不同的地方出現(xiàn)在vnode節(jié)點(diǎn)創(chuàng)建時(shí)。vue 內(nèi)部通過(guò)_createElement函數(shù)實(shí)現(xiàn)vnode的創(chuàng)建。
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標(biāo)簽
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))) {
// 如果是組件標(biāo)簽,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標(biāo)簽不是合法的HTML標(biāo)簽,不能直接new VNode()創(chuàng)建vnode。Vue 會(huì)通過(guò)resolveAsset函數(shù)檢查該標(biāo)簽是否為自定義組件的標(biāo)簽。
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實(shí)例本身有無(wú)該組件
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]
// 如果實(shí)例上沒(méi)有找到,去查找原型鏈
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是我們?nèi)肿?cè)的組件,顯然可以在this.$options.components找到其定義。因此,Vue會(huì)執(zhí)行createComponent函數(shù)來(lái)生成組件的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的構(gòu)造函數(shù)
const baseCtor = context.$options._base
// 如果Ctor是一個(gè)選項(xiàng)對(duì)象,需要使用Vue.extend使用選項(xiàng)對(duì)象,創(chuàng)建將組件選項(xiàng)對(duì)象轉(zhuǎn)換成一個(gè)Vue的子類
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
// 如果Ctor還不是一個(gè)構(gòu)造函數(shù)或者異步組件工廠函數(shù),不再往下執(zhí)行。
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 || {}
// 重新解析構(gòu)造函數(shù)的選項(xiàng)對(duì)象,在組件構(gòu)造函數(shù)創(chuàng)建后,Vue可能會(huì)使用全局混入造成構(gòu)造函數(shù)選項(xiàng)對(duì)象改變。
resolveConstructorOptions(Ctor)
// 處理組件的v-model
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// 提取props
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// 函數(shù)式組件
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)
// 創(chuàng)建 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允許通過(guò)一個(gè)選項(xiàng)對(duì)象定義組件,Vue需要使用Vue.extend將組件的選項(xiàng)對(duì)象轉(zhuǎn)換成一個(gè)構(gòu)造函數(shù)。
/**
* Vue類繼承,以Vue的原型為原型創(chuàng)建Vue組件子類。繼承實(shí)現(xiàn)方式是采用Object.create(),在內(nèi)部實(shí)現(xiàn)中,加入了緩存的機(jī)制,避免重復(fù)創(chuàng)建子類。
*/
Vue.extend = function (extendOptions: Object): Function {
// extendOptions 是組件的選項(xiàng)對(duì)象,與vue所接收的一樣
extendOptions = extendOptions || {}
// Super變量保存對(duì)父類Vue的引用
const Super = this
// SuperId 保存父類的cid
const SuperId = Super.cid
// 緩存構(gòu)造函數(shù)
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)
}
// 定義組件的構(gòu)造函數(shù)
const Sub = function VueComponent (options) {
this._init(options)
}
// 組件的原型對(duì)象指向Vue的選項(xiàng)對(duì)象
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
// 為組件分配一個(gè)cid
Sub.cid = cid++
// 將組件的選項(xiàng)對(duì)象與Vue的選項(xiàng)合并
Sub.options = mergeOptions(
Super.options,
extendOptions
)
// 通過(guò)super屬性指向父類
Sub['super'] = Super
// 將組件實(shí)例的props和computed屬代理到組件原型對(duì)象上,避免每個(gè)實(shí)例創(chuàng)建的時(shí)候重復(fù)調(diào)用Object.defineProperty。
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
// 復(fù)制父類Vue上的extend/mixin/use等全局方法
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// 復(fù)制父類Vue上的component、directive、filter等資源注冊(cè)方法
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub
}
// 保存父類Vue的選項(xiàng)對(duì)象
Sub.superOptions = Super.options
// 保存組件的選項(xiàng)對(duì)象
Sub.extendOptions = extendOptions
// 保存最終的選項(xiàng)對(duì)象
Sub.sealedOptions = extend({}, Sub.options)
// 緩存組件的構(gòu)造函數(shù)
cachedCtors[SuperId] = Sub
return Sub
}
}
還有一處重要的代碼是installComponentHooks(data)。該方法會(huì)給組件vnode的data添加組件鉤子,這些鉤子在組件的不同階段被調(diào)用,例如init鉤子在組件patch時(shí)會(huì)調(diào)用。
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]
// 內(nèi)置的組件vnode鉤子
const toMerge = componentVNodeHooks[key]
// 合并鉤子
if (existing !== toMerge && !(existing && existing._merged)) {
hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
}
}
}
// 組件vnode的鉤子。
const componentVNodeHooks = {
// 實(shí)例化組件
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 {
// 生成組件實(shí)例
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
// 觸發(fā)組件的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標(biāo)簽一樣,為組件生成vnode節(jié)點(diǎn):
// 創(chuàng)建 vnode
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
組件在patch時(shí)對(duì)vnode的處理與普通標(biāo)簽有所不同。
Vue 如果發(fā)現(xiàn)正在patch的vnode是組件,那么調(diào)用createComponent方法。
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
// 執(zhí)行組件鉤子中的init鉤子,創(chuàng)建組件實(shí)例
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// init鉤子執(zhí)行后,如果vnode是個(gè)子組件,該組件應(yīng)該創(chuàng)建一個(gè)vue子實(shí)例,并掛載到DOM元素上。子組件的vnode.elm也設(shè)置完成。然后我們只需要返回該DOM元素。
if (isDef(vnode.componentInstance)) {
// 設(shè)置vnode.elm
initComponent(vnode, insertedVnodeQueue)
// 將組件的elm插入到父組件的dom節(jié)點(diǎn)上
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
createComponent會(huì)調(diào)用組件vnode的data對(duì)象上定義的init鉤子方法,創(chuàng)建組件實(shí)例?,F(xiàn)在我們回過(guò)頭來(lái)看下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 {
// 生成組件實(shí)例
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
// 掛載組件,與vue的$mount一樣
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
}
// ...省略其他代碼
由于組件是初次創(chuàng)建,因此init鉤子會(huì)調(diào)用createComponentInstanceForVnode創(chuàng)建一個(gè)組件實(shí)例,并賦值給vnode.componentInstance。
export function createComponentInstanceForVnode (
vnode: any,
parent: any,
): Component {
// 內(nèi)部組件選項(xiàng)
const options: InternalComponentOptions = {
// 標(biāo)記是否是組件
_isComponent: true,
// 父Vnode
_parentVnode: vnode,
// 父Vue實(shí)例
parent
}
// check inline-template render functions
const inlineTemplate = vnode.data.inlineTemplate
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
// new 一個(gè)組件實(shí)例。組件實(shí)例化 與 new Vue() 執(zhí)行的過(guò)程相同。
return new vnode.componentOptions.Ctor(options)
}
createComponentInstanceForVnode 中會(huì)執(zhí)行 new vnode.componentOptions.Ctor(options)。由前面我們?cè)趧?chuàng)建組件vnode時(shí)可知,vnode.componentOptions的值是一個(gè)對(duì)象:{ Ctor, propsData, listeners, tag, children },其中包含了組件的構(gòu)造函數(shù)Ctor。因此 new vnode.componentOptions.Ctor(options)等價(jià)于new VueComponent(options)。
// 生成組件實(shí)例 const child = vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance) // 掛載組件,與vue的$mount一樣 child.$mount(hydrating ? vnode.elm : undefined, hydrating)
等價(jià)于:
new VueComponent(options).$mount(hydrating ? vnode.elm : undefined, hydrating)
這段代碼想必大家都很熟悉了,是組件初始化和掛載的過(guò)程。組件的初始化和掛載與在前文中所介紹Vue初始化和掛載過(guò)程相同,因此不再展開(kāi)說(shuō)明。大致的過(guò)程就是創(chuàng)建了一個(gè)組件實(shí)例并掛載后。使用initComponent將組件實(shí)例的$el設(shè)置為vnode.elm的值。最后,調(diào)用insert將組件實(shí)例的DOM根節(jié)點(diǎn)插入其父節(jié)點(diǎn)。然后就完成了組件的處理。
總結(jié)
通過(guò)對(duì)組件底層實(shí)現(xiàn)的分析,我們可以知道,每個(gè)組件都是一個(gè)VueComponent實(shí)例,而VueComponent又是繼承自Vue。每個(gè)組件實(shí)例獨(dú)立維護(hù)自己的狀態(tài)、模板的解析、DOM的創(chuàng)建和更新。篇幅有限,文中只分析了基本的組件的注冊(cè)解析過(guò)程,未對(duì)異步組件、keep-alive等做分析。等后面再慢慢補(bǔ)上。
以上就是詳解vue 組件的實(shí)現(xiàn)原理的詳細(xì)內(nèi)容,更多關(guān)于vue組件的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Vue引入并使用Element組件庫(kù)的兩種方式小結(jié)
本文主要介紹了Vue引入并使用Element組件庫(kù)的兩種方式小結(jié),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01
vue中關(guān)于this.refs為空出現(xiàn)原因及分析
這篇文章主要介紹了vue中關(guān)于this.refs為空出現(xiàn)原因及分析,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-05-05
詳解Vue的數(shù)據(jù)及事件綁定和filter過(guò)濾器
這篇文章主要為大家介紹了Vue的數(shù)據(jù)及事件綁定和filter過(guò)濾器,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來(lái)幫助2022-01-01
輕量級(jí)富文本編輯器wangEditor結(jié)合vue使用方法示例
在我們項(xiàng)目中,有些時(shí)候需要使用富文本編輯器。本文將以百度開(kāi)發(fā)的Ueditor結(jié)合Vue.js介紹一下。非常具有實(shí)用價(jià)值,需要的朋友可以參考下2018-10-10
vue使用echarts實(shí)現(xiàn)柱狀圖動(dòng)態(tài)排序效果
echarts在前端開(kāi)發(fā)中實(shí)屬必不可缺的大數(shù)據(jù)可視化工具,這篇文章主要為大家詳細(xì)介紹了vue如何使用echarts實(shí)現(xiàn)柱狀圖動(dòng)態(tài)排序效果,感興趣的可以了解下2023-10-10
vue項(xiàng)目登錄模塊滑塊拼圖驗(yàn)證功能實(shí)現(xiàn)代碼(純前端)
滑塊驗(yàn)證作為一種反機(jī)器人的工具,也會(huì)不斷發(fā)展和演進(jìn),以適應(yīng)不斷變化的威脅,這篇文章主要給大家介紹了vue項(xiàng)目登錄模塊滑塊拼圖驗(yàn)證功能實(shí)現(xiàn)的相關(guān)資料,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-07-07
vue.js中created()與activated()的個(gè)人使用解讀
這篇文章主要介紹了vue.js中created()與activated()的個(gè)人使用,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07
vue3+vite+antd如何實(shí)現(xiàn)自定義主題
這篇文章主要介紹了vue3+vite+antd如何實(shí)現(xiàn)自定義主題問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03

