vue單頁快取實現方案分析keep-alive
behind
vue單頁快取實現方案分析
實現前進重新整理,返回不重新整理的功能,並且返回時可以記住上一頁的滾動位置,有兩套方案可選
方案一:vue的keep-alive元件
- 具體使用如下:
<keep-alive max="10"> <router-view/> </keep-alive>
- 為什麼這麼使用?
如vue官網(https://cn.vuejs.org/v2/api/#keep-alive)介紹:
<keep-alive> 包裹動態元件時,會快取不活動的元件例項,而不是銷燬它們。和 <transition> 相似,<keep-alive> 是一個抽象元件:它自身不會渲染一個 DOM 元素,也不會出現在父元件鏈中。
當元件在 <keep-alive> 內被切換,它的 activated 和 deactivated 這兩個生命週期鉤子函式將會被對應執行。主要用於保留元件狀態或避免重新渲染。
因為快取的需要通常出現在切換頁面時,所以就需要結合vue-router的router-view來實現
- 為什麼keep-alive能實現快取?
render () { const slot = this.$slots.default const vnode: VNode = getFirstComponentChild(slot) const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions if (componentOptions) { // check pattern 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 // same constructor may get registered as different local components // so cid alone is not enough (#3269) ? 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 { 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 } return vnode || (slot && slot[0]) }
如上keep-alive原始碼,其中render函式是這樣實現的,要渲染的試圖元件作為插槽內容被獲取到,當渲染到路徑匹配到的檢視元件時會根據vnode儲存的內容拿到對應的name,一次將這些元件例項放到變數cache中,這樣根據路由就可以找到快取的vnode,返回給createComponent方法去執行
initComponent,vue元件渲染這塊的程式碼如下
function initComponent (vnode, insertedVnodeQueue) { if (isDef(vnode.data.pendingInsert)) { insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert) vnode.data.pendingInsert = null } vnode.elm = vnode.componentInstance.$el if (isPatchable(vnode)) { invokeCreateHooks(vnode, insertedVnodeQueue) setScope(vnode) } else { // empty component root. // skip all element-related modules except for ref (#3455) registerRef(vnode) // make sure to invoke the insert hook insertedVnodeQueue.push(vnode) } }
這裡會有 vnode.elm
快取了 vnode
建立生成的 DOM 節點。所以對於首次渲染而言,除了在 <keep-alive>
中建立快取,和普通元件渲染沒什麼區別。
- 能實現的功能
能夠把要快取的元件渲染的vnode記到cache裡邊,當返回的時候用快取裡邊的dom直接渲染,還有keep-alive元件提供的include
和 exclude屬性,可以有條件的快取想快取的元件,如果配置了
max
並且快取的長度超過了這個max的值
,還要從快取中刪除第一個
- 存在的問題
存在的問題是儲存vnode節點的key是name,也就是定義路由時元件對應的name,這就會導致同樣的path,不同引數的時候開啟的是從cache裡邊拿到的vnode,很多業務場景都是根據引數來顯示不同內容,而keep-alive底層並沒有對此做擴充套件,可以看下keep-alive原始碼
const key: ?string = 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 { 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.key就是路由裡邊定義的name,所以要用這套方案來實現的根據不同引數展示不同檢視的功能就要對這裡的key做改造,但是keep-alive是vue自帶的,沒法改底層,然後就誕生了我的第二套方案
方案二:navigation元件,scrollbehavior
github上找到類似功能的元件vue-navigation,這個vue元件可以實現返回走快取,底層原理跟keep-alive一樣,實際上是改寫了keep-alive元件,前進重新整理時新增了一個引數VNK,這樣在路由發生變化的時候都會用給url帶一個引數,並且cache的key取值依賴這個引數,借鑑這個元件的思路,做了一個類似keep-alive的元件,其中key的值是getKey方法獲取的,改寫以後的render方法如下
render () { var vnode = this.$slots.default ? this.$slots.default[0] : null if (vnode) { vnode.key = vnode.key || (vnode.isComment ? 'comment' : vnode.tag) const { cache, keys } = this var key = getKey(this.$route, keyName) if (vnode.key.indexOf(key) === -1) { vnode.key = '__navigation-' + key + '-' + vnode.key } if (cache[key]) { if (vnode.key === cache[key].key) { vnode.componentInstance = cache[key].componentInstance } else { cache[key].componentInstance.$destroy() cache[key] = vnode } remove(keys, key) keys.push(key) } else { 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 } return vnode }
getKey方法實現
//url上新增引數vnk的值 export function genKey() { // const t = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' const t = 'xxxxxxxx' return t.replace(/[xy]/g, function (c) { const r = Math.random() * 16 | 0 const v = c === 'x' ? r : (r & 0x3 | 0x8) return v.toString(16) }) } // export function getKey(route, keyName) { return `${route.name || route.path}?${route.query[keyName]}` }
通過新寫一個install方法掛載這個導航元件到vue上就可以實現前進重新整理,返回走快取,並且可以配置最大快取數,使用如下(後期開源,目前只在內部映象):
//app.vue <navigation max="10"> <router-view/> </navigation>
//入口js import Navigation from 'component/navigation'
最後剩下返回上一頁記住上一頁的位置,之所以沒有用開源的這個元件的記位置,是因為它的scrollTop是繫結在子元素上,而我的專案之前不是SPA,scroll都是綁在window上的,導致我的專案window上的scrollTop失效,改動較大,還是使用了vue-router提供的scrollBehavior,在路由配置裡引入
實現如下:
var scrollBehavior = async (to, from, savedPosition) => { if (savedPosition) { return savedPosition } else { return new Promise((resolve, reject) => { setTimeout(() => { resolve({ x: 0, y: to.meta.savedPosition || 0 }) }, 300) }) } } const router = new VueRouter({ mode: 'history', scrollBehavior, routes: [{ path: '', redirect: '/mobile/home.html', meta: { needMtaReport: true, parentsStyle: { height: '100%', minHeight: '100%' } } }, { name: 'scienceCompetition', path: '/mobile/scienceCompetition.html', component: scienceCompetition }] }
搜尋企鵝醫典公眾號或者小程式可以體驗到單頁快取的效果,尤其是返回的速度
- 總結:
- 單頁快取下js載入解析編譯執行的時間縮短了,返回的時候由於走快取js指令碼的佔用時間完全可以忽略,從而整體上縮減了頁面的載入渲染時間,
- 來自測試給出的效能資料
2. 因為專案以前不是單頁,程式碼裡邊定義了很多全域性變數或者全域性事件繫結,改成單頁後全域性變數的值依然存在,就會導致業務邏輯出現bug,所以使用單頁需要注意全域性變數或是事件的謹慎使用,具體的踩坑記錄在
vue單頁快取存在的問題及解決方案https://www.cnblogs.com/afterwawa/p/9439507.html
3.通過push進入下一頁時,head裡邊會累加前面頁面的靜態資源,訪問的頁面越多,最後的頁面掛的靜態的資源越多,返回的時候並不會減少已經載入的靜態資源,單頁快取是典型的空間換時間的方案,記憶體的開銷比較大,能否對資源動態增減以及記憶體佔用的優化一直在探索中,暫時沒有找到很好的解決方法。。。。。