前面花了兩節的內容介紹了組件,從組件的原理講到組件的應用,包括異步組件和函數式組件的實現和使用場景。衆所周知,組件是貫穿整個Vue設計理念的東西,而且也是指導咱們開發的核心思想,因此接下來的幾篇文章,將從新回到組件的內容去作源碼分析,首先會從經常使用的動態組件開始,包括內聯模板的原理,最後會簡單的提到內置組件的概念,爲以後的文章埋下伏筆。html
動態組件我相信大部分在開發的過程當中都會用到,當咱們須要在不一樣的組件之間進行狀態切換時,動態組件能夠很好的知足咱們的需求,其中的核心是component
標籤和is
屬性的使用。vue
例子是一個動態組件的基本使用場景,當點擊按鈕時,視圖根據this.chooseTabs
值在組件child1,child2,child3
間切換。node
// vue
<div id="app">
<button @click="changeTabs('child1')">child1</button>
<button @click="changeTabs('child2')">child2</button>
<button @click="changeTabs('child3')">child3</button>
<component :is="chooseTabs">
</component>
</div>
// js
var child1 = {
template: '<div>content1</div>',
}
var child2 = {
template: '<div>content2</div>'
}
var child3 = {
template: '<div>content3</div>'
}
var vm = new Vue({
el: '#app',
components: {
child1,
child2,
child3
},
methods: {
changeTabs(tab) {
this.chooseTabs = tab;
}
}
})
複製代碼
<component>
的解讀和前面幾篇內容一致,會從AST
解析階段提及,過程也不會專一每個細節,而是把和以往處理方式不一樣的地方特別說明。針對動態組件解析的差別,集中在processComponent
上,因爲標籤上is
屬性的存在,它會在最終的ast
樹上打上component
屬性的標誌。算法
// 針對動態組件的解析
function processComponent (el) {
var binding;
// 拿到is屬性所對應的值
if ((binding = getBindingAttr(el, 'is'))) {
// ast樹上多了component的屬性
el.component = binding;
}
if (getAndRemoveAttr(el, 'inline-template') != null) {
el.inlineTemplate = true;
}
}
複製代碼
最終的ast
樹以下:bash
有了ast
樹,接下來是根據ast
樹生成可執行的render
函數,因爲有component
屬性,render
函數的產生過程會走genComponent
分支。app
// render函數生成函數
var code = generate(ast, options);
// generate函數的實現
function generate (ast,options) {
var state = new CodegenState(options);
var code = ast ? genElement(ast, state) : '_c("div")';
return {
render: ("with(this){return " + code + "}"),
staticRenderFns: state.staticRenderFns
}
}
function genElement(el, state) {
···
var code;
// 動態組件分支
if (el.component) {
code = genComponent(el.component, el, state);
}
}
複製代碼
針對動態組件的處理邏輯其實很簡單,當沒有內聯模板標誌時(後面會講),拿到後續的子節點進行拼接,和普通組件惟一的區別在於,_c
的第一個參數再也不是一個指定的字符串,而是一個表明組件的變量。異步
// 針對動態組件的處理
function genComponent (
componentName,
el,
state
) {
// 擁有inlineTemplate屬性時,children爲null
var children = el.inlineTemplate ? null : genChildren(el, state, true);
return ("_c(" + componentName + "," + (genData$2(el, state)) + (children ? ("," + children) : '') + ")")
}
複製代碼
其實咱們能夠對比普通組件和動態組件在render
函數上的區別,結果一目瞭然。函數
"with(this){return _c('div',{attrs:{"id":"app"}},[_c('child1',[_v(_s(test))])],1)}"
源碼分析
"with(this){return _c('div',{attrs:{"id":"app"}},[_c(chooseTabs,{tag:"component"})],1)}"
post
簡單的總結,動態組件和普通組件的區別在於:
ast
階段新增了component
屬性,這是動態組件的標誌render
函數階段因爲component
屬性的存在,會執行genComponent
分支,genComponent
會針對動態組件的執行函數進行特殊的處理,和普通組件不一樣的是,_c
的第一個參數再也不是不變的字符串,而是指定的組件名變量。render
到vnode
階段和普通組件的流程相同,只是字符串換成了變量,並有{ tag: 'component' }
的data
屬性。例子中chooseTabs
此時取的是child1
。有了render函數,接下來從vnode到真實節點的過程和普通組件在流程和思路上基本一致,這一階段能夠回顧以前介紹組件流程的分析
因爲本身對源碼的理解還不夠透徹,讀了動態組件的建立流程以後,心中產生了一個疑問,從原理的過程分析,動態組件的核心實際上是is
這個關鍵字,它在編譯階段就以component
屬性將該組件定義爲動態組件,而component
做爲標籤好像並無特別大的用途,只要有is
關鍵字的存在,組件標籤名設置爲任意自定義標籤均可以達到動態組件的效果?(componenta, componentb
)。這個字符串僅以{ tag: 'component' }
的形式存在於vnode
的data
屬性存在。那是否是說明,所謂動態組件只是因爲is
的單方面限制?那component
標籤的意義又在哪裏?(求教大佬!!)
因爲動態組件除了有is
做爲傳值外,還能夠有inline-template
做爲配置,藉此前提,恰好能夠理清楚Vue
中內聯模板的原理和設計思想。Vue
在官網有一句醒目的話,提示咱們inline-template
會讓模板的做用域變得更加難以理解。所以建議儘可能使用template
選項來定義模板,而不是用內聯模板的形式。接下來,咱們經過源碼去定位一下所謂做用域難以理解的緣由。
咱們先簡單調整上面的例子,從使用角度上入手:
// html
<div id="app">
<button @click="changeTabs('child1')">child1</button>
<button @click="changeTabs('child2')">child2</button>
<button @click="changeTabs('child3')">child3</button>
<component :is="chooseTabs" inline-template>
<span>{{test}}</span>
</component>
</div>
// js
var child1 = {
data() {
return {
test: 'content1'
}
}
}
var child2 = {
data() {
return {
test: 'content2'
}
}
}
var child3 = {
data() {
return {
test: 'content3'
}
}
}
var vm = new Vue({
el: '#app',
components: {
child1,
child2,
child3
},
data() {
return {
chooseTabs: 'child1',
}
},
methods: {
changeTabs(tab) {
this.chooseTabs = tab;
}
}
})
複製代碼
例子中達到的效果和文章第一個例子一致,很明顯和以往認知最大的差別在於,父組件裏的環境能夠訪問到子組件內部的環境變量。初看以爲挺難以想象的。咱們回憶一下以前父組件能訪問到子組件的情形,從大的方向上有兩個:
- 1. 採用事件機制,子組件經過$emit
事件,將子組件的狀態告知父組件,達到父訪問子的目的。 - 2. 利用做用域插槽的方式,將子的變量經過props
的形式傳遞給父,而父經過v-slot
的語法糖去接收,而咱們以前分析的結果是,這種方式本質上仍是經過事件派發的形式去通知父組件。
以前分析過程也有提過父組件沒法訪問到子環境的變量,其核心的緣由在於: 父級模板裏的全部內容都是在父級做用域中編譯的;子模板裏的全部內容都是在子做用域中編譯的。 那麼咱們有理由猜測,內聯模板是否是違背了這一原則,讓父的內容放到了子組件建立過程去編譯呢?咱們接着往下看:
回到ast
解析階段,前面分析到,針對動態組件的解析,關鍵在於processComponent
函數對is
屬性的處理,其中還有一個關鍵是對inline-template
的處理,它會在ast
樹上增長inlineTemplate
屬性。
// 針對動態組件的解析
function processComponent (el) {
var binding;
// 拿到is屬性所對應的值
if ((binding = getBindingAttr(el, 'is'))) {
// ast樹上多了component的屬性
el.component = binding;
}
// 添加inlineTemplate屬性
if (getAndRemoveAttr(el, 'inline-template') != null) {
el.inlineTemplate = true;
}
}
複製代碼
render
函數生成階段因爲inlineTemplate
的存在,父的render
函數的子節點爲null
,這一步也決定了inline-template
下的模板並非在父組件階段編譯的,那模板是如何傳遞到子組件的編譯過程呢?答案是模板以屬性的形式存在,待到子實例時拿到屬性值
function genComponent (componentName,el,state) {
// 擁有inlineTemplate屬性時,children爲null
var children = el.inlineTemplate ? null : genChildren(el, state, true);
return ("_c(" + componentName + "," + (genData$2(el, state)) + (children ? ("," + children) : '') + ")")
}
複製代碼
咱們看看最終render
函數的結果,其中模板以{render: function(){···}}
的形式存在於父組件的inlineTemplate
屬性中。
"_c('div',{attrs:{"id":"app"}},[_c(chooseTabs,{tag:"component",inlineTemplate:{render:function(){with(this){return _c('span',[_v(_s(test))])}},staticRenderFns:[]}})],1)"
最終vnode
結果也顯示,inlineTemplate
對象會保留在父組件的data
屬性中。
// vnode結果
{
data: {
inlineTemplate: {
render: function() {}
},
tag: 'component'
},
tag: "vue-component-1-child1"
}
複製代碼
有了vnode
後,來到了關鍵的最後一步,根據vnode
生成真實節點的過程。從根節點開始,遇到vue-component-1-child1
,會經歷實例化建立子組件的過程,實例化子組件前會先對inlineTemplate
屬性進行處理。
function createComponentInstanceForVnode (vnode,parent) {
// 子組件的默認選項
var options = {
_isComponent: true,
_parentVnode: vnode,
parent: parent
};
var inlineTemplate = vnode.data.inlineTemplate;
// 內聯模板的處理,分別拿到render函數和staticRenderFns
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render;
options.staticRenderFns = inlineTemplate.staticRenderFns;
}
// 執行vue子組件實例化
return new vnode.componentOptions.Ctor(options)
}
複製代碼
子組件的默認選項配置會根據vnode
上的inlineTemplate
屬性拿到模板的render
函數。分析到這一步結論已經很清楚了。內聯模板的內容最終會在子組件中解析,因此模板中能夠拿到子組件的做用域這個現象也不足爲奇了。
最後說說Vue
思想中的另外一個概念,內置組件,其實vue
的官方文檔有對內置組件進行了列舉,分別是component, transition, transition-group, keep-alive, slot
,其中<slot>
咱們在插槽這一節已經詳細介紹過,而component
的使用這一節也花了大量的篇幅從使用到原理進行了分析。然而學習了slot,component
以後,我開始意識到slot
和component
並非真正的內置組件。**內置組件是已經在源碼初始化階段就全局註冊好的組件。**而<slot>
和<component>
並無被當成一個組件去處理,所以也沒有組件的生命週期。slot
只會在render
函數階段轉換成renderSlot
函數進行處理,而component
也只是藉助is
屬性將createElement
的第一個參數從字符串轉換爲變量,僅此而已。所以從新回到概念的理解,**內置組件是源碼自身提供的組件,**因此這一部份內容的重點,會放在內置組件是何時註冊的,編譯時有哪些不一樣這兩個問題上來。這一部分只是一個拋磚引玉,接下來會有兩篇文章專門詳細介紹keep-alive,transition, transition-group
的實現原理。
Vue
初始化階段會在構造器的components
屬性添加三個組件對象,每一個組件對象的寫法和咱們在自定義組件過程的寫法一致,有render
函數,有生命週期,也會定義各類數據。
// keep-alive組件選項
var KeepAlive = {
render: function() {}
}
// transition 組件選項
var Transition = {
render: function() {}
}
// transition-group 組件選項
var TransitionGroup = {
render: function() {},
methods: {},
···
}
var builtInComponents = {
KeepAlive: KeepAlive
};
var platformComponents = {
Transition: Transition,
TransitionGroup: TransitionGroup
};
// Vue構造器的選項配置,compoents選項合併
extend(Vue.options.components, builtInComponents);
extend(Vue.options.components, platformComponents);
複製代碼
extend
方法咱們在系列的開頭,分析選項合併的時候有說過,將對象上的屬性合併到源對象中,屬性相同則覆蓋。
// 將_from對象合併到to對象,屬性相同時,則覆蓋to對象的屬性
function extend (to, _from) {
for (var key in _from) {
to[key] = _from[key];
}
return to
}
複製代碼
最終Vue
構造器擁有了三個組件的配置選項。
Vue.components = {
keepAlive: {},
transition: {},
transition-group: {},
}
複製代碼
僅僅有定義是不夠的。組件須要被全局使用還得進行全局的註冊,這其實在深刻剖析Vue源碼 - 選項合併(下)已經闡述清楚了。Vue實例在初始化過程當中,最重要的第一步是進行選項的合併,而像內置組件這些資源類選項會有專門的選項合併策略,最終構造器上的組件選項會以原型鏈的形式註冊到實例的compoonents
選項中(指令和過濾器同理)。
// 資源選項
var ASSET_TYPES = [
'component',
'directive',
'filter'
];
// 定義資源合併的策略
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets; // 定義默認策略
});
function mergeAssets (parentVal,childVal,vm,key) {
var res = Object.create(parentVal || null); // 以parentVal爲原型建立一個空對象
if (childVal) {
assertObjectType(key, childVal, vm); // components,filters,directives選項必須爲對象
return extend(res, childVal) // 子類選項賦值給空對象
} else {
return res
}
}
複製代碼
關鍵的兩步一個是var res = Object.create(parentVal || null);
,它會以parentVal
爲原型建立一個空對象,最後是經過extend
將用戶自定義的component
選項複製到空對象中。選項合併以後,內置組件也所以在全局完成了註冊。
{
components: {
child1,
__proto__: {
keepAlive: {},
transition: {},
transitionGroup: {}
}
}
}
複製代碼
最後咱們看看內置組件對象中並無template
模板,而是render
函數,除了減小了耗性能的模板解析過程,我認爲重要的緣由是內置組件並無渲染的實體。最後的最後,讓咱們一塊兒期待後續對keep-alive
和transition
的原理分析,敬請期待。