keep-alive實現原理

結合例子有如下狀況html

<keep-alive>
    <coma v-if="visible"></coma>
    <comb v-else></comb>
</keep-alive>
<button @click="visible = !visible">更改</button>
複製代碼

例如在comacomb都有一個input都有對應的value,若是咱們不用keep-alive,當更改visible的時候,這兩個組件都會從新渲染,先前輸入的內容就會丟失,會執行一遍完整的生命週期流程:beforeCreate => created...。
可是若是咱們用了keep-alive,那麼在次切換visible的時候,input對應的value爲上次更改時候的值。 因此keep-alive主要是用於保持組件的狀態,避免組件反覆建立。vue

原理

keep-alive的使用方法定在core/components/keep-alivenode

export default {
    abstract: true,
    props: {
        include: patternTypes, // 緩存白名單
        exclude: patternTypes,  // 緩存黑名單
        max: [String, Number] // 緩存的實例上限
    },
    created() {
        // 用於緩存虛擬DOM
        this.cache = Object.create(null);
        this.keys = [];
    },
    mounted() {
    // 用於監聽i黑白名單,若是發生調用pruneCache
    // pruneCache更新vue的cache緩存
        this.$watch('include', val => {
            pruneCache(this, name => matches(val, name))
        })
        this.$watch('exclude', val => {
            pruneCache(this, name => !matches(val, name))
        })
    }
    render() {
        //...
    }
}
複製代碼

上面代碼中定義了多個聲明週期的操做,最重要的render函數,下面來看看是如何實現的git

render

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 // 定義組件的緩存key
        // 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) // 調整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])
  }
複製代碼

進行分步驟進行分析github

  1. 獲取keep-alive對象包括的第一個子組件對象
  2. 根據白黑名單是否匹配返回自己的vnode
  3. 根據vnodecidtag生成的key,在緩存對象中是否有當前緩存,若是有則返回,並更新keykeys中的位置
  4. 若是當前緩存對象不存在緩存,就往cache添加這個的內容,而且根據LRU算法刪除最近沒有使用的實例
  5. 設置爲第一個子組件對象的keep-alivetrue

首次渲染

結合文章開頭的文章進行分析當前例子,當頁面首次渲染的時候,由於組件的渲染過程是先子組件後父組件的,因此這裏就能拿到子組件的數據,而後把子組件的vnode信息存儲到cache中,而且把coma組件的keepAlive的置爲true。 這個有個疑問,爲何能拿到子組件的componentOptions,藉助上面個例子,咱們知道生成vnode是經過render函數,render函數是經過在platforms/web/entry-runtime-with-compiler中定義,經過compileToFunctionstemplate編譯爲render函數,看一下生成的對應render函數web

<template>
    <div class="parent">
        <keep-alive>
            <coma v-if="visible"></coma>
        <comb v-else></comb>
        </keep-alive>
    </div>
</template>
<script> (function anonymous() { with(this) { return _c('div', { staticClass: "parent" }, [ _c('keep-alive', [(visibility) ? _c('coma') : _c('comb')], 1), _c('button', { on: { "click": change } }, [_v("change")])], 1) } }) </script>
複製代碼

能夠看到生成的render函數中有關keep-alive的生成過程算法

_c('keep-alive', [(visibility) ? _c('coma') : _c('comb')], 1),
複製代碼

keep-alive中先調用了_c('coma'),因此才能訪問到到子組件的componentOptions,具體的_c是在vdom/create-element.js中定義,他判斷是生成組件vnode仍是其餘的。數組

更改data,觸發patch

在首次渲染的時候,咱們更改coma中的input的值,看當visible再次更改成true的時候,input是否會記住先前的值。由於更改了visible的值後,會從新執行這段代碼緩存

updateComponent = () => {
    vm._update(vm._render())
}
複製代碼

因此就會從新執行keep-aliverender函數,由於在首次渲染的時候已經把數據存入到cache中,因此此次數據直接從cache中獲取執行。bash

vnode.componentInstance = cache[key].componentInstance
複製代碼

在首次渲染的時候提到當key值不存在的時候會先將子組件的vnode緩存起來,若是經過打斷點的方式能夠看到首次渲染的時候componentInstanceundefinedcomponentInstance實際是在patch過程當中調用組件的init鉤子才生成的,那麼爲何這個時候能拿到呢,這裏經過一個例子來進行講解例若有下面例子

a = {
    b: 1
}
c = a;
a.b = 5;
console.log(c.b) // 5
複製代碼

object是引用類型,因此原對象發生更改的時候引用的地方也會發生改變
那麼就把先前的狀態信息從新賦值給了coma,而後爲何賦值給了comacoma的就不會執行組件的建立過程呢,看patch的代碼,當執行到createComponent的時候,由於coma爲組件,就會執行組件相關的邏輯

// core/vdom/patch.js
function createComponent(vnode, insertedVnodeQueue, parentElm, refELm) {
    let i = vnode.data;
    if (isDef(i)) {
        const isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
        if (isDef(i = i.hook) && isDef(i = i.init)) {
            i(vnode, false);
        }
    }
}
// core/vdom/create-component
init(vnode) {
    if (vnode.componentInstance && 
        !vnode.componentInstance._isDetroyed &&
        vnode.data.keepAlive) {
            const mountedNode: any = node;
            componentVnodeHooks.prepatch(mountedNode, mountedNode)
    } else {
        const child = vnode.componentInstance = createComponentInstanceForVnode(
            vnode,
            activeInstance
        )
        child.$mount(vnode.elm)
    }
}
複製代碼

由於vnode.componentInstancekeep-alive已經進行了從新賦值,因此而且keepAlivetrue,因此只會執行prepatch,因此createdmounted鉤子都不會執行。

keep-alive自己建立和patch過程

core/instance/render中,能夠看到updateComponent的定義

updateComponent = () => {
    vm._update(vm._render())
}
複製代碼

因此首先調用keep-aliverender函數生成vnode,而後調用vm._update執行patch操做,那麼keep-alive和普通組件在首次建立的時候和patch過程當中有什麼差別呢?

首次渲染

無論keep-alive是否是抽象組件,他終究是個也是個組件,因此也會執行組件相應的邏輯,在首次渲染的時候執行patch操做,執行到core/vdom/patch

function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
         const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
        if (isDef(i = i.hook) && isDef(i = i.init)) {
            i(vnode, false /* hydrating */)
        }
    }
}
複製代碼

由於是首次渲染因此componentInstance並不存在,因此只執行了init鉤子,init的具體做用就是建立子組件實例。
keep-alive畢竟是抽象組件,那抽象組件和正常組件區別體如今哪兒呢? 在core/instance/lifecycle中能夠看到,不是抽象組件的時候纔會往父組件中加入自己,,而且子組件也不會往抽象組件$children中加入本身。這個函數又是在vm._init中進行調用的

let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }
複製代碼

更改數據後patch過程

結合上面的例子,當visible發生更改的時候,會影響到keep-alive組件嗎,在patch的那片文章提到過當data中的值發生改變的時候,會觸發updateComponent

updateComponent = () => {
    vm._update(vm._render())
}
複製代碼

就會從新執行keep-aliverender函數,從新執行根組件的patch過程,具體的原理課參照Vue 源碼patch過程詳解,這裏就直接執行了keep-alive組件的prepatch鉤子

待解決

這裏有個問題須要解決一下,每次到達下一個tick的時候都須要進行從新生成vnode,這裏有什麼辦法優化嗎,能不能用其餘方式來替換,仍是說必須這麼作?小夥伴能想到什麼好的辦法嗎?

keep-alive是不是必須的

能夠看到keep-alive對於緩存數據是有巨大幫助的,而且能夠防止組件反覆建立。那麼就有問題了,是否絕大多數組件均可以使用keep-alive用於提升性能。

  1. 什麼場景使用
    在頁面中,咱們若是返回上一個頁面是會刷新數據的,若是咱們須要保留離開頁面時候的狀態,那麼就須要使用keep-alive
  2. 什麼場景不使用
    先思考使用keep-alive是否有必要,若是兩個組件切換是不須要保存狀態的,那還須要嗎。你可能說用keep-alive能節省性能,那咱們在須要在activated重置這些屬性。這樣作有幾點風險
    1. 你能肯定把全部的變量都進行了重置了嗎,這個風險是可控的嗎
    2. 全部的緩存都放在了cache中,當組件過多的時候內容過多,就致使這個對象巨大,還能起到提升性能的需求嗎,這個表示懷疑態度

Vue的源碼分析的文章會一直更新,麻煩關注一下個人github

相關文章
相關標籤/搜索