詳解vue 元件的實現原理
元件機制的設計,可以讓開發者把一個複雜的應用分割成一個個功能獨立元件,降低開發的難度的同時,也提供了極好的複用性和可維護性。本文我們一起從原始碼的角度,瞭解一下元件的底層實現原理。
元件註冊時做了什麼?
在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,context ) } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options,'components',tag))) { // 如果是元件標籤,e.g. my-custom-tag vnode = createComponent(Ctor,context,tag) } else { vnode = new VNode( tag,context ) } } else { // direct component options / constructor vnode = createComponent(tag,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,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,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}` : ''}`,{ Ctor,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}` : ''}`,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,refElm) } return true } } }
createComponent會呼叫元件vnode的data物件上定義的init鉤子方法,建立元件例項。現在我們回過頭來看下init鉤子的程式碼:
// ... 省略其他程式碼 init (vnode: VNodeWithData,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,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元件的資料請關注我們其它相關文章!