中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

淺談Vue頁面級緩存解決方案feb-alive(上)

發布時間:2020-08-31 01:33:21 來源:腳本之家 閱讀:275 作者:米老糍 欄目:web開發

feb-alive

github地址
體驗鏈接

使用理由

  • 開發者無需因為動態路由或者普通路由的差異而將數據初始化邏輯寫在不同的鉤子里beforeRouteUpdate或者activated
  • 開發者無需手動緩存頁面狀態,例如通過localStorage或者sessionStorage緩存當前頁面的數據
  • feb-alive會幫你處理路由meta信息的存儲與恢復

為什么開發feb-laive?

當我們通過Vue開發項目時候,是否會有以下場景需求?

  • /a跳轉到/b
  • 后退到/a時候,希望從緩存中恢復頁面
  • 再次跳轉到/b時,分兩種情況
    • 情況一: 通過鏈接或者push跳轉,則希望重新創建/b頁面,而不是從緩存中讀取
    • 情況二: 如果點擊瀏覽器自帶前進按鈕,則還是從緩存中讀取頁面。

這個場景需求著重強調了緩存,緩存帶來的好處是,我上次頁面的數據及狀態都被保留,無需在從服務器拉取數據,使用戶體驗大大提高。

嘗試用keep-alive實現頁面緩存

<keep-alive>
 <router-view></router-view>
</keep-alive>

so easy但是理想很完美,現實很殘酷

存在問題

-/a跳到/b,再跳轉到/a 的時候,頁面中的數據是第一次訪問的/a頁面,明明是鏈接跳轉,確出現了緩存的效果,而我們期望的是像app一樣開啟一個新的頁面。

  • 同理動態路由跳轉/page/1->/page/2因為兩個頁面引用的是同一個組件,所以跳轉時頁面就不會有任何改變,因為keep-alive的緩存的key是根據組件來生成的(當然Vue提供了beforeRouteUpdate鉤子供我們刷新數據)
  • 總結:keep-alive的緩存是==組件級別==的,而不是==頁面級別==的。

舉個應用場景

例如瀏覽文章頁面,依次訪問3篇文章

  • /artical/1
  • /artical/2
  • /artical/3

當我從/artical/3后退到/artical/2時候,由于組件緩存,此時頁面還是文章3的內容,所以必須通過beforeRouteUpdate來重新拉取頁面2的數據。(注意此處后退不會觸發組件的activated鉤子,因為兩個路由都渲染同個組件,所以實例會被復用,不會執行reactivateComponent)

如果你想從/artical/3后退到/artical/2時,同時想恢復之前在/artical/2中的一些狀態,那么你還需要自己針對/artical/2中的所有狀態數據進行存儲和恢復。

綜上:keep-alive實現的組件級別的緩存和我們想象中的緩存還是有差距的,keep-alive并不能滿足我們的需求。

==針對這些問題,所以feb-alive插件誕生了==

由于feb-alive是基于keep-alive實現的,所以我們先簡單分析一下keep-alive是如何實現緩存的

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 () {
 // 獲取默認插槽
 const slot = this.$slots.default
 // 獲取第一個組件,也就和官方說明的一樣,keep-alive要求同時只有一個子元素被渲染,如果你在其中有 v-for 則不會工作。
 const vnode: VNode = getFirstComponentChild(slot)
 // 判斷是否存在組件選項,也就是說只對組件有效,對于普通的元素則直接返回對應的vnode
 const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
 if (componentOptions) {
  // 檢測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則使用,否則通過cid+tag生成一個key
  const key: ?string = vnode.key == null
  ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
  : vnode.key
  // 判斷是否存在緩存
  if (cache[key]) {
  // 直接復用組件實例,并更新key的位置
  vnode.componentInstance = cache[key].componentInstance
  remove(keys, key)
  keys.push(key)
  } else {
  // 此處存儲的vnode還沒有實例,在之后的流程中通過在createComponent中會生成實例
  cache[key] = vnode
  keys.push(key)
  // 當緩存數量大于閾值時,刪除最早的key
  if (this.max && keys.length > parseInt(this.max)) {
   pruneCacheEntry(cache, keys[0], keys, this._vnode)
  }
  }
  // 設置keepAlive屬性,createComponent中會判斷是否已經生成組件實例,如果是且keepAlive為true則會觸發actived鉤子。
  vnode.data.keepAlive = true
 }
 return vnode || (slot && slot[0])
 }
}

keep-alive是一個抽象組件,組件實例中維護了一份cache,也就是以下代碼部分

created () {
 // 存儲組件緩存
 this.cache = Object.create(null)
 this.keys = []
}

由于路由切換并不會銷毀keep-alive組件,所以緩存是一直存在的(嵌套路由中,子路由外層的keep-alive情況會不一樣,后續會提到)

繼續看下keep-alive在緩存的存儲和讀取的具體實現,先用一個簡單的demo來描述keep-alive對于組件的緩存以及恢復緩存的過程

let Foo = {
 template: '<div class="foo">foo component</div>',
 name: 'Foo'
}
let Bar = {
 template: '<div class="bar">bar component</div>',
 name: 'Bar'
}
let gvm = new Vue({
 el: '#app',
 template: `
  <div id="#app">
   <keep-alive>
    <component :is="renderCom"></component>
   </keep-alive>
   <button @click="change">切換組件</button>
  </div>
 `,
 components: {
  Foo,
  Bar
 },
 data: {
  renderCom: 'Foo'
 },
 methods: {
  change () {
   this.renderCom = this.renderCom === 'Foo' ? 'Bar': 'Foo'
  }
 }
})

上面例子中,根實例的template會被編譯成如下render函數

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#模板編譯

根據上面的render函數可以知道,vnode生成的過程是深度遞歸的,先創建子元素的vnode再創建父元素的vnode。
所以首次渲染的時候,在生成keep-alive組件vnode的時候,Foo組件的vnode已經生成好了,并且作為keep-alive組件vnode構造函數(_c)的參數傳入。

_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 {...}, // 調用 $createElement/_c的組件實例, 此處是根組件實例對象
 data: {
  hook: {
   init: f,
   prepatch: f,
   insert: f,
   destroy: f
  } 
 }
}

此處需要注意組件的vnode是沒有children的,而是將原本的children作為vnode的componentOptions的children屬性,componentOptions在組件實例化的時候會被用到,同時在初始化的時候componentOptions.children最終會賦值給vm.$slots,源碼部分如下

// createComponent函數
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最后都會通過patch函數進行渲染,將vnode轉換成真實的dom,對于組件則會通過createComponent進行渲染

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
  }
 }
 }

接下去分兩步介紹

  1. keep-alive組件本身的渲染
  2. keep-alive包裹組件的渲染,本例中的Foo組件和Bar組件

先講講本例中針對keep-alive組件本身的渲染

  1. 根組件實例化
  2. 根組件$mount
  3. 根組件調用mountComponent
  4. 根組件生成renderWatcher
  5. 根組件調用updateComponent
  6. 根組件調用vm.render()生成根組件vnode
  7. 根組件調用vm.update(vnode)
  8. 根組件調用vm.patch(oldVnode, vnode)
  9. 根組件調用createElm(vnode)
  10. 在children渲染的時候,如果遇到組件類型的vnode則調用createComponent(vnode),而正是在這個過程中,進行了子組件的實例化及掛載($mount)

所以在執行createElm(keepAliveVnode)的過程中會對keep-alive組件的實例化及掛載,而在實例化的過程中,keep-alive包裹的子組件的vnode會賦值給keep-alive組件實例的$slot屬性,所以在keep-alive實例調用render函數時,可以通過this.$slot拿到包裹組件的vnode,在demo中,就是Foo組件的vnode,具體分析下keep-alive組件的render函數

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])
 }

上面分析到,在執行createElm(keepAliveVnode)的過程中,會執行keep-alive組件的實例化及掛載($mount),而在掛載的過程中,會執行keep-alive的render函數,之前分析過,在render函數中,可以通過this.$slot獲取到子組件的vnode,從上面源碼中,可以知道,keep-alive只處理默認插槽的第一個子組件,言外之意如果在keep-alive中包裹多個組件的話,剩下的組件會被忽略,例如:

<keep-alive>
 <Foo />
 <Bar />
</keep-alive>
// 只會渲染Foo組件

繼續分析,在拿到Foo組件vnode后,判斷了componentOptions,由于我們的Foo是一個組件,所以這里componentOptions是存在的,進到if邏輯中,此處include 表示只有匹配的組件會被緩存,而 exclude 表示任何匹配的組件都不會被緩存,demo中并沒有設置相關規則,此處先忽略。

const { cache, keys } = this
cache, keys是在keep-alive組件的create鉤子中生成的,用來存儲被keep-alive緩存的組件的實例以及對應vnode的key
created () {
 this.cache = Object.create(null)
 this.keys = []
}

繼續下面

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}: '')作為存儲組件實例的key,據此可以知道,如果我們不指定組件的key的話,對于相同的組件會匹配到同一個緩存,這也是為什么最開始在描述keep-alive的時候強調它是一個組件級的緩存方案。

那么首次渲染的時候,cache和keys都是空的,這里就會走else邏輯

cache[key] = vnode
keys.push(key)
if (this.max && keys.length > parseInt(this.max)) {
 pruneCacheEntry(cache, keys[0], keys, this._vnode)
}

以key作為cache的健進行存儲Foo組件vnode(注意此時vnode上面還沒有componentInstance),這里利用了對象存儲的原理,之后進行Foo組件實例化的時候會將其實例賦值給vnode.componentInstance,那么在下次keep-alive組件render的時候就可以獲取到vnode.componentInstance。

所以首次渲染僅僅是在keep-alive的cache上面,存儲了包裹組件Foo的vnode。

針對包裹組件的渲染

上面已經講到執行了keep-alive的render函數,根據上面的源碼可以知道,render函數返回了Foo組件的vnode,那么在keep-alive執行patch的時候,會創建Foo組件的實例,然后再進行Foo組件的掛載,這個過程與普通組件并沒有區別,在此不累述。

當組件從Foo切換到Bar時

本例中由于renderCom屬性的變化,會觸發根組件的renderWatcher,之后會執行patch(oldVnode, vnode)
在進行child vnode比較的時候,keep-alive的新老vnode比較會被判定為sameVnode,之后會進入到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創建的時候,會注入一些生命周期鉤子,其中就包含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組件的實例在此次根組件重渲染的過程中會復用,這也保證了keep-alive組件實例上面之前存儲cache還是存在的

var child = vnode.componentInstance = oldVnode.componentInstance;

下面的updateChildComponent這個函數非常關鍵,這個函數擔任了Foo組件切換到Bar組件的關鍵任務。我們知道,由于keep-alive組件是在此處是復用的,所以不會再觸發initRender,所以vm.$slot不會再次更新。所以在updateChildComponent函數擔起了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函數主要更新了當前組件實例上的一些屬性,這里包括props,listeners,slots。我們著重講一下slots更新,這里通過resolveSlots獲取到最新的包裹組件的vnode,也就是demo中的Bar組件,之后通過vm.$forceUpdate強制keep-alive組件進行重新渲染。(小提示:當我們的組件有插槽的時候,該組件的父組件re-render時會觸發該組件實例$fourceUpdate,這里會有性能損耗,因為不管數據變動是否對slot有影響,都會觸發強制更新,根據vueConf上尤大的介紹,此問題在3.0會被優化),例如

// Home.vue
<template>
 <Artical>
  <Foo />
 </Artical>
</tempalte>

此例中當Home組件更新的時候,會觸發Artical組件的強制刷新,而這種刷新是多余的。

繼續,在更新了keep-alive實例的forceUpdate,之后再次進入到keep-alive的render函數中

render () {
 const slot = this.$slots.default
 const vnode: VNode = getFirstComponentChild(slot)
 // ...
}

此時render函數中獲取到vnode就是Bar組件的vnode,接下去的流程和Foo渲染一樣,只不過也是把Bar組件的vnode緩存到keep-alive實例的cache對象中。

當組件從Bar再次切換到Foo時

針對keep-alive組件邏輯還是和上面講述的一樣

  • 執行prepatch
  • 復用keep-alive組件實例
  • 執行updateChildComponent,更新$slots
  • 觸發vm.$forceUpdate
  • 觸發keep-alive組件render函數

再次進入到render函數,這時候cache[key]就會匹配到Foo組件首次渲染時候緩存的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組件,根據規則,此時生成的key和第一此渲染Foo組件時生成的key是一樣的,所以本次keep-alive的render函數進入到了第一個if分支,也就是匹配到了cache[key],把緩存的componentInstance賦值給當前vnode,然后更新keys(當存在max的時候,能夠保證被刪除的是比較老的緩存)。

很多同學可能會問,這里設置vnode.componentInstance會有什么作用。這里涉及到vue的源碼部分。

由于是從Bar組件切換到Foo組件,所以在patch的時候,比對到此處,并不會被判定為sameVnode,所以自然而然走到createElm,由于Foo是Vue組件,所以會進入到createComponent,所以最終進入到下面函數片段

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
  }
 }
 }

可以根據上面對于keep-alive源碼的分析,此處isReactivated為true,接下去會進入到vnode生成的時候掛在的生命周期init函數

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
 );
 },
 ...
}

此時由于實例已經存在,且keepAlive為true,所以會走第一個if邏輯,會執行prepatch,更新組件屬性及一些監聽器,如果存在插槽的話,還會更新插槽,并執行$forceUpdate,此處在前面已經分析過,不做累述。

繼續createComponent,在函數內部會執行initComponent和insert

if (isDef(vnode.componentInstance)) {
 // 將實例上的dom賦值給vnode
 initComponent(vnode, insertedVnodeQueue);
 // 插入dom
 insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
 reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}

至此,當組件從Bar再次切換到Foo時,實例與dom都得到了復用,達到一個很高的體驗效果!而我們之后要實現的feb-alive就是基于keep-alive實現的。

Vue頁面級緩存解決方案feb-alive (下)

參考文檔

vue-navigation
Vue.js 技術揭秘

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

晴隆县| 汤阴县| 新晃| 应城市| 博客| 苏州市| 高平市| 江川县| 泰和县| 青神县| 荣成市| 海兴县| 荣昌县| 阿克陶县| 芦溪县| 隆昌县| 神池县| 横峰县| 三门县| 泰安市| 兴宁市| 扎鲁特旗| 正阳县| 南丹县| 金平| 旅游| 收藏| 赫章县| 阳西县| 兰西县| 哈巴河县| 波密县| 通榆县| 霞浦县| 井研县| 合肥市| 合山市| 肃宁县| 婺源县| 娄底市| 营山县|