//main.js import Vue from 'vue' import App from './App' import router from './router' Vue.config.productionTip = false let Hello = { name: 'hello', template: '這是全局組件hello' } Vue.component('hello', Hello) new Vue({ el: '#app', router, components: { App }, template: '' })
上面咱們就經過Vue.component()
註冊了一個全局組件hello
,接下來分析源碼實現的時候也是基於這個例子來進行的。html
<template> <div id="app"> <img src="./assets/logo.png"> <HelloWorld/> </div> </template> <script> import HelloWorld from './components/HelloWorld.vue' export default { name: 'App', components:{ HelloWorld } } </script>
像這樣就註冊了一個HelloWorld
的局部組件。vue
//【代碼塊1】 //代碼所在文件:src/core/global-api/index.js export function initGlobalAPI(Vue: GlobalAPI){ //...省略其餘無關代碼 initAssetRegisters(Vue) //這個方法就是用於組件註冊的方法 }
//【代碼塊2】 //代碼所在文件:src/core/global-api/assets.js export function initAssetRegister(Vue){ ASSET_TYPES.forEach(type=>{ //ASSET_TYPES包括component、directive、filter Vue[type] = function(id, definition){ //...一些條件判斷 if(type === 'component' && isPlainObject(definition)){ definition.name = definition.name || id definition = this.options._base.extend(definition) //將definition轉換爲一個繼承於Vue的構造函數 } //...其餘類型的處理 this.options[type+'s'][id] = definition //將這個構造函數掛載到Vue.options.components上 return definition } }) }
此時,咱們能夠單步調試一下咱們上面的例子,來看一下definition
一開始是什麼,以及執行掛載後Vue.options
變成了什麼樣子:node
//【代碼塊3】 //代碼所在文件:src/core/instance/init.js Vue.prototype._init = function(options){ //..省略其餘無關代碼 if(options && options._isComponent){ //組件 initInternalComponent(vm, options) }else{ //非組件 vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options||{}, vm ) } }
這裏將本身定義的組件的options
與Vue.options
作了一個合併,而且賦值給了vm.$options
,而經過【代碼塊2】咱們能夠知道全局組件的構造函數已經被放在了Vue.options.components
上,因此通過這一步,vm.$options.components
上面也有了全局組件的構造函數。因此如今在任意組件都能拿到全局組件,由於任何組件初始化的時候都會執行這個合併。api
咱們能夠經過單步調試上面的例子看一下如今的vm.$options
上面有些什麼app
//【代碼塊4】 //代碼所在文件:src/core/vdom/create-element.js export function _createElement(context, tag, data, children, normalization){ if(typeof tag === 'string'){ //... if(config.isReservedTag(tag)){ //...保留的html標籤 }else if(isDef(Ctor = resolveAsset(context.$options, 'component', tag))){ //已經註冊過的全局組件 vnode = createComponent(Ctor, data, context, children, tag) }else{ //不是內置標籤也不是已經註冊過的組件,就建立一個全新的vnode vnode = new VNode( tag, data, children, undefined, undefined, context ) } } }
上面代碼中有一個比較重要的方法resolveAsset()
,用於判斷在context.$options.compononts
(即vm.$options.components
)上面是否能找到這個組件的構造函數,若是能找到,返回這個構造函數,(具體方法見【代碼塊5】)根據【代碼塊3】咱們能夠知道若是這個組件是全局註冊的組件,那麼咱們就能夠獲得這個構造函數,並進入這個else if
判斷,經過createComponent()
獲得vnode
。dom
//【代碼塊5】 //代碼所在文件:src/core/utils/options.js export function resolveAsset(options, type, id, warnMissing){ //options即上面調用的時候傳入的context.$options, //由【代碼塊3】,vm.$options是由咱們自定義的options以及Vue上的options合併而來的 //type如今是components const assets = options[type] // check local registration variations first 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] // fallback to prototype chain 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 }
先經過 const assets = options[type]
拿到 assets
,而後再嘗試拿 assets[id]
,這裏有個順序,先直接使用 id
拿,若是不存在,則把 id
變成駝峯的形式再拿,若是仍然不存在則在駝峯的基礎上把首字母再變成大寫的形式再拿,若是仍然拿不到則報錯。這樣說明了咱們在使用 Vue.component(id, definition)
全局註冊組件的時候,id
能夠是連字符、駝峯或首字母大寫的形式。函數
組件在執行render()
的時候,會執行createComponent
函數,在這個函數裏面會執行extend()
函數生成一個構造函數,也是在這個extend()
函數中,執行了一個options
的合併this
//【代碼塊5】 //代碼所在文件:src/core/global-api/extend.js Vue.entend = function(extendOptions){ //... Sub.options = mergeOptions( Super.options, //Vue的options extendOptions //定義組件的那個對象 ) //... }
能夠看出這裏是將本身傳入的options
(即定義組件的那個對象)與Vue.options
合併,而後放到Sub.options
上,同時,由於Sub.options
上面合併了Vue
的options
,因此組件裏面也能夠拿到全局註冊的組件。spa
//【代碼塊6(同代碼塊3)】 //代碼所在文件:src/core/instance/init.js Vue.prototype._init = function(options){ //.. if(options && options._isComponent){ initInternalComponent(vm, options) }else{ vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options||{}, vm ) } }
組件初始化的過程當中會進入if
判斷語句,執行initInternalComponent()
prototype
//【代碼塊7】 //代碼所在文件:src/core/instance/init.js export function initInternalComponent (vm: Component, options: InternalComponentOptions) { const opts = vm.$options = Object.create(vm.constructor.options) //vm.constructor即爲Sub,在代碼塊5中,咱們已經將局部組件放在了Sub.options上 //因此這裏將局部組件的構造函數放在了vm.$options上 //這樣在執行【代碼塊4】的時候一樣也能經過resolveAsset獲得局部註冊組件的構造函數 const parentVnode = options._parentVnode opts.parent = options.parent opts._parentVnode = parentVnode //將componentOptions裏面的別的屬性賦值給opts const vnodeComponentOptions = parentVnode.componentOptions opts.propsData = vnodeComponentOptions.propsData opts._parentListeners = vnodeComponentOptions.listeners opts._renderChildren = vnodeComponentOptions.children opts._componentTag = vnodeComponentOptions.tag if (options.render) { opts.render = options.render opts.staticRenderFns = options.staticRenderFns } }
因爲全局註冊的組件是將組件的構造函數擴展到了Vue.options.components
上,而組件在初始化的時候都會將自身options
與Vue.options
合併,擴展到當前組件的vm.$options.components
下,因此全局組件能在任意組件被使用。而局部註冊的組件是將組件的構造函數擴展到了當前組件的vm.$options.components
下,因此只能在當前組件使用。