組件是
Vue
的一個重要核心,咱們在進行項目工程化時,會將頁面的結構組件化。組件化意味着獨立和共享,而兩個結論並不矛盾,獨立的組件開發可讓開發者專一於某個功能項的開發和擴展,而組件的設計理念又使得功能項更加具備複用性,不一樣的頁面能夠進行組件功能的共享。對於開發者而言,編寫Vue
組件是掌握Vue
開發的核心基礎,Vue
官網也花了大量的篇幅介紹了組件的體系和各類使用方法。這一節內容,咱們會深刻Vue
組件內部的源碼,瞭解組件註冊的實現思路,並結合上一節介紹的實例掛載分析組件渲染掛載的基本流程,最後咱們將分析組件和組件之間是如何創建聯繫的。我相信,掌握這些底層的實現思路對於咱們從此在解決vue
組件相關問題上會有明顯的幫助。html
熟悉Vue
開發流程的都知道,Vue
組件在使用以前須要進行註冊,而註冊的方式有兩種,全局註冊和局部註冊。在進入源碼分析以前,咱們先回憶一下二者的用法,以便後續掌握二者的差別。vue
Vue.component('my-test', {
template: '<div>{{test}}</div>',
data () {
return {
test: 1212
}
}
})
var vm = new Vue({
el: '#app',
template: '<div id="app"><my-test><my-test/></div>'
})
複製代碼
其中組件的全局註冊須要在全局實例化Vue前調用,註冊以後能夠用在任何新建立的Vue
實例中調用。html5
var myTest = {
template: '<div>{{test}}</div>',
data () {
return {
test: 1212
}
}
}
var vm = new Vue({
el: '#app',
component: {
myTest
}
})
複製代碼
當只須要在某個局部用到某個組件時,可使用局部註冊的方式進行組件註冊,此時局部註冊的組件只能在註冊該組件內部使用。node
在簡單回顧組件的兩種註冊方式後,咱們來看註冊過程到底發生了什麼,咱們以全局組件註冊爲例。它經過Vue.component(name, {...})
進行組件註冊,Vue.component
是在Vue
源碼引入階段定義的靜態方法。算法
// 初始化全局api
initAssetRegisters(Vue);
var ASSET_TYPES = [
'component',
'directive',
'filter'
];
function initAssetRegisters(Vue){
// 定義ASSET_TYPES中每一個屬性的方法,其中包括component
ASSET_TYPES.forEach(function (type) {
// type: component,directive,filter
Vue[type] = function (id,definition) {
if (!definition) {
// 直接返回註冊組件的構造函數
return this.options[type + 's'][id]
}
...
if (type === 'component') {
// 驗證component組件名字是否合法
validateComponentName(id);
}
if (type === 'component' && isPlainObject(definition)) {
// 組件名稱設置
definition.name = definition.name || id;
// Vue.extend() 建立子組件,返回子類構造器
definition = this.options._base.extend(definition);
}
// 爲Vue.options 上的component屬性添加將子類構造器
this.options[type + 's'][id] = definition;
return definition
}
});
}
複製代碼
Vue.components
有兩個參數,一個是須要註冊組件的組件名,另外一個是組件選項,若是第二個參數沒有傳遞,則會直接返回註冊過的組件選項。不然意味着須要對該組件進行註冊,註冊過程先會對組件名的合法性進行檢測,要求組件名不容許出現非法的標籤,包括Vue
內置的組件名,如slot, component
等。api
function validateComponentName(name) {
if (!new RegExp(("^[a-zA-Z][\\-\\.0-9_" + (unicodeRegExp.source) + "]*$")).test(name)) {
// 正則判斷檢測是否爲非法的標籤
warn(
'Invalid component name: "' + name + '". Component names ' +
'should conform to valid custom element name in html5 specification.'
);
}
// 不能使用Vue自身自定義的組件名,如slot, component,不能使用html的保留標籤,如 h1, svg等
if (isBuiltInTag(name) || config.isReservedTag(name)) {
warn(
'Do not use built-in or reserved HTML elements as component ' +
'id: ' + name
);
}
}
複製代碼
在通過組件名的合法性檢測後,會調用extend
方法爲組件建立一個子類構造器,此時的this.options._base
表明的就是Vue
構造器。extend
方法的定義在介紹選項合併章節有重點介紹過,它會基於父類去建立一個子類,此時的父類是Vue
,而且建立過程子類會繼承父類的方法,並會和父類的選項進行合併,最終返回一個子類構造器。bash
代碼處還有一個邏輯,Vue.component()
默認會把第一個參數做爲組件名稱,可是若是組件選項有name
屬性時,name
屬性值會將組件名覆蓋。app
總結起來,全局註冊組件就是Vue
實例化前建立一個基於Vue
的子類構造器,並將組件的信息加載到實例options.components
對象中。dom
**接下來天然而然會想到一個問題,局部註冊和全局註冊在實現上的區別體如今哪裏?**咱們不急着分析局部組件的註冊流程,先以全局註冊的組件爲基礎,看看做爲組件,它的掛載流程有什麼不一樣。async
上一節內容咱們介紹了Vue
如何將一個模板,經過render
函數的轉換,最終生成一個Vnode tree
的,在不包含組件的狀況下,_render
函數的最後一步是直接調用new Vnode
去建立一個完整的Vnode tree
。然而有一大部分的分支咱們並無分析,那就是遇到組件佔位符的場景。執行階段若是遇到組件,處理過程要比想像中複雜得多,咱們經過一張流程圖展開分析。
咱們結合實際的例子對照着流程圖分析一下這個過程:
Vue.component('test', {
template: '<span></span>'
})
var vm = new Vue({
el: '#app',
template: '<div><test></test></div>'
})
複製代碼
render
函數function() {
with(this){return _c('div',[_c('test')],1)}
}
複製代碼
Vue
根實例初始化會執行 vm.$mount(vm.$options.el)
實例掛載的過程,按照以前的邏輯,完整流程會經歷render
函數生成Vnode
,以及Vnode
生成真實DOM
的過程。render
函數生成Vnode
過程當中,子會優先父執行生成Vnode
過程,也就是_c('test')
函數會先被執行。'test'
會先判斷是普通的html
標籤仍是組件的佔位符。new Vnode
過程,這也是上一章節咱們分析的過程;若是是組件的佔位符,則會在判斷組件已經被註冊過的前提下進入createComponent
建立子組件Vnode
的過程。createComponent
是建立組件Vnode
的過程,建立過程會再次合併選項配置,並安裝組件相關的內部鉤子(後面文章會再次提到內部鉤子的做用),最後經過new Vnode()
生成以vue-component
開頭的Virtual DOM
render
函數執行過程也是一個循環遞歸調用建立Vnode
的過程,執行3,4步以後,完整的生成了一個包含各個子組件的Vnode tree
_createElement
函數的實現以前章節分析過一部分,咱們重點看看組件相關的操做。
// 內部執行將render函數轉化爲Vnode的函數
function _createElement(context,tag,data,children,normalizationType) {
···
if (typeof tag === 'string') {
// 子節點的標籤爲普通的html標籤,直接建立Vnode
if (config.isReservedTag(tag)) {
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
);
// 子節點標籤爲註冊過的組件標籤名,則子組件Vnode的建立過程
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 建立子組件Vnode
vnode = createComponent(Ctor, data, context, children, tag);
}
}
}
複製代碼
config.isReservedTag(tag)
用來判斷標籤是否爲普通的html
標籤,若是是普通節點會直接建立Vnode
節點,若是不是,則須要判斷這個佔位符組件是否已經註冊到,咱們能夠經過context.$options.components[組件名]
拿到註冊後的組件選項。如何判斷組件是否已經全局註冊,看看resolveAsset
的實現。
// 須要明確組件是否已經被註冊
function resolveAsset (options,type,id,warnMissing) {
// 標籤爲字符串
if (typeof id !== 'string') {
return
}
// 這裏是 options.component
var assets = options[type];
// 這裏的分支分別支持大小寫,駝峯的命名規範
if (hasOwn(assets, id)) { return assets[id] }
var camelizedId = camelize(id);
if (hasOwn(assets, camelizedId)) { return assets[camelizedId] }
var PascalCaseId = capitalize(camelizedId);
if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] }
// fallback to prototype chain
var res = assets[id] || assets[camelizedId] || assets[PascalCaseId];
if (warnMissing && !res) {
warn(
'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
options
);
}
// 最終返回子類的構造器
return res
}
複製代碼
拿到註冊過的子類構造器後,調用createComponent
方法建立子組件Vnode
// 建立子組件過程
function createComponent (
Ctor, // 子類構造器
data,
context, // vm實例
children, // 子節點
tag // 子組件佔位符
) {
···
// Vue.options裏的_base屬性存儲Vue構造器
var baseCtor = context.$options._base;
// 針對局部組件註冊場景
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor);
}
data = data || {};
// 構造器配置合併
resolveConstructorOptions(Ctor);
// 掛載組件鉤子
installComponentHooks(data);
// return a placeholder vnode
var name = Ctor.options.name || tag;
// 建立子組件vnode,名稱以 vue-component- 開頭
var vnode = new VNode(("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),data, undefined, undefined, undefined, context,{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },asyncFactory);
return vnode
}
複製代碼
這裏將大部分的代碼都拿掉了,只留下建立Vnode
相關的代碼,最終會經過new Vue
實例化一個名稱以vue-component-
開頭的Vnode
節點。其中兩個關鍵的步驟是配置合併和安裝組件鉤子函數,選項合併的內容能夠查看這個系列的前兩節,這裏看看installComponentHooks
安裝組件鉤子函數時作了哪些操做。
// 組件內部自帶鉤子
var componentVNodeHooks = {
init: function init (vnode, hydrating) {
},
prepatch: function prepatch (oldVnode, vnode) {
},
insert: function insert (vnode) {
},
destroy: function destroy (vnode) {
}
};
var hooksToMerge = Object.keys(componentVNodeHooks);
// 將componentVNodeHooks 鉤子函數合併到組件data.hook中
function installComponentHooks (data) {
var hooks = data.hook || (data.hook = {});
for (var i = 0; i < hooksToMerge.length; i++) {
var key = hooksToMerge[i];
var existing = hooks[key];
var toMerge = componentVNodeHooks[key];
// 若是鉤子函數存在,則執行mergeHook$1方法合併
if (existing !== toMerge && !(existing && existing._merged)) {
hooks[key] = existing ? mergeHook$1(toMerge, existing) : toMerge;
}
}
}
function mergeHook$1 (f1, f2) {
// 返回一個依次執行f1,f2的函數
var merged = function (a, b) {
f1(a, b);
f2(a, b);
};
merged._merged = true;
return merged
}
複製代碼
組件默認自帶的這幾個鉤子函數會在後續patch
過程的不一樣階段執行,這部份內容不在本節的討論範圍。
在說到全局註冊和局部註冊的用法時留下了一個問題,局部註冊和全局註冊二者的區別在哪裏。其實局部註冊的原理一樣簡單,咱們使用局部註冊組件時會經過在父組件選項配置中的components
添加子組件的對象配置,這和全局註冊後在Vue
的options.component
添加子組件構造器的結果很類似。區別在於:
- 1.局部註冊添加的對象配置是在某個組件下,而全局註冊添加的子組件是在根實例下。 - 2.局部註冊添加的是一個子組件的配置對象,而全局註冊添加的是一個子類構造器。
所以局部註冊中缺乏了一步構建子類構造器的過程,這個過程放在哪裏進行呢? 回到createComponent
的源碼,源碼中根據選項是對象仍是函數來區分局部和全局註冊組件,若是選項的值是對象,則該組件是局部註冊的組件,此時在建立子Vnode
時會調用 父類的extend
方法去建立一個子類構造器。
function createComponent (...) {
...
var baseCtor = context.$options._base;
// 針對局部組件註冊場景
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor);
}
}
複製代碼
根據前面的分析,不論是全局註冊的組件仍是局部註冊的組件,組件並無進行實例化,那麼組件實例化的過程發生在哪一個階段呢?咱們接着看Vnode tree
渲染真實DOM
的過程。
vm._render()
生成完整的Virtual Dom
樹後,緊接着執行Vnode
渲染真實DOM
的過程,這個過程是vm.update()
方法的執行,而其核心是vm.__patch__
。vm.__patch__
內部會經過 createElm
去建立真實的DOM
元素,期間遇到子Vnode
會遞歸調用createElm
方法。createComponent
方法判斷的,該方法和渲染Vnode
階段的方法createComponent
不一樣,他會調用子組件的init
初始化鉤子函數,並完成組件的DOM
插入。init
初始化鉤子函數的核心是new
實例化這個子組件並將子組件進行掛載,實例化子組件的過程又回到合併配置,初始化生命週期,初始化事件中心,初始化渲染的過程。實例掛載又會執行$mount
過程。__patch__
核心代碼是經過createElm
建立真實節點,當建立過程當中遇到子vnode
時,會調用createChildren
,createChildren
的目的是對子vnode
遞歸調用createElm
建立子組件節點。
// 建立真實dom
function createElm (vnode,insertedVnodeQueue,parentElm,refElm,nested,ownerArray,index) {
···
// 遞歸建立子組件真實節點,直到完成全部子組件的渲染才進行根節點的真實節點插入
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
···
var children = vnode.children;
//
createChildren(vnode, children, insertedVnodeQueue);
···
insert(parentElm, vnode.elm, refElm);
}
function createChildren(vnode, children, insertedVnodeQueue) {
for (var i = 0; i < children.length; ++i) {
// 遍歷子節點,遞歸調用建立真實dom節點的方法 - createElm
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
}
}
複製代碼
createComponent
方法會對子組件Vnode
進行處理中,還記得在Vnode
生成階段爲子Vnode
安裝了一系列的鉤子函數嗎,在這個步驟咱們能夠經過是否擁有這些定義好的鉤子來判斷是不是已經註冊過的子組件,若是條件知足,則執行組件的init
鉤子。
init
鉤子作的事情只有兩個,實例化組件構造器,執行子組件的掛載流程。(keep-alive
分支看具體的文章分析)
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
var i = vnode.data;
// 是否有鉤子函數能夠做爲判斷是否爲組件的惟一條件
if (isDef(i = i.hook) && isDef(i = i.init)) {
// 執行init鉤子函數
i(vnode, false /* hydrating */);
}
···
}
var componentVNodeHooks = {
// 忽略keepAlive過程
// 實例化
var child = vnode.componentInstance = createComponentInstanceForVnode(vnode,activeInstance);
// 掛載
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
}
function createComponentInstanceForVnode(vnode, parent) {
···
// 實例化Vue子組件實例
return new vnode.componentOptions.Ctor(options)
}
複製代碼
顯然Vnode
生成真實DOM
的過程也是一個不斷遞歸建立子節點的過程,patch
過程若是遇到子Vnode
,會優先實例化子組件,而且執行子組件的掛載流程,而掛載流程又會回到_render,_update
的過程。在全部的子Vnode
遞歸掛載後,最終纔會真正掛載根節點。
平常開發中,咱們能夠經過vm.$parent
拿到父實例,也能夠在父實例中經過vm.$children
拿到實例中的子組件。顯然,Vue
在組件和組件之間創建了一層關聯。接下來的內容,咱們將探索如何創建組件之間的聯繫。
不論是父實例仍是子實例,在初始化實例階段有一個initLifecycle
的過程。這個過程會**把當前實例添加到父實例的$children
屬性中,並設置自身的$parent
屬性指向父實例。**舉一個具體的應用場景:
<div id="app">
<component-a></component-a>
</div>
Vue.component('component-a', {
template: '<div>a</div>'
})
var vm = new Vue({ el: '#app'})
console.log(vm) // 將實例對象輸出
複製代碼
因爲vue
實例向上沒有父實例,因此vm.$parent
爲undefined
,vm
的$children
屬性指向子組件componentA
的實例。
子組件componentA
的 $parent
屬性指向它的父級vm
實例,它的$children
屬性指向爲空
源碼解析以下:
function initLifecycle (vm) {
var options = vm.$options;
// 子組件註冊時,會把父組件的實例掛載到自身選項的parent上
var parent = options.parent;
// 若是是子組件,而且該組件不是抽象組件時,將該組件的實例添加到父組件的$parent屬性上,若是父組件是抽象組件,則一直往上層尋找,直到該父級組件不是抽象組件,並將,將該組件的實例添加到父組件的$parent屬性
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent;
}
parent.$children.push(vm);
}
// 將自身的$parent屬性指向父實例。
vm.$parent = parent;
vm.$root = parent ? parent.$root : vm;
vm.$children = [];
vm.$refs = {};
vm._watcher = null;
vm._inactive = null;
vm._directInactive = false;
// 該實例是否掛載
vm._isMounted = false;
// 該實例是否被銷燬
vm._isDestroyed = false;
// 該實例是否正在被銷燬
vm._isBeingDestroyed = false;
}
複製代碼
最後簡單講講抽象組件,在vue
中有不少內置的抽象組件,例如<keep-alive></keep-alive>,<slot><slot>
等,這些抽象組件並不會出如今子父級的路徑上,而且它們也不會參與DOM
的渲染。
這一小節,結合了實際的例子分析了組件註冊流程到組件掛載渲染流程,Vue
中咱們能夠定義全局的組件,也能夠定義局部的組件,全局組件須要進行全局註冊,核心方法是Vue.component
,他須要在根組件實例化前進行聲明註冊,緣由是咱們須要在實例化前拿到組件的配置信息併合併到options.components
選項中。註冊的本質是調用extend
建立一個子類構造器,全局和局部的不一樣是局部建立子類構造器是發生在建立子組件Vnode
階段。而建立子Vnode
階段最關鍵的一步是定義了不少內部使用的鉤子。有了一個完整的Vnode tree
接下來會進入真正DOM
的生成,在這個階段若是遇到子組件Vnode
會進行子構造器的實例化,並完成子組件的掛載。遞歸完成子組件的掛載後,最終才又回到根組件的掛載。 有了組件的基本知識,下一節咱們重點分析一下組件的進階用法。