上一節最後稍微提到了
Vue
內置組件的相關內容,從這一節開始,將會對某個具體的內置組件進行分析。首先是keep-alive
,它是咱們平常開發中常用的組件,咱們在不一樣組件間切換時,常常要求保持組件的狀態,以免重複渲染組件形成的性能損耗,而keep-alive
常常和上一節介紹的動態組件結合起來使用。因爲內容過多,keep-alive
的源碼分析將分爲上下兩部分,這一節主要圍繞keep-alive
的首次渲染展開。vue
keep-alive
的使用只須要在動態組件的最外層添加標籤便可。node
<div id="app">
<button @click="changeTabs('child1')">child1</button>
<button @click="changeTabs('child2')">child2</button>
<keep-alive>
<component :is="chooseTabs">
</component>
</keep-alive>
</div>
var child1 = {
template: '<div><button @click="add">add</button><p>{{num}}</p></div>',
data() {
return {
num: 1
}
},
methods: {
add() {
this.num++
}
},
}
var child2 = {
template: '<div>child2</div>'
}
var vm = new Vue({
el: '#app',
components: {
child1,
child2,
},
data() {
return {
chooseTabs: 'child1',
}
},
methods: {
changeTabs(tab) {
this.chooseTabs = tab;
}
}
})
複製代碼
簡單的結果以下,動態組件在child1,child2
之間來回切換,當第二次切到child1
時,child1
保留着原來的數據狀態,num = 5
。react
按照以往分析的經驗,咱們會從模板的解析開始提及,第一個疑問即是:內置組件和普通組件在編譯過程有區別嗎?答案是沒有的,不論是內置的仍是用戶定義組件,本質上組件在模板編譯成render
函數的處理方式是一致的,這裏的細節不展開分析,有疑惑的能夠參考前幾節的原理分析。最終針對keep-alive
的render
函數的結果以下:算法
with(this){···_c('keep-alive',{attrs:{"include":"child2"}},[_c(chooseTabs,{tag:"component"})],1)}
api
有了render
函數,接下來從子開始到父會執行生成Vnode
對象的過程,_c('keep-alive'···)
的處理,會執行createElement
生成組件Vnode
,其中因爲keep-alive
是組件,因此會調用createComponent
函數去建立子組件Vnode
,createComponent
以前也有分析過,這個環節和建立普通組件Vnode
不一樣之處在於,keep-alive
的Vnode
會剔除多餘的屬性內容,因爲keep-alive
除了slot
屬性以外,其餘屬性在組件內部並無意義,例如class
樣式,<keep-alive clas="test"></keep-alive>
等,因此在Vnode
層剔除掉多餘的屬性是有意義的。而<keep-alive slot="test">
的寫法在2.6以上的版本也已經被廢棄。(其中abstract
做爲抽象組件的標誌,以及其做用咱們後面會講到)數組
// 建立子組件Vnode過程
function createComponent(Ctordata,context,children,tag) {
// abstract是內置組件(抽象組件)的標誌
if (isTrue(Ctor.options.abstract)) {
// 只保留slot屬性,其餘標籤屬性都被移除,在vnode對象上再也不存在
var slot = data.slot;
data = {};
if (slot) {
data.slot = slot;
}
}
}
複製代碼
keep-alive
之因此特別,是由於它不會重複渲染相同的組件,只會利用初次渲染保留的緩存去更新節點。因此爲了全面瞭解它的實現原理,咱們須要從keep-alive
的首次渲染開始提及。緩存
爲了理清楚流程,我大體畫了一個流程圖,流程圖大體覆蓋了初始渲染keep-alive
所執行的過程,接下來會照着這個過程進行源碼分析。bash
和渲染普通組件相同的是,Vue
會拿到前面生成的Vnode
對象執行真實節點建立的過程,也就是熟悉的patch
過程,patch
執行階段會調用createElm
建立真實dom
,在建立節點途中,keep-alive
的vnode
對象會被認定是一個組件Vnode
,所以針對組件Vnode
又會執行createComponent
函數,它會對keep-alive
組件進行初始化和實例化。app
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
var i = vnode.data;
if (isDef(i)) {
// isReactivated用來判斷組件是否緩存。
var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
if (isDef(i = i.hook) && isDef(i = i.init)) {
// 執行組件初始化的內部鉤子 init
i(vnode, false /* hydrating */);
}
if (isDef(vnode.componentInstance)) {
// 其中一個做用是保留真實dom到vnode中
initComponent(vnode, insertedVnodeQueue);
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
}
}
複製代碼
keep-alive
組件會先調用內部鉤子init
方法進行初始化操做,咱們先看看init
過程作了什麼操做。dom
// 組件內部鉤子
var componentVNodeHooks = {
init: function init (vnode, hydrating) {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
var mountedNode = vnode; // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode);
} else {
// 將組件實例賦值給vnode的componentInstance屬性
var child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
);
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
}
},
// 後面分析
prepatch: function() {}
}
複製代碼
第一次執行,很明顯組件vnode
沒有componentInstance
屬性,vnode.data.keepAlive
也沒有值,因此會調用createComponentInstanceForVnode
方法進行組件實例化並將組件實例賦值給vnode
的componentInstance
屬性, 最終執行組件實例的$mount
方法進行實例掛載。
createComponentInstanceForVnode
就是組件實例化的過程,而組件實例化從系列的第一篇就開始說了,無非就是一系列選項合併,初始化事件,生命週期等初始化操做。
function createComponentInstanceForVnode (vnode, parent) {
var options = {
_isComponent: true,
_parentVnode: vnode,
parent: parent
};
// 內聯模板的處理,忽略這部分代碼
···
// 執行vue子組件實例化
return new vnode.componentOptions.Ctor(options)
}
複製代碼
咱們在使用組件的時候常常利用對象的形式定義組件選項,包括data,method,computed
等,並在父組件或根組件中註冊。keep-alive
一樣遵循這個道理,內置兩字也說明了keep-alive
是在Vue
源碼中內置好的選項配置,而且也已經註冊到全局,這一部分的源碼能夠參考深刻剖析Vue源碼 - Vue動態組件的概念,你會亂嗎?小節末尾對內置組件構造器和註冊過程的介紹。這一部分咱們重點關注一下keep-alive
的具體選項。
// keepalive組件選項
var KeepAlive = {
name: 'keep-alive',
// 抽象組件的標誌
abstract: true,
// keep-alive容許使用的props
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created: function created () {
// 緩存組件vnode
this.cache = Object.create(null);
// 緩存組件名
this.keys = [];
},
destroyed: function destroyed () {
for (var key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys);
}
},
mounted: function mounted () {
var this$1 = this;
// 動態include和exclude
// 對include exclue的監聽
this.$watch('include', function (val) {
pruneCache(this$1, function (name) { return matches(val, name); });
});
this.$watch('exclude', function (val) {
pruneCache(this$1, function (name) { return !matches(val, name); });
});
},
// keep-alive的渲染函數
render: function render () {
// 拿到keep-alive下插槽的值
var slot = this.$slots.default;
// 第一個vnode節點
var vnode = getFirstComponentChild(slot);
// 拿到第一個組件實例
var componentOptions = vnode && vnode.componentOptions;
// keep-alive的第一個子組件實例存在
if (componentOptions) {
// check pattern
//拿到第一個vnode節點的name
var name = getComponentName(componentOptions);
var ref = this;
var include = ref.include;
var exclude = ref.exclude;
// 經過判斷子組件是否知足緩存匹配
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
var ref$1 = this;
var cache = ref$1.cache;
var keys = ref$1.keys;
var key = vnode.key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? ("::" + (componentOptions.tag)) : '')
: vnode.key;
// 再次命中緩存
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance;
// make current key freshest
remove(keys, key);
keys.push(key);
} else {
// 初次渲染時,將vnode緩存
cache[key] = vnode;
keys.push(key);
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
// 爲緩存組件打上標誌
vnode.data.keepAlive = true;
}
// 將渲染的vnode返回
return vnode || (slot && slot[0])
}
};
複製代碼
keep-alive
選項跟咱們平時寫的組件選項仍是基本相似的,惟一的不一樣是keep-ailve
組件沒有用template
而是使用render
函數。keep-alive
本質上只是存緩存和拿緩存的過程,並無實際的節點渲染,因此使用render
處理是最優的選擇。
仍是先回到流程圖的分析。上面說到keep-alive
在執行組件實例化以後會進行組件的掛載。而掛載$mount
又回到vm._render(),vm._update()
的過程。因爲keep-alive
擁有render
函數,因此咱們能夠直接將焦點放在render
函數的實現上。
keep-alive
下插槽的內容,也就是keep-alive
須要渲染的子組件,例子中是chil1 Vnode
對象,源碼中對應getFirstComponentChild
函數function getFirstComponentChild (children) {
if (Array.isArray(children)) {
for (var i = 0; i < children.length; i++) {
var c = children[i];
// 組件實例存在,則返回,理論上返回第一個組件vnode
if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
return c
}
}
}
}
複製代碼
keep-alive
組件的使用過程當中,Vue
源碼容許咱們是用include, exclude
來定義匹配條件,include
規定了只有名稱匹配的組件纔會被緩存,exclude
規定了任何名稱匹配的組件都不會被緩存。更者,咱們可使用max
來限制能夠緩存多少匹配實例,而爲何要作數量的限制呢?咱們後文會提到。拿到子組件的實例後,咱們須要先進行是否知足匹配條件的判斷,其中匹配的規則容許使用數組,字符串,正則的形式。
var include = ref.include;
var exclude = ref.exclude;
// 經過判斷子組件是否知足緩存匹配
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
// matches
function matches (pattern, name) {
// 容許使用數組['child1', 'child2']
if (Array.isArray(pattern)) {
return pattern.indexOf(name) > -1
} else if (typeof pattern === 'string') {
// 容許使用字符串 child1,child2
return pattern.split(',').indexOf(name) > -1
} else if (isRegExp(pattern)) {
// 容許使用正則 /^child{1,2}$/g
return pattern.test(name)
}
/* istanbul ignore next */
return false
}
複製代碼
若是組件不知足緩存的要求,則直接返回組件的vnode
,不作任何處理,此時組件會進入正常的掛載環節。
render
函數執行的關鍵一步是緩存vnode
,因爲是第一次執行render
函數,選項中的cache
和keys
數據都沒有值,其中cache
是一個空對象,咱們將用它來緩存{ name: vnode }
枚舉,而keys
咱們用來緩存組件名。 所以咱們在第一次渲染keep-alive
時,會將須要渲染的子組件vnode
進行緩存。cache[key] = vnode;
keys.push(key);
複製代碼
vnode
打上標記, 並將子組件的Vnode
返回。 vnode.data.keepAlive = true
咱們再回到createComponent
的邏輯,以前提到createComponent
會先執行keep-alive
組件的初始化流程,也包括了子組件的掛載。而且咱們經過componentInstance
拿到了keep-alive
組件的實例,而接下來重要的一步是將真實的dom
保存再vnode
中。
function createComponent(vnode, insertedVnodeQueue) {
···
if (isDef(vnode.componentInstance)) {
// 其中一個做用是保留真實dom到vnode中
initComponent(vnode, insertedVnodeQueue);
// 將真實節點添加到父節點中
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
}
複製代碼
insert
的源碼不列舉出來,它只是簡單的調用操做dom
的api
,將子節點插入到父節點中,咱們能夠重點看看initComponent
關鍵步驟的邏輯。
function initComponent() {
···
// vnode保留真實節點
vnode.elm = vnode.componentInstance.$el;
···
}
複製代碼
所以,咱們很清晰的回到以前遺留下來的問題,爲何keep-alive
須要一個max
來限制緩存組件的數量。緣由就是keep-alive
緩存的組件數據除了包括vnode
這一描述對象外,還保留着真實的dom
節點,而咱們知道真實節點對象是龐大的,因此大量保留緩存組件是耗費性能的。所以咱們須要嚴格控制緩存的組件數量,而在緩存策略上也須要作優化,這點咱們在下一篇文章也繼續提到。
因爲isReactivated
爲false
,reactivateComponent
函數也不會執行。至此keep-alive
的初次渲染流程分析完畢。
若是忽略步驟的分析,只對初次渲染流程作一個總結:內置的keep-alive
組件,讓子組件在第一次渲染的時候將vnode
和真實的elm
進行了緩存。
這一節的最後順便提一下上文提到的抽象組件的概念。Vue
提供的內置組件都有一個描述組件類型的選項,這個選項就是{ astract: true }
,它代表了該組件是抽象組件。什麼是抽象組件,爲何要有這一類型的區別呢?我以爲歸根究底有兩個方面的緣由。
dom
節點,而只是做爲中間的數據過渡層處理,在keep-alive
中是對組件緩存的處理。initLifecycle
的代碼。Vue.prototype._init = function() {
···
var vm = this;
initLifecycle(vm)
}
function initLifecycle (vm) {
var options = vm.$options;
var parent = options.parent;
if (parent && !options.abstract) {
// 若是有abstract屬性,一直往上層尋找,直到不是抽象組件
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent;
}
parent.$children.push(vm);
}
···
}
複製代碼
子組件在註冊階段會把父實例掛載到自身選項的parent
屬性上,在initLifecycle
過程當中,會反向拿到parent
上的父組件vnode
,併爲其$children
屬性添加該子組件vnode
,若是在反向找父組件的過程當中,父組件擁有abstract
屬性,便可斷定該組件爲抽象組件,此時利用parent
的鏈條往上尋找,直到組件不是抽象組件爲止。initLifecycle
的處理,讓每一個組件都能找到上層的父組件以及下層的子組件,使得組件之間造成一個緊密的關係樹。
有了第一次的緩存處理,當第二次渲染組件時,
keep-alive
又會有哪些魔法的存在呢,以前留下的緩存優化又是什麼?這些都會在下一小節一一解開。