vue3?keepalive源碼解析解決線上問題

 更新時間:2022年07月06日 10:03:42   作者:那個曾經的少年回來了  
這篇文章主要為大家講解了vue3?keepalive源碼解析解決線上問題,需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

引言

  • 1、通過本文可以了解到vue3 keepalive功能
  • 2、通過本文可以了解到vue3 keepalive使用場景
  • 3、通過本文可以學習到vue3 keepalive真實的使用過程
  • 4、通過本文可以學習vue3 keepalive源碼調試
  • 5、通過本文可以學習到vue3 keepalive源碼的精簡分析

1、keepalive功能

  • keepalive是vue3中的一個全局組件
  • keepalive 本身不會渲染出來,也不會出現在dom節點當中,但是它會被渲染為vnode,通過vnode可以跟蹤到keepalive中的cache和keys,當然也是在開發環境才可以,build打包以后沒有暴露到vnode中(這個還要再確認一下)
  • keepalive 最重要的功能就是緩存組件
  • keepalive 通過LRU緩存淘汰策略來更新組件緩存,可以更有效的利用內存,防止內存溢出,源代碼中的最大緩存數max為10,也就是10個組件之后,就開始淘汰最先被緩存的組件了

2、keepalive使用場景

  • 這里先假設一個場景: A頁面是首頁=====> B頁面列表頁面(需要緩存的頁面)=======> C 詳情頁 由C詳情頁到到B頁面的時候,要返回到B的緩存頁面,包括頁面的基礎數據和列表的滾動條位置信息 如果由B頁面返回到A頁面,則需要將B的緩存頁清空
  • 上述另外一個場景:進入頁面直接緩存,然后就結束了,這個比較簡單本文就不討論了

3、在項目中的使用過程

keepalive組件總共有三個參數

  • include:可傳字符串、正則表達式、數組,名稱匹配成功的組件會被緩存
  • exclude:可傳字符串、正則表達式、數組,名稱匹配成功的組件不會被緩存
  • max:可傳數字,限制緩存組件的最大數量,默認為10

首先在App.vue根代碼中添加引入keepalive組件,通過這里可以發現,我這里緩存的相當于整個頁面,當然你也可以進行更細粒度的控制頁面當中的某個區域組件

    <template>
        <router-view v-slot="{ Component }">
            <keep-alive :include="keepAliveCache">
                <component :is="Component" :key="$route.name" />
            </keep-alive>
        </router-view>
    </template>
    <script lang="ts" setup>
    import { computed } from "vue";
    import { useKeepAliverStore } from "@/store";
    const useStore = useKeepAliverStore();
    const keepAliveCache = computed(() => {
        return useStore.caches;
    });
    </script>

通過App.vue可以發現,通過pinia(也就是vue2中使用的vuex)保存要緩存的頁面組件, 來處理include緩存,和保存頁面組件中的滾動條信息數據

    import { defineStore } from "pinia";
    export const useKeepAliverStore = defineStore("useKeepAliverStore", {
        state: () => ({
            caches: [] as any,
            scrollList: new Map(),  // 緩存頁面組件如果又滾動條的高度
        }),
        actions: {
            add(name: string) {
                this.caches.push(name);
            },
            remove(name: string) {
                console.log(this.caches, 'this.caches')
                this.caches = this.caches.filter((item: any) => item !== name);
                console.log(this.caches, 'this.caches')
            },
            clear() {
                this.caches = []
            }
        }
    });

組件路由剛剛切換時,通過beforeRouteEnter將組件寫入include, 此時組件生命周期還沒開始。如果都已經開始執行組件生命周期了,再寫入就意義了。

所以這個鉤子函數就不能寫在setup中,要單獨提出來寫。當然你也可以換成路由的其他鉤子函數處理beforeEach,但這里面使用的話,好像使用不了pinia,這個還需要進一步研究一下。

    import { useRoute, useRouter, onBeforeRouteLeave } from "vue-router";
    import { useKeepAliverStore } from "@/store";
    const useStore = useKeepAliverStore()
    export default {
        name:"record-month",
        beforeRouteEnter(to, from, next) {
            next(vm => {
                if(from.name === 'Home' && to.name === 'record-month') {
                useStore.add(to.name)
                }
            });
        }
    }
    </script>

組件路由離開時判斷,是否要移出緩存,這個鉤子就直接寫在setup中就可以了。

    onBeforeRouteLeave((to, from) => {
        console.log(to.name, "onBeforeRouteLeave");
        if (to.name === "new-detection-detail") {
            console.log(to, from, "進入詳情頁面不做處理");
        } else {
            useStore.remove(from.name)
            console.log(to, from, "刪除組件緩存");
        }
    });

在keepalive兩個鉤子函數中進行處理scroll位置的緩存,onActivated中獲取緩存中的位置, onDeactivated記錄位置到緩存

    onActivated(() => {
        if(useStore.scrollList.get(routeName)) {
            const top = useStore.scrollList.get(routeName)
            refList.value.setScrollTop(Number(top))
        }
    });
    onDeactivated(() => {
        const top = refList.value.getScrollTop()
        useStore.scrollList.set(routeName, top)
    });

這里定義一個方法,設置scrollTop使用了原生javascript的api

    const setScrollTop = (value: any) => {
        const dom = document.querySelector('.van-pull-refresh')
        dom!.scrollTop = value
    }

同時高度怎么獲取要先注冊scroll事件,然后通過getScrollTop 獲取當前滾動條的位置進行保存即可

    onMounted(() => {
        scrollDom.value = document.querySelector('.van-pull-refresh') as HTMLElement
        const throttledFun = useThrottleFn(() => {
            console.log(scrollDom.value?.scrollTop, 'addEventListener')
            state.scrollTop = scrollDom.value!.scrollTop
        }, 500)
        if(scrollDom.value) {
            scrollDom.value.addEventListener('scroll',throttledFun)
        }
    })
    const getScrollTop = () => {
        console.log('scrollDom.vaue', scrollDom.value?.scrollTop)
        return state.scrollTop
    }

上面注冊scroll事件中使用了一個useThrottleFn,這個類庫是@vueuse/core中提供的,其中封裝了很多工具都非常不錯,用興趣的可以研究研究

    https://vueuse.org/shared/usethrottlefn/#usethrottlefn

此時也可以查看找到實例的vnode查找到keepalive,是在keepalive緊挨著的子組件里

    const instance = getCurrentInstance()
    console.log(instance.vnode.parent) // 這里便是keepalive組件vnode
    // 如果是在開發環境中可以查看到cache對象
    instance.vnode.parent.__v_cache
    // vue源碼中,在dev環境對cache進行暴露,生產環境是看不到的
    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
        ;(instance as any).__v_cache = cache
    }

4、vue3 keepalive源碼調試

1、克隆代碼

    git clone git@github.com:vuejs/core.git

2、安裝依賴

    pnpm i

3、如果不能使用pnpm,可以先通過npm安裝一下

    npm i pnpm -g

4、安裝完成以后,找到根目錄package.json文件中的scripts

    // 在dev命令后添加 --source-map是從已轉換的代碼,映射到原始的源文件
    "dev": "node scripts/dev.js  --sourcemap"

參考 http://www.gpbgw.com/article/154583.htm

5、執行pnpm run dev則會build vue源碼

    pnpm run dev
    //則會出現以下,代表成功了(2022年5月27日),后期vue源代碼作者可能會更新,相應的提示可能發生變更,請注意一下
    > @3.2.36 dev H:\github\sourceCode\core
    > node scripts/dev.js  --sourcemap
    watching: packages\vue\dist\vue.global.js
    //到..\..\core\packages\vue\dist便可以看到編譯成功,以及可以查看到examples樣例demo頁面

6、然后在 ....\core\packages\vue\examples\composition中添加一個aehyok.html文件,將如下代碼進行拷貝,然后通過chrome瀏覽器打開,F12,找到源代碼的Tab頁面,通過快捷鍵Ctrl+ P 輸入KeepAlive便可以找到這個組件,然后通過左側行標右鍵就可以添加斷點,進行調試,也可以通過右側的【調用堆?!窟M行快速跳轉代碼進行調試。

    <script src="../../dist/vue.global.js"></script>
    <script type="text/x-template" id="template-1">
        <div>template-1</div>
        <div>template-1</div>
    </script>
    <script type="text/x-template" id="template-2">
        <div>template-2</div>
        <div>template-2</div>
    </script>
    <script>
    const { reactive, computed } = Vue
    const Demo1 = {
        name: 'Demo1',
        template: '#template-1',
        setup(props) {
        }
    }
    const Demo2 = {
        name: 'Demo2',
        template: '#template-2',
        setup(props) {
        }
    }
    </script>
    <!-- App template (in DOM) -->
    <div id="demo">
        <div>Hello World</div>
        <div>Hello World</div>
        <div>Hello World</div>
        <button @click="changeClick(1)">組件一</button>
        <button @click="changeClick(2)">組件二</button>
        <keep-alive :include="includeCache">
            <component :is="componentCache" :key="componentName" v-if="componentName" />
        </keep-alive>
    </div>
    <!-- App script -->
    <script>
    Vue.createApp({
    components: {
        Demo1,
        Demo2
    },
    data: () => ({
        includeCache: [],
        componentCache: '',
        componentName: '',
    }),
    methods:{
        changeClick(type) {
            if(type === 1) {
                if(!this.includeCache.includes('Demo1')) {
                    this.includeCache.push('Demo1')
                }
                console.log(this.includeCache, '000')
                this.componentCache = Demo1
                this.componentName = 'Demo1'
            }
            if(type === 2) {
                if(!this.includeCache.includes('Demo2')) {
                    this.includeCache.push('Demo2')
                }
                console.log(this.includeCache, '2222')
                this.componentName = 'Demo2'
                this.componentCache = Demo2
            }
        }
    }
    }).mount('#demo')
    </script>

7、調試源碼發現 keepalive中的render函數(或者說時setup中的return 函數)在子組件切換時就會去執行,變更邏輯緩存

  • 第一次進入頁面初始化keepalive組件會執行一次,
  • 然后點擊組件一,再次執行render函數
  • 然后點擊組件二,會再次執行render函數

8、調試截圖說明

9、調試操作,小視頻觀看

5、vue3 keealive源碼粗淺分析

通過查看vue3 KeepAlive.ts源碼,源碼路徑:https://github.com/vuejs/core/blob/main/packages/runtime-core/src/components/KeepAlive.ts

    // 在setup初始化中,先獲取keepalive實例
    // getCurrentInstance() 可以獲取當前組件的實例
    const instance = getCurrentInstance()!
    // KeepAlive communicates with the instantiated renderer via the
    // ctx where the renderer passes in its internals,
    // and the KeepAlive instance exposes activate/deactivate implementations.
    // The whole point of this is to avoid importing KeepAlive directly in the
    // renderer to facilitate tree-shaking.
    const sharedContext = instance.ctx as KeepAliveContext
    // if the internal renderer is not registered, it indicates that this is server-side rendering,
    // for KeepAlive, we just need to render its children
    /// SSR 判斷,暫時可以忽略掉即可。
    if (__SSR__ && !sharedContext.renderer) {
        return () => {
            const children = slots.default && slots.default()
            return children && children.length === 1 ? children[0] : children
        }
    }
    // 通過Map存儲緩存vnode,
    // 通過Set存儲緩存的key(在外面設置的key,或者vnode的type)
    const cache: Cache = new Map()
    const keys: Keys = new Set()
    let current: VNode | null = null
    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
    ;(instance as any).__v_cache = cache
    }
    const parentSuspense = instance.suspense
    const {
    renderer: {
        p: patch,
        m: move,
        um: _unmount,
        o: { createElement }
    }
    } = sharedContext
    // 創建了隱藏容器
    const storageContainer = createElement('div')
    // 在實例上注冊兩個鉤子函數 activate,  deactivate
    sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
        const instance = vnode.component!
        move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
        // in case props have changed
        patch(
            instance.vnode,
            vnode,
            container,
            anchor,
            instance,
            parentSuspense,
            isSVG,
            vnode.slotScopeIds,
            optimized
        )
        queuePostRenderEffect(() => {
            instance.isDeactivated = false
            if (instance.a) {
            invokeArrayFns(instance.a)
            }
            const vnodeHook = vnode.props && vnode.props.onVnodeMounted
            if (vnodeHook) {
            invokeVNodeHook(vnodeHook, instance.parent, vnode)
            }
        }, parentSuspense)
        if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
            // Update components tree
            devtoolsComponentAdded(instance)
        }
    }
    sharedContext.deactivate = (vnode: VNode) => {
        const instance = vnode.component!
        move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
        queuePostRenderEffect(() => {
            if (instance.da) {
            invokeArrayFns(instance.da)
            }
            const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
            if (vnodeHook) {
            invokeVNodeHook(vnodeHook, instance.parent, vnode)
            }
            instance.isDeactivated = true
        }, parentSuspense)
        if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
            // Update components tree
            devtoolsComponentAdded(instance)
        }
    }
    // 組件卸載
    function unmount(vnode: VNode) {
        // reset the shapeFlag so it can be properly unmounted
        resetShapeFlag(vnode)
        _unmount(vnode, instance, parentSuspense, true)
    }
    // 定義 include和exclude變化時,對緩存進行動態處理
    function pruneCache(filter?: (name: string) => boolean) {
        cache.forEach((vnode, key) => {
            const name = getComponentName(vnode.type as ConcreteComponent)
            if (name && (!filter || !filter(name))) {
            pruneCacheEntry(key)
            }
        })
    }
    function pruneCacheEntry(key: CacheKey) {
        const cached = cache.get(key) as VNode
        if (!current || cached.type !== current.type) {
            unmount(cached)
        } else if (current) {
            // current active instance should no longer be kept-alive.
            // we can't unmount it now but it might be later, so reset its flag now.
            resetShapeFlag(current)
        }
        cache.delete(key)
        keys.delete(key)
    }
    // 可以發現通過include 可以配置被顯示的組件,
    // 當然也可以設置exclude來配置不被顯示的組件,
    // 組件切換時隨時控制緩存
    watch(
    () => [props.include, props.exclude],
    ([include, exclude]) => {
        include && pruneCache(name => matches(include, name))
        exclude && pruneCache(name => !matches(exclude, name))
    },
    // prune post-render after `current` has been updated
    { flush: 'post', deep: true }
    )
    // 定義當前組件Key
    // cache sub tree after render
        let pendingCacheKey: CacheKey | null = null
        // 這是一個重要的方法,設置緩存
        const cacheSubtree = () => {
        // fix #1621, the pendingCacheKey could be 0
        if (pendingCacheKey != null) {
            cache.set(pendingCacheKey, getInnerChild(instance.subTree))
        }
        }
        onMounted(cacheSubtree)
        onUpdated(cacheSubtree)
        // 組件卸載的時候,對緩存列表進行循環判斷處理
        onBeforeUnmount(() => {
            cache.forEach(cached => {
                const { subTree, suspense } = instance
                const vnode = getInnerChild(subTree)
                if (cached.type === vnode.type) {
                // current instance will be unmounted as part of keep-alive's unmount
                resetShapeFlag(vnode)
                // but invoke its deactivated hook here
                const da = vnode.component!.da
                da && queuePostRenderEffect(da, suspense)
                return
                }
                unmount(cached)
            })
        })
    // 同時在keepAlive組件setup生命周期中,return () => {} 渲染的時候,對組件進行判斷邏輯處理,同樣對include和exclude判斷渲染。
    // 判斷keepalive組件中的子組件,如果大于1個的話,直接警告處理了
    // 另外如果渲染的不是虛擬dom(vNode),則直接返回渲染即可。
    return () => {
        // eslint-disable-next-line no-debugger
        console.log(props.include, 'watch-include')
        pendingCacheKey = null
        if (!slots.default) {
            return null
        }
        const children = slots.default()
        const rawVNode = children[0]
        if (children.length > 1) {
            if (__DEV__) {
            warn(`KeepAlive should contain exactly one component child.`)
            }
            current = null
            return children
        } else if (
            !isVNode(rawVNode) ||
            (!(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) &&
            !(rawVNode.shapeFlag & ShapeFlags.SUSPENSE))
        ) {
            current = null
            return rawVNode
        }
        // 接下來處理時Vnode虛擬dom的情況,先獲取vnode
        let vnode = getInnerChild(rawVNode)
        // 節點類型
        const comp = vnode.type as ConcreteComponent
        // for async components, name check should be based in its loaded
        // inner component if available
        // 獲取組件名稱
        const name = getComponentName(
            isAsyncWrapper(vnode)
            ? (vnode.type as ComponentOptions).__asyncResolved || {}
            : comp
        )
        //這個算是最熟悉的通過props傳遞進行的參數,進行解構
        const { include, exclude, max } = props
        // include判斷 組件名稱如果沒有設置, 或者組件名稱不在include中,
        // exclude判斷 組件名稱有了,或者匹配了
        // 對以上兩種情況都不進行緩存處理,直接返回當前vnode虛擬dom即可。
        if (
            (include && (!name || !matches(include, name))) ||
            (exclude && name && matches(exclude, name))
        ) {
            current = vnode
            return rawVNode
        }
        // 接下來開始處理有緩存或者要緩存的了
        // 先獲取一下vnode的key設置,然后看看cache緩存中是否存在
        const key = vnode.key == null ? comp : vnode.key
        const cachedVNode = cache.get(key)
        // 這一段可以忽略了,好像時ssContent相關,暫時不管了,沒看明白??
        // clone vnode if it's reused because we are going to mutate it
        if (vnode.el) {
            vnode = cloneVNode(vnode)
            if (rawVNode.shapeFlag & ShapeFlags.SUSPENSE) {
            rawVNode.ssContent = vnode
            }
        }
        // 上面判斷了,如果沒有設置key,則使用vNode的type作為key值
        pendingCacheKey = key
        //判斷上面緩存中是否存在vNode
        // if 存在的話,就將緩存中的vnode復制給當前的vnode
        // 同時還判斷了組件是否為過渡組件 transition,如果是的話 需要注冊過渡組件的鉤子
        // 同時先刪除key,然后再重新添加key
        // else 不存在的話,就添加到緩存即可
        // 并且要判斷一下max最大緩存的數量是否超過了,超過了,則通過淘汰LPR算法,刪除最舊的一個緩存
        // 最后又判斷了一下是否為Suspense。也是vue3新增的高階組件。
        if (cachedVNode) {
            // copy over mounted state
            vnode.el = cachedVNode.el
            vnode.component = cachedVNode.component
            if (vnode.transition) {
            // recursively update transition hooks on subTree
            setTransitionHooks(vnode, vnode.transition!)
            }
            // avoid vnode being mounted as fresh
            vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
            // make this key the freshest
            keys.delete(key)
            keys.add(key)
        } else {
            keys.add(key)
            // prune oldest entry
            if (max && keys.size > parseInt(max as string, 10)) {
            pruneCacheEntry(keys.values().next().value)
            }
        }
        // avoid vnode being unmounted
        vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
        current = vnode
        return isSuspense(rawVNode.type) ? rawVNode : vnode

6、總結

通過這次查看vue3 keepalive源碼發現,其實也沒那么難,當然這次查看源代碼也只是粗略查看,并沒有看的那么細,主要還是先解決問題。動動手調試一下,有時候真的就是不逼一下自己都不知道自己有多么的優秀。原來我也能稍微看看源代碼了。以后有空可以多看看vue3源代碼,學習一下vue3的精髓。了解vue3更為細節的一些知識點。

本文涉及到的代碼后續會整理到該代碼倉庫中

https://github.com/aehyok/vue-qiankun

最后自己每天工作中的筆記記錄倉庫,主要以文章鏈接和問題處理方案為主

https://github.com/aehyok/2022

以上就是vue3 keepalive源碼解析解決線上問題的詳細內容,更多關于vue3 keepalive的資料請關注腳本之家其它相關文章!

相關文章

  • vue.js入門教程之計算屬性

    vue.js入門教程之計算屬性

    Vue.js 的內聯表達式非常方便,但它最合適的使用場景是簡單的布爾操作或字符串拼接。如果涉及更復雜的邏輯,你應該使用計算屬性。這篇文章我們將一起學習vue.js的計算屬性。什么是計算屬性,為什么要用這東西呢?通過下面這篇文章你將解決這些問題,下面來一起看看吧。
    2016-09-09
  • 在vue中通過axios異步使用echarts的方法

    在vue中通過axios異步使用echarts的方法

    本篇文章主要介紹了在vue中通過axios異步使用echarts的方法,小編覺得挺不錯的,現在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-01-01
  • vue組件學習教程

    vue組件學習教程

    這篇文章主要為大家詳細介紹了vue組件學習教程,根據Vue官方文檔學習的筆記,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2017-09-09
  • vue基礎之事件簡寫、事件對象、冒泡、默認行為、鍵盤事件實例分析

    vue基礎之事件簡寫、事件對象、冒泡、默認行為、鍵盤事件實例分析

    這篇文章主要介紹了vue基礎之事件簡寫、事件對象、冒泡、默認行為、鍵盤事件,結合實例形式分析了vue.js事件簡寫、冒泡及阻止冒泡等相關操作技巧,需要的朋友可以參考下
    2019-03-03
  • Vue中created與mounted的區別淺析

    Vue中created與mounted的區別淺析

    在使用vue框架的過程中,我們經常需要給一些數據做一些初始化處理,這時候我們常用的就是在created與mounted選項中作出處理,這篇文章主要給大家介紹了關于Vue中created與mounted區別的相關資料,其中部分知識點可能是我們日常工作會見到或用到的,需要的朋友可以參考下
    2022-06-06
  • 基于vue.js中事件修飾符.self的用法(詳解)

    基于vue.js中事件修飾符.self的用法(詳解)

    下面小編就為大家分享一篇基于vue.js中事件修飾符.self的用法詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2018-02-02
  • Vant Weapp組件踩坑:picker的初始賦值解決

    Vant Weapp組件踩坑:picker的初始賦值解決

    這篇文章主要介紹了Vant Weapp組件踩坑:picker的初始賦值解決,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-11-11
  • vue路由前進后退動畫效果的實現代碼

    vue路由前進后退動畫效果的實現代碼

    這篇文章主要介紹了vue路由前進后退動畫效果,本文通過實例代碼給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下
    2018-12-12
  • vue使用監聽實現全選反選功能

    vue使用監聽實現全選反選功能

    最近做的項目用到了全選全不選功能,于是我就自己動手寫了一個,基于vue使用監聽實現全選反選功能,具體實例代碼大家參考下本文
    2018-07-07
  • Vue?watch中監聽值的變化,判斷后修改值方式

    Vue?watch中監聽值的變化,判斷后修改值方式

    這篇文章主要介紹了Vue?watch中監聽值的變化,判斷后修改值方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-04-04

最新評論

美丽人妻被按摩中出中文字幕