重學Vue源碼,根據黃軼大佬的vue技術揭祕,逐個過一遍,鞏固一下vue源碼知識點,畢竟嚼碎了纔是本身的,全部文章都同步在 公衆號(道道里的前端棧) 和 github 上。前端
在開發過程當中,自定義組件必須先註冊纔可使用,若是直接使用的話,會報一個錯:未知的自定義元素,就像下面這樣:vue
'Unknown custom element: <xxx> - did you register the component correctly?
For recursive components, make sure to provide the "name" option.'
複製代碼
在vue中提供了2種組件註冊的方式:全局註冊
和 局部註冊
,下面來把它們分析一下。node
全局註冊一個組件,通常會在 main.js
中這樣寫:git
Vue.component("comp-name", {
// options
})
複製代碼
使用了一個 Vue.component
函數來註冊,這個函數的定義過程是在最開始初始化Vue的全局函數的時候,代碼在 src/core/global-api/assets.js
中:github
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
ASSET_TYPES.forEach(type => {
Vue[type] = function ( id: string, definition: Function | Object ): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && type === 'component') {
validateComponentName(id)
}
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
this.options[type + 's'][id] = definition
return definition
}
}
})
複製代碼
能夠看出來經過遍歷 ASSET_TYPES
,往Vue上擴展了幾個方法,每一個方法都有兩個參數,一個id,一個自定義函數或對象,若是沒有 definitioin
,那就不日後走了,不然就繼續。在後面的邏輯裏,對組件名作了一層校驗,後面若是 type
是一個組件,而且它的定義是一個普通對象,就把 name
賦值,接着用 this.options._base.extend()
,把第二個參數轉換成一個構造器, this.options._base
其實就是大Vue(以前分析過,經過 Vue.options._base = Vue
得知的),而後使用 Vue.extend()
把參數轉化爲構造器,最後把這個構造器賦值給 this.options[type + 's'][id]
,也就是給大Vue擴展定義了一個 components
構造器,最終掛載到了 Vue.options.components
上。api
因爲在Vue初始化的時候會調用一個 _createElement
方法(它在render函數渲染和建立元素的時候分析過,能夠看這篇createElement作了什麼),在這個方法裏有這樣一段代碼:markdown
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
複製代碼
註冊組件會走到 vnode = createComponent(Ctor, data, context, children, tag)
邏輯中,就會建立一個組件vnode,能夠看到調用了一個 resolveAssets
方法,傳入了 vm.$options
, components
和 tag
,這個方法定義在 src/core/util/options.js
:ide
export function resolveAsset ( options: Object, type: string, id: string, warnMissing?: boolean ): any {
/* istanbul ignore if */
if (typeof id !== 'string') {
return
}
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
}
複製代碼
這裏注意一下第一個參數 options
,以前在分析合併配置的時候(能夠看這篇Vue的合併配置過程)有提到: vm.$options
實際上是自定義配置和大 Vue.options 一塊兒合併出來的,因此在 asset.js
中的最後,給大Vue擴展一個 components
,在 resolveAsset
第一個參數 options
上就能夠去找,也就是下面的一些 if
判斷了。函數
繼續看,type
傳入的是 components
,賦值給 assets
,而後判斷 assets
自身有 id
屬性的話,就返回它,不然就把 id
轉化爲駝峯,後面一樣的邏輯,根據駝峯去找,若是駝峯找不到,就找首字母大寫,若是仍是找不到,那麼註釋上寫的,去原型上找,原型上找的順序也是先 id
,再駝峯,再首字母大寫,若是還找不到,那這個vnode就是個空(或者說不認識這個vnode),也就會走到 _createElement
方法的後面判斷,也就是經過 new 來建立一個VNode:oop
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
複製代碼
這也就是爲何自定義全局組件的時候能夠把 id
寫成 駝峯,或者 首字母大寫 的方式使用。
捋一下,若是是全局自定義組件,就會在大 Vue.options.components 裏擴展了一個構造器,接着在初始化建立元素(_createElement)的時候,經過 resolveAsset
傳入的 tag
,解析出來一個有關組件標籤的定義,而後返回這個構造器,把它傳入到 createComponent
裏去建立組件的vnode,而後走patch和update過程最終變成一個真實DOM。
局部註冊通常會在某個vue文件這樣寫:
<template>
<Comp />
</template>
<script>
import Comp from "Comp.vue";
export default {
components:{
Comp
}
}
</script>
複製代碼
這樣就引入了一個局部組件,如今來分析一下它的過程。
回顧一下 Vue.extend
是如何合併 options
的:
const Sub = function VueComponent (options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
Sub.options = mergeOptions(
Super.options,
extendOptions
)
複製代碼
Super.options
是大 Vue.$options,後面的 extendOptions
就是上面例子中的這一塊:
export default {
components:{
Comp
}
}
複製代碼
把這兩個合併到了子組件構造器的 options
上,就是 Sub.options
上,接着在這個Sub初始化的時候,會調用一個 initInternalComponent
方法(也就是調用 _init
裏面的方法,代碼在 /src/core/instance/init.js
):
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
複製代碼
接着看下 initInternalComponent
這個方法:
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
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
}
}
複製代碼
裏面的 vm.constructor
就是上面說的 Sub
,這樣 Sub.options
就能夠拿到咱們在頁面裏寫的組件配置,而後賦值給 vm.$options
,因此能夠經過 vm.$options.components
拿到頁面裏定義的組件配置,那在全局註冊裏的提到的 assets
就能夠拿到這個局部註冊的組件配置。
注意:因爲局部組件的合併配置是擴展到 Sub.options
的,因此引入的這個局部組件只能在當前組件下使用(或者說當前vue頁面),而全局註冊是擴展到大 Vue.options 下的,也就是會走到 _init
方法的 vm.$options = mergeOptions()
這裏:
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
複製代碼
因此能夠全局使用。
個人公衆號:道道里的前端棧,每一天一篇前端文章,嚼碎的感受真奇妙~