feb-alive
網(wǎng)站建設(shè)哪家好,找成都創(chuàng)新互聯(lián)公司!專注于網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)站建設(shè)、微信開(kāi)發(fā)、微信小程序定制開(kāi)發(fā)、集團(tuán)企業(yè)網(wǎng)站建設(shè)等服務(wù)項(xiàng)目。為回饋新老客戶創(chuàng)新互聯(lián)還提供了北林免費(fèi)建站歡迎大家使用!
github地址
體驗(yàn)鏈接
使用理由
為什么開(kāi)發(fā)feb-laive?
當(dāng)我們通過(guò)Vue開(kāi)發(fā)項(xiàng)目時(shí)候,是否會(huì)有以下場(chǎng)景需求?
這個(gè)場(chǎng)景需求著重強(qiáng)調(diào)了緩存,緩存帶來(lái)的好處是,我上次頁(yè)面的數(shù)據(jù)及狀態(tài)都被保留,無(wú)需在從服務(wù)器拉取數(shù)據(jù),使用戶體驗(yàn)大大提高。
嘗試用keep-alive實(shí)現(xiàn)頁(yè)面緩存
so easy但是理想很完美,現(xiàn)實(shí)很殘酷
存在問(wèn)題
-/a跳到/b,再跳轉(zhuǎn)到/a 的時(shí)候,頁(yè)面中的數(shù)據(jù)是第一次訪問(wèn)的/a頁(yè)面,明明是鏈接跳轉(zhuǎn),確出現(xiàn)了緩存的效果,而我們期望的是像app一樣開(kāi)啟一個(gè)新的頁(yè)面。
舉個(gè)應(yīng)用場(chǎng)景
例如瀏覽文章頁(yè)面,依次訪問(wèn)3篇文章
當(dāng)我從/artical/3后退到/artical/2時(shí)候,由于組件緩存,此時(shí)頁(yè)面還是文章3的內(nèi)容,所以必須通過(guò)beforeRouteUpdate來(lái)重新拉取頁(yè)面2的數(shù)據(jù)。(注意此處后退不會(huì)觸發(fā)組件的activated鉤子,因?yàn)閮蓚€(gè)路由都渲染同個(gè)組件,所以實(shí)例會(huì)被復(fù)用,不會(huì)執(zhí)行reactivateComponent)
如果你想從/artical/3后退到/artical/2時(shí),同時(shí)想恢復(fù)之前在/artical/2中的一些狀態(tài),那么你還需要自己針對(duì)/artical/2中的所有狀態(tài)數(shù)據(jù)進(jìn)行存儲(chǔ)和恢復(fù)。
綜上:keep-alive實(shí)現(xiàn)的組件級(jí)別的緩存和我們想象中的緩存還是有差距的,keep-alive并不能滿足我們的需求。
==針對(duì)這些問(wèn)題,所以feb-alive插件誕生了==
由于feb-alive是基于keep-alive實(shí)現(xiàn)的,所以我們先簡(jiǎn)單分析一下keep-alive是如何實(shí)現(xiàn)緩存的
export default { name: 'keep-alive', abstract: true, props: { include: patternTypes, exclude: patternTypes, max: [String, Number] }, created () { this.cache = Object.create(null) this.keys = [] }, destroyed () { for (const key in this.cache) { pruneCacheEntry(this.cache, key, this.keys) } }, mounted () { this.$watch('include', val => { pruneCache(this, name => matches(val, name)) }) this.$watch('exclude', val => { pruneCache(this, name => !matches(val, name)) }) }, render () { // 獲取默認(rèn)插槽 const slot = this.$slots.default // 獲取第一個(gè)組件,也就和官方說(shuō)明的一樣,keep-alive要求同時(shí)只有一個(gè)子元素被渲染,如果你在其中有 v-for 則不會(huì)工作。 const vnode: VNode = getFirstComponentChild(slot) // 判斷是否存在組件選項(xiàng),也就是說(shuō)只對(duì)組件有效,對(duì)于普通的元素則直接返回對(duì)應(yīng)的vnode const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions if (componentOptions) { // 檢測(cè)include和exclude const name: ?string = getComponentName(componentOptions) const { include, exclude } = this if ( // not included (include && (!name || !matches(include, name))) || // excluded (exclude && name && matches(exclude, name)) ) { return vnode } const { cache, keys } = this // 如果指定了子組件的key則使用,否則通過(guò)cid+tag生成一個(gè)key const key: ?string = vnode.key == null ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') : vnode.key // 判斷是否存在緩存 if (cache[key]) { // 直接復(fù)用組件實(shí)例,并更新key的位置 vnode.componentInstance = cache[key].componentInstance remove(keys, key) keys.push(key) } else { // 此處存儲(chǔ)的vnode還沒(méi)有實(shí)例,在之后的流程中通過(guò)在createComponent中會(huì)生成實(shí)例 cache[key] = vnode keys.push(key) // 當(dāng)緩存數(shù)量大于閾值時(shí),刪除最早的key if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode) } } // 設(shè)置keepAlive屬性,createComponent中會(huì)判斷是否已經(jīng)生成組件實(shí)例,如果是且keepAlive為true則會(huì)觸發(fā)actived鉤子。 vnode.data.keepAlive = true } return vnode || (slot && slot[0]) } }
keep-alive是一個(gè)抽象組件,組件實(shí)例中維護(hù)了一份cache,也就是以下代碼部分
created () { // 存儲(chǔ)組件緩存 this.cache = Object.create(null) this.keys = [] }
由于路由切換并不會(huì)銷毀keep-alive組件,所以緩存是一直存在的(嵌套路由中,子路由外層的keep-alive情況會(huì)不一樣,后續(xù)會(huì)提到)
繼續(xù)看下keep-alive在緩存的存儲(chǔ)和讀取的具體實(shí)現(xiàn),先用一個(gè)簡(jiǎn)單的demo來(lái)描述keep-alive對(duì)于組件的緩存以及恢復(fù)緩存的過(guò)程
let Foo = { template: 'foo component', name: 'Foo' } let Bar = { template: ' ', name: 'Bar' } let gvm = new Vue({ el: '#app', template: ``, components: { Foo, Bar }, data: { renderCom: 'Foo' }, methods: { change () { this.renderCom = this.renderCom === 'Foo' ? 'Bar': 'Foo' } } })
上面例子中,根實(shí)例的template會(huì)被編譯成如下render函數(shù)
function anonymous( ) { with(this){return _c('div',{attrs:{"id":"#app"}},[_c('keep-alive',[_c(renderCom,{tag:"component"})],1),_c('button',{on:{"click":change}})],1)} }
可使用在線編譯:https://cn.vuejs.org/v2/guide/render-function.html#模板編譯
根據(jù)上面的render函數(shù)可以知道,vnode生成的過(guò)程是深度遞歸的,先創(chuàng)建子元素的vnode再創(chuàng)建父元素的vnode。
所以首次渲染的時(shí)候,在生成keep-alive組件vnode的時(shí)候,F(xiàn)oo組件的vnode已經(jīng)生成好了,并且作為keep-alive組件vnode構(gòu)造函數(shù)(_c)的參數(shù)傳入。
_c('keep-alive',[_c(renderCom,{tag:"component"})
生成的keep-alive組件的vnode如下
{ tag: 'vue-component-2-keep-alive', ... children: undefined, componentInstance: undefined, componentOptions: { Ctor: f VueComponent(options), children: [Vnode], listeners: undefined, propsData: {}, tag: 'keep-alive' }, context: Vue {...}, // 調(diào)用 $createElement/_c的組件實(shí)例, 此處是根組件實(shí)例對(duì)象 data: { hook: { init: f, prepatch: f, insert: f, destroy: f } } }
此處需要注意組件的vnode是沒(méi)有children的,而是將原本的children作為vnode的componentOptions的children屬性,componentOptions在組件實(shí)例化的時(shí)候會(huì)被用到,同時(shí)在初始化的時(shí)候componentOptions.children最終會(huì)賦值給vm.$slots,源碼部分如下
// createComponent函數(shù) function createComponent (Ctor, data, context, children, tag) { // 此處省略部分代碼 ... 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 }
Vue最后都會(huì)通過(guò)patch函數(shù)進(jìn)行渲染,將vnode轉(zhuǎn)換成真實(shí)的dom,對(duì)于組件則會(huì)通過(guò)createComponent進(jìn)行渲染
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { var i = vnode.data; if (isDef(i)) { var isReactivated = isDef(vnode.componentInstance) && i.keepAlive; if (isDef(i = i.hook) && isDef(i = i.init)) { i(vnode, false /* hydrating */); } // after calling the init hook, if the vnode is a child component // it should've created a child instance and mounted it. the child // component also has set the placeholder vnode's elm. // in that case we can just return the element and be done. if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue); insert(parentElm, vnode.elm, refElm); if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm); } return true } } }
接下去分兩步介紹
先講講本例中針對(duì)keep-alive組件本身的渲染
所以在執(zhí)行createElm(keepAliveVnode)的過(guò)程中會(huì)對(duì)keep-alive組件的實(shí)例化及掛載,而在實(shí)例化的過(guò)程中,keep-alive包裹的子組件的vnode會(huì)賦值給keep-alive組件實(shí)例的$slot屬性,所以在keep-alive實(shí)例調(diào)用render函數(shù)時(shí),可以通過(guò)this.$slot拿到包裹組件的vnode,在demo中,就是Foo組件的vnode,具體分析下keep-alive組件的render函數(shù)
render () { const slot = this.$slots.default const vnode: VNode = getFirstComponentChild(slot) const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions if (componentOptions) { const name: ?string = getComponentName(componentOptions) const { include, exclude } = this if ( // not included (include && (!name || !matches(include, name))) || // excluded (exclude && name && matches(exclude, name)) ) { return vnode } const { cache, keys } = this const key: ?string = vnode.key == null ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') : vnode.key if (cache[key]) { vnode.componentInstance = cache[key].componentInstance remove(keys, key) keys.push(key) } else { cache[key] = vnode keys.push(key) if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode) } } vnode.data.keepAlive = true } return vnode || (slot && slot[0]) }
上面分析到,在執(zhí)行createElm(keepAliveVnode)的過(guò)程中,會(huì)執(zhí)行keep-alive組件的實(shí)例化及掛載($mount),而在掛載的過(guò)程中,會(huì)執(zhí)行keep-alive的render函數(shù),之前分析過(guò),在render函數(shù)中,可以通過(guò)this.$slot獲取到子組件的vnode,從上面源碼中,可以知道,keep-alive只處理默認(rèn)插槽的第一個(gè)子組件,言外之意如果在keep-alive中包裹多個(gè)組件的話,剩下的組件會(huì)被忽略,例如:
// 只會(huì)渲染Foo組件
繼續(xù)分析,在拿到Foo組件vnode后,判斷了componentOptions,由于我們的Foo是一個(gè)組件,所以這里componentOptions是存在的,進(jìn)到if邏輯中,此處include 表示只有匹配的組件會(huì)被緩存,而 exclude 表示任何匹配的組件都不會(huì)被緩存,demo中并沒(méi)有設(shè)置相關(guān)規(guī)則,此處先忽略。
const { cache, keys } = this cache, keys是在keep-alive組件的create鉤子中生成的,用來(lái)存儲(chǔ)被keep-alive緩存的組件的實(shí)例以及對(duì)應(yīng)vnode的key created () { this.cache = Object.create(null) this.keys = [] }
繼續(xù)下面
const key: ?string = vnode.key == null ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') : vnode.key if (cache[key]) { vnode.componentInstance = cache[key].componentInstance remove(keys, key) keys.push(key) } else { cache[key] = vnode keys.push(key) if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode) } }
首先,取出vnode的key,如果vnode.key存在則使用vnode.key,不存在則用componentOptions.Ctor.cid + (componentOptions.tag ?::${componentOptions.tag}: '')
作為存儲(chǔ)組件實(shí)例的key,據(jù)此可以知道,如果我們不指定組件的key的話,對(duì)于相同的組件會(huì)匹配到同一個(gè)緩存,這也是為什么最開(kāi)始在描述keep-alive的時(shí)候強(qiáng)調(diào)它是一個(gè)組件級(jí)的緩存方案。
那么首次渲染的時(shí)候,cache和keys都是空的,這里就會(huì)走else邏輯
cache[key] = vnode keys.push(key) if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode) }
以key作為cache的健進(jìn)行存儲(chǔ)Foo組件vnode(注意此時(shí)vnode上面還沒(méi)有componentInstance),
這里利用了對(duì)象存儲(chǔ)的原理,之后進(jìn)行Foo組件實(shí)例化的時(shí)候會(huì)將其實(shí)例賦值給vnode.componentInstance,那么在下次keep-alive組件render的時(shí)候就可以獲取到vnode.componentInstance。
所以首次渲染僅僅是在keep-alive的cache上面,存儲(chǔ)了包裹組件Foo的vnode。
針對(duì)包裹組件的渲染
上面已經(jīng)講到執(zhí)行了keep-alive的render函數(shù),根據(jù)上面的源碼可以知道,render函數(shù)返回了Foo組件的vnode,那么在keep-alive執(zhí)行patch的時(shí)候,會(huì)創(chuàng)建Foo組件的實(shí)例,然后再進(jìn)行Foo組件的掛載,這個(gè)過(guò)程與普通組件并沒(méi)有區(qū)別,在此不累述。
當(dāng)組件從Foo切換到Bar時(shí)
本例中由于renderCom屬性的變化,會(huì)觸發(fā)根組件的renderWatcher,之后會(huì)執(zhí)行patch(oldVnode, vnode)
在進(jìn)行child vnode比較的時(shí)候,keep-alive的新老vnode比較會(huì)被判定為sameVnode,之后會(huì)進(jìn)入到patchVnode的邏輯
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) { if (oldVnode === vnode) { return } // 此處省略代碼 ... var i; var data = vnode.data; if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode); } // 此處省略代碼 ... }
由于我們的keep-alive是組件,所以在vnode創(chuàng)建的時(shí)候,會(huì)注入一些生命周期鉤子,其中就包含prepatch鉤子,其代碼如下
prepatch: function prepatch (oldVnode, vnode) { var options = vnode.componentOptions; var child = vnode.componentInstance = oldVnode.componentInstance; updateChildComponent( child, options.propsData, // updated props options.listeners, // updated listeners vnode, // new parent vnode options.children // new children ); }
由此可知,keep-alive組件的實(shí)例在此次根組件重渲染的過(guò)程中會(huì)復(fù)用,這也保證了keep-alive組件實(shí)例上面之前存儲(chǔ)cache還是存在的
var child = vnode.componentInstance = oldVnode.componentInstance;
下面的updateChildComponent這個(gè)函數(shù)非常關(guān)鍵,這個(gè)函數(shù)擔(dān)任了Foo組件切換到Bar組件的關(guān)鍵任務(wù)。我們知道,由于keep-alive組件是在此處是復(fù)用的,所以不會(huì)再觸發(fā)initRender,所以vm.$slot不會(huì)再次更新。所以在updateChildComponent函數(shù)擔(dān)起了slot更新的重任
function updateChildComponent ( vm, propsData, listeners, parentVnode, renderChildren ) { if (process.env.NODE_ENV !== 'production') { isUpdatingChildComponent = true; } // determine whether component has slot children // we need to do this before overwriting $options._renderChildren var hasChildren = !!( renderChildren || // has new static slots vm.$options._renderChildren || // has old static slots parentVnode.data.scopedSlots || // has new scoped slots vm.$scopedSlots !== emptyObject // has old scoped slots ); // ... // resolve slots + force update if has children if (hasChildren) { vm.$slots = resolveSlots(renderChildren, parentVnode.context); vm.$forceUpdate(); } if (process.env.NODE_ENV !== 'production') { isUpdatingChildComponent = false; } }
updateChildComponent函數(shù)主要更新了當(dāng)前組件實(shí)例上的一些屬性,這里包括props,listeners,slots。我們著重講一下slots更新,這里通過(guò)resolveSlots獲取到最新的包裹組件的vnode,也就是demo中的Bar組件,之后通過(guò)vm.$forceUpdate強(qiáng)制keep-alive組件進(jìn)行重新渲染。(小提示:當(dāng)我們的組件有插槽的時(shí)候,該組件的父組件re-render時(shí)會(huì)觸發(fā)該組件實(shí)例$fourceUpdate,這里會(huì)有性能損耗,因?yàn)椴还軘?shù)據(jù)變動(dòng)是否對(duì)slot有影響,都會(huì)觸發(fā)強(qiáng)制更新,根據(jù)vueConf上尤大的介紹,此問(wèn)題在3.0會(huì)被優(yōu)化),例如
// Home.vue
此例中當(dāng)Home組件更新的時(shí)候,會(huì)觸發(fā)Artical組件的強(qiáng)制刷新,而這種刷新是多余的。
繼續(xù),在更新了keep-alive實(shí)例的forceUpdate,之后再次進(jìn)入到keep-alive的render函數(shù)中
render () { const slot = this.$slots.default const vnode: VNode = getFirstComponentChild(slot) // ... }
此時(shí)render函數(shù)中獲取到vnode就是Bar組件的vnode,接下去的流程和Foo渲染一樣,只不過(guò)也是把Bar組件的vnode緩存到keep-alive實(shí)例的cache對(duì)象中。
當(dāng)組件從Bar再次切換到Foo時(shí)
針對(duì)keep-alive組件邏輯還是和上面講述的一樣
再次進(jìn)入到render函數(shù),這時(shí)候cache[key]就會(huì)匹配到Foo組件首次渲染時(shí)候緩存的vnode了,看下這部分邏輯
const key: ?string = vnode.key == null ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') : vnode.key if (cache[key]) { vnode.componentInstance = cache[key].componentInstance remove(keys, key) keys.push(key) } else { cache[key] = vnode keys.push(key) if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode) } }
由于keep-alive包裹的組件是Foo組件,根據(jù)規(guī)則,此時(shí)生成的key和第一此渲染Foo組件時(shí)生成的key是一樣的,所以本次keep-alive的render函數(shù)進(jìn)入到了第一個(gè)if分支,也就是匹配到了cache[key],把緩存的componentInstance賦值給當(dāng)前vnode,然后更新keys(當(dāng)存在max的時(shí)候,能夠保證被刪除的是比較老的緩存)。
很多同學(xué)可能會(huì)問(wèn),這里設(shè)置vnode.componentInstance會(huì)有什么作用。這里涉及到vue的源碼部分。
由于是從Bar組件切換到Foo組件,所以在patch的時(shí)候,比對(duì)到此處,并不會(huì)被判定為sameVnode,所以自然而然走到createElm,由于Foo是Vue組件,所以會(huì)進(jìn)入到createComponent,所以最終進(jìn)入到下面函數(shù)片段
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { var i = vnode.data; if (isDef(i)) { var isReactivated = isDef(vnode.componentInstance) && i.keepAlive; if (isDef(i = i.hook) && isDef(i = i.init)) { i(vnode, false /* hydrating */); } // after calling the init hook, if the vnode is a child component // it should've created a child instance and mounted it. the child // component also has set the placeholder vnode's elm. // in that case we can just return the element and be done. if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue); insert(parentElm, vnode.elm, refElm); if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm); } return true } } }
可以根據(jù)上面對(duì)于keep-alive源碼的分析,此處isReactivated為true,接下去會(huì)進(jìn)入到vnode生成的時(shí)候掛在的生命周期init函數(shù)
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 { var child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance ); child.$mount(hydrating ? vnode.elm : undefined, hydrating); } }, prepatch: function prepatch (oldVnode, vnode) { var options = vnode.componentOptions; var child = vnode.componentInstance = oldVnode.componentInstance; updateChildComponent( child, options.propsData, // updated props options.listeners, // updated listeners vnode, // new parent vnode options.children // new children ); }, ... }
此時(shí)由于實(shí)例已經(jīng)存在,且keepAlive為true,所以會(huì)走第一個(gè)if邏輯,會(huì)執(zhí)行prepatch,更新組件屬性及一些監(jiān)聽(tīng)器,如果存在插槽的話,還會(huì)更新插槽,并執(zhí)行$forceUpdate,此處在前面已經(jīng)分析過(guò),不做累述。
繼續(xù)createComponent,在函數(shù)內(nèi)部會(huì)執(zhí)行initComponent和insert
if (isDef(vnode.componentInstance)) { // 將實(shí)例上的dom賦值給vnode initComponent(vnode, insertedVnodeQueue); // 插入dom insert(parentElm, vnode.elm, refElm); if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm); } return true }
至此,當(dāng)組件從Bar再次切換到Foo時(shí),實(shí)例與dom都得到了復(fù)用,達(dá)到一個(gè)很高的體驗(yàn)效果!而我們之后要實(shí)現(xiàn)的feb-alive就是基于keep-alive實(shí)現(xiàn)的。
Vue頁(yè)面級(jí)緩存解決方案feb-alive (下)
參考文檔
vue-navigation
Vue.js 技術(shù)揭秘
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持創(chuàng)新互聯(lián)。