Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hot reload problem when use keep-alive wrap router-view with a dynamic key #1332

Open
hxlyd opened this issue May 31, 2018 · 29 comments
Open

Comments

@hxlyd
Copy link

hxlyd commented May 31, 2018

Version

13.3.0

Reproduction link

https://codesandbox.io/s/vyy3yokjj0

Steps to reproduce

1.modify the code in the script tag of the Hello component

2.the router-view component disappear from the page

3.we can find the component is activated with devtools

What is expected?

Hot reload normally

What is actually happening?

Have to refresh the page manually each time modify the code

@maksnester
Copy link

maksnester commented Jul 17, 2018

I came here to report this issue back here, but it's already reported, so I just share the original issue from vue-router then to create a link.

vuejs/vue-router#2150

@twickstrom
Copy link

Does anyone have a work around?

@franciscolourenco
Copy link

Anyone? :)

@NikitaKA
Copy link

Still nothing?

@mcoope31
Copy link

mcoope31 commented Jul 3, 2019

This still persists. Is everyone using keep-alive just refreshing each time still?

@twickstrom
Copy link

This still persists. Is everyone using keep-alive just refreshing each time still?

For the most part... sigh

@vitto32
Copy link

vitto32 commented Sep 24, 2019

Very annoying.
I just spent a few hours to find where the issue comes from (blank page after HMR).
In my case it seems to happen only if the edit involves a new line character.

@ThomasKientz
Copy link

@mcoope31

This still persists. Is everyone using keep-alive just refreshing each time still?

I am commenting <keep-alive> while working on a component inside scoped inside it.

@7iomka
Copy link

7iomka commented Dec 17, 2019

Any workaround?

@mcoope31
Copy link

The best workaround I found is
<nuxt :keep-alive="!isDev" />
where isDev = process.env.NODE_ENV === 'development'

haven't thought about it since.

@hellomrbigshot
Copy link

Same problem.

@nikolawan
Copy link

Workaround: disable keep-alive for debug.
Warning: view component must have name

<template>
    <keep-alive :exclude="exclude">
      <router-view></router-view>
    </keep-alive>
</template>
<script>
export default {
  computed: {
    exclude () {
      if (process.env.NODE_ENV === 'production') {
        return ''
      }
      return /.+/
    }
  }
}
</script>

@nailfar
Copy link

nailfar commented Mar 20, 2020

因为router-view 给了key 所以会白屏

<keep-alive>
    <router-view :key="$route.fullPath"></router-view>
</keep-alive>

keep-alive 的源码: 缓存的key 就是取的 router-view 的key;

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

每次hot update 的时候 cid 是会变化的 看 下面 vue-hot-reload 的源码 record.Ctor.cid = newCtor.cid

if (record.Ctor) {
      if (version[1] < 2) {
        // preserve pre 2.2 behavior for global mixin handling
        record.Ctor.extendOptions = options
      }
      var newCtor = record.Ctor.super.extend(options)
      // prevent record.options._Ctor from being overwritten accidentally
      newCtor.options._Ctor = record.options._Ctor
      record.Ctor.options = newCtor.options
      record.Ctor.cid = newCtor.cid
      record.Ctor.prototype = newCtor.prototype
      if (newCtor.release) {
        // temporary global mixin strategy used in < 2.0.0-alpha.6
        newCtor.release()
      }
    } else {
      updateOptions(record.options, options)
    }

修改方法:keep-alive组件缓存 key的取法 加上 (componentOptions.Ctor as any).cid

const key = vnode.key
        ? (componentOptions.Ctor as any).cid + (componentOptions.tag ? `::${componentOptions.tag}` : "")
        : vnode.key + (componentOptions.Ctor as any).cid;

组件全局覆盖,伪代码:

const cmpt = Vue.component("KeepAlive"); // 混入原生组件,重写render
delete (cmpt as any).mounted;
const newKeepAlive= Vue.extend({
  name: "keep-alive",
  mixins: [cmpt],
 
  render() {
    // override
 }

export default function install(vue){
  process.env.NODE_ENV === "develop" && vue.component("keep-alive",newKeepAlive)
}

@sodatea 大佬能修复下吗,没搞过pr

@syoueicc
Copy link

Workaround: disable keep-alive for debug.
Warning: view component must have name

<template>
    <keep-alive :exclude="exclude">
      <router-view></router-view>
    </keep-alive>
</template>
<script>
export default {
  computed: {
    exclude () {
      if (process.env.NODE_ENV === 'production') {
        return ''
      }
      return /.+/
    }
  }
}
</script>

it's working!👍

@ericwu-wish
Copy link

ericwu-wish commented May 27, 2020

@nailfar
for vue 2.6, i did some work around, this will require name to be set in component:

import Vue from "vue"
/*
* https://github.com/vuejs/vue-loader/issues/1332#issuecomment-601572625
*/
function isDef(v) {
    return v !== undefined && v !== null
}
function isAsyncPlaceholder(node) {
    return node.isComment && node.asyncFactory
}
function getFirstComponentChild(children) {
    if (Array.isArray(children)) {
    for (var i = 0; i < children.length; i++) {
        var c = children[i]
        if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
        return c
        }
    }
    }
}
function getComponentName(opts) {
    return opts && (opts.Ctor.options.name || opts.tag)
}
function matches(pattern, name) {
    if (Array.isArray(pattern)) {
    return pattern.indexOf(name) > -1
    } else if (typeof pattern === "string") {
    return pattern.split(",").indexOf(name) > -1
    } else if (isRegExp(pattern)) {
    return pattern.test(name)
    }
    /* istanbul ignore next */
    return false
}
function remove(arr, item) {
    if (arr.length) {
    var index = arr.indexOf(item)
    if (index > -1) {
        return arr.splice(index, 1)
    }
    }
}
function pruneCacheEntry(cache, key, keys, current) {
    var cached$$1 = cache[key]
    if (cached$$1 && (!current || cached$$1.tag !== current.tag)) {
    cached$$1.componentInstance.$destroy()
    }
    cache[key] = null
    remove(keys, key)
}
function pruneCache(keepAliveInstance, filter) {
    var cache = keepAliveInstance.cache
    var keys = keepAliveInstance.keys
    var _vnode = keepAliveInstance._vnode
    const cachedNameKeyMap = keepAliveInstance.cachedNameKeyMap
    for (var key in cache) {
    var cachedNode = cache[key]
    if (cachedNode) {
        var name = getComponentName(cachedNode.componentOptions)
        if (name && !filter(name)) {
        delete cachedNameKeyMap[name]
        pruneCacheEntry(cache, key, keys, _vnode)
        }
    }
    }
}
const patternTypes = [String, RegExp, Array]
const KeepAlive = {
  name: "keep-alive",
  abstract: true,

  props: {
    include: patternTypes,
    exclude: patternTypes,
    max: [String, Number],
  },

  created() {
    this.cache = Object.create(null)
    this.cachedNameKeyMap = 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
    const vnode = getFirstComponentChild(slot)
    const componentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      // check pattern
      const name = getComponentName(componentOptions)
      const { include, exclude } = this
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }
      const { cache, cachedNameKeyMap, keys } = this
      const key =
        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 old component for hmr
        if (name && cachedNameKeyMap[name] && cachedNameKeyMap[name] !== key) {
          pruneCacheEntry(cache, cachedNameKeyMap[name], keys)
        }
        cachedNameKeyMap[name] = 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])
  },
}
// ovveride original keep-alive
process.env.NODE_ENV === "development" && Vue.component("KeepAlive", KeepAlive)

@SuspiciousLookingOwl
Copy link

For me, this only happens when I'm adding new line / deleting existing line to the template. If I'm modifying existing line, the hot reload works fine.

@haoqunjiang
Copy link
Member

The newline issue is tracked here: #1682

@dwatts3624
Copy link

@nailfar & @ericwu-wish thank you so much for the direction here. I would have been completely lost without the callout regarding componentOptions.Ctor.cid.

The solution I landed on is pretty much on par with what @nailfar wrote up which I tested and have verified in multiple scenarios.

To make this easier I've created a plugin which will resolve the issue for anyone interested: https://www.npmjs.com/package/vue-keep-alive-dev

Though, honestly, I wonder if there's really much harm in appending the cid to the cache globally as it stays stationary in line with the component even when HMR isn't running (though I may be missing something) so I'm going to throw out a PR to Vue for consideration.

@wuyuweixin
Copy link

I refreshing each time.Very annoying.

@wuyuweixin
Copy link

Hope it can be fixed soon.

@mingyuyuyu
Copy link

@sodatea hao dalao,when fix it?

@mingyuyuyu
Copy link

its so unfriendly

@FossPrime
Copy link

FossPrime commented Apr 29, 2021

Update:
I hear this is a non issue on the more granular Webpack 5... so I'll just focus on upgrading to that.
Though if I need this now, I'll look on getting rid of the dynamic ID on the router-view and use props, vuex and better state management instead.


I need a better workaround...

  • Is there a way to detect HMR?
  • Can we reload keep-alive with a button instead?
  • I have video playing in a destroyed keep alive ghost element... wth, maybe that's a clue on how to fix it. To clarify, HMR killed my keep-alive video element... it's not in the dom from what I can tell from the inspector, but the audio is still playing through my speakers...

I really can't disable keep-alive as it's critical to letting the browser know I've interacted with the page. And the runtime behavior of the app is completely different without keep alive.

beishuangzz added a commit to beishuangzz/vue that referenced this issue May 26, 2021
@DoveAz
Copy link

DoveAz commented Jun 7, 2021

Any update ?

@beishuangzz
Copy link

Add cid comparison.

import Vue from "vue";

let patternTypes = [String, RegExp, Array];

function pruneCacheEntry(cache, key, keys, current) {
    let cached$$1 = cache[key];
    if (cached$$1 && (!current || cached$$1.tag !== current.tag)) {
        cached$$1.componentInstance.$destroy();
    }
    cache[key] = null;
    remove(keys, key);
}

function pruneCache(keepAliveInstance, filter) {
    let cache = keepAliveInstance.cache;
    let keys = keepAliveInstance.keys;
    let _vnode = keepAliveInstance._vnode;
    for (let key in cache) {
        let cachedNode = cache[key];
        if (cachedNode) {
            let name = getComponentName(cachedNode.componentOptions);
            if (name && !filter(name)) {
                pruneCacheEntry(cache, key, keys, _vnode);
            }
        }
    }
}

function matches(pattern, name) {
    if (Array.isArray(pattern)) {
        return pattern.indexOf(name) > -1
    } else if (typeof pattern === 'string') {
        return pattern.split(',').indexOf(name) > -1
    } else if (isRegExp(pattern)) {
        return pattern.test(name)
    }
    /* istanbul ignore next */
    return false
}

function isDef(v) {
    return v !== undefined && v !== null
}

/**
 * Remove an item from an array.
 */
function remove(arr, item) {
    if (arr.length) {
        var index = arr.indexOf(item);
        if (index > -1) {
            return arr.splice(index, 1)
        }
    }
}

function getFirstComponentChild(children) {
    if (Array.isArray(children)) {
        for (let i = 0; i < children.length; i++) {
            let c = children[i];
            if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
                return c
            }
        }
    }
}

function getComponentName(opts) {
    return opts && (opts.Ctor.options.name || opts.tag)
}

const keepAlive = {
    name: 'keep-alive',
    abstract: true,

    props: {
        include: patternTypes,
        exclude: patternTypes,
        max: [String, Number]
    },

    created: function created() {
        this.cache = Object.create(null);
        this.keys = [];
        this.$emit('getInstance', this);
    },

    destroyed: function destroyed() {
        for (let key in this.cache) {
            pruneCacheEntry(this.cache, key, this.keys);
        }
    },

    mounted: function mounted() {
        let this$1 = this;

        this.$watch('include', function (val) {
            pruneCache(this$1, function (name) {
                return matches(val, name);
            });
        });
        this.$watch('exclude', function (val) {
            pruneCache(this$1, function (name) {
                return !matches(val, name);
            });
        });
    },

    render: function render() {
        let slot = this.$slots.default;
        let vnode = getFirstComponentChild(slot);
        let componentOptions = vnode && vnode.componentOptions;
        if (componentOptions) {
            if (componentOptions.Ctor) {
                vnode._cid = componentOptions.Ctor.cid;//记录cid
            }
            // check pattern
            let name = getComponentName(componentOptions);
            let ref = this;
            let include = ref.include;
            let exclude = ref.exclude;
            if (
                // not included
                (include && (!name || !matches(include, name))) ||
                // excluded
                (exclude && name && matches(exclude, name))
            ) {
                return vnode
            }

            let ref$1 = this;
            let cache = ref$1.cache;
            let keys = ref$1.keys;
            let key = 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]) {
                //判断cid是否相同, 不同则有过热重载的reload, 需要重建缓存
                if (vnode._cid === cache[key]._cid) {
                    vnode.componentInstance = cache[key].componentInstance;
                    // make current key freshest
                    remove(keys, key);
                    keys.push(key);
                } else {
                    cache[key].componentInstance.$destroy();
                    cache[key] = vnode;
                }

            } 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])
    }
};
//只在开发模式下生效
if (process.env.NODE_ENV === "development") {
    Vue.component('keep-alive', keepAlive);
}


@alexxiyang
Copy link

Add cid comparison.

import Vue from "vue";

let patternTypes = [String, RegExp, Array];

function pruneCacheEntry(cache, key, keys, current) {
    let cached$$1 = cache[key];
    if (cached$$1 && (!current || cached$$1.tag !== current.tag)) {
        cached$$1.componentInstance.$destroy();
    }
    cache[key] = null;
    remove(keys, key);
}

function pruneCache(keepAliveInstance, filter) {
    let cache = keepAliveInstance.cache;
    let keys = keepAliveInstance.keys;
    let _vnode = keepAliveInstance._vnode;
    for (let key in cache) {
        let cachedNode = cache[key];
        if (cachedNode) {
            let name = getComponentName(cachedNode.componentOptions);
            if (name && !filter(name)) {
                pruneCacheEntry(cache, key, keys, _vnode);
            }
        }
    }
}

function matches(pattern, name) {
    if (Array.isArray(pattern)) {
        return pattern.indexOf(name) > -1
    } else if (typeof pattern === 'string') {
        return pattern.split(',').indexOf(name) > -1
    } else if (isRegExp(pattern)) {
        return pattern.test(name)
    }
    /* istanbul ignore next */
    return false
}

function isDef(v) {
    return v !== undefined && v !== null
}

/**
 * Remove an item from an array.
 */
function remove(arr, item) {
    if (arr.length) {
        var index = arr.indexOf(item);
        if (index > -1) {
            return arr.splice(index, 1)
        }
    }
}

function getFirstComponentChild(children) {
    if (Array.isArray(children)) {
        for (let i = 0; i < children.length; i++) {
            let c = children[i];
            if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
                return c
            }
        }
    }
}

function getComponentName(opts) {
    return opts && (opts.Ctor.options.name || opts.tag)
}

const keepAlive = {
    name: 'keep-alive',
    abstract: true,

    props: {
        include: patternTypes,
        exclude: patternTypes,
        max: [String, Number]
    },

    created: function created() {
        this.cache = Object.create(null);
        this.keys = [];
        this.$emit('getInstance', this);
    },

    destroyed: function destroyed() {
        for (let key in this.cache) {
            pruneCacheEntry(this.cache, key, this.keys);
        }
    },

    mounted: function mounted() {
        let this$1 = this;

        this.$watch('include', function (val) {
            pruneCache(this$1, function (name) {
                return matches(val, name);
            });
        });
        this.$watch('exclude', function (val) {
            pruneCache(this$1, function (name) {
                return !matches(val, name);
            });
        });
    },

    render: function render() {
        let slot = this.$slots.default;
        let vnode = getFirstComponentChild(slot);
        let componentOptions = vnode && vnode.componentOptions;
        if (componentOptions) {
            if (componentOptions.Ctor) {
                vnode._cid = componentOptions.Ctor.cid;//记录cid
            }
            // check pattern
            let name = getComponentName(componentOptions);
            let ref = this;
            let include = ref.include;
            let exclude = ref.exclude;
            if (
                // not included
                (include && (!name || !matches(include, name))) ||
                // excluded
                (exclude && name && matches(exclude, name))
            ) {
                return vnode
            }

            let ref$1 = this;
            let cache = ref$1.cache;
            let keys = ref$1.keys;
            let key = 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]) {
                //判断cid是否相同, 不同则有过热重载的reload, 需要重建缓存
                if (vnode._cid === cache[key]._cid) {
                    vnode.componentInstance = cache[key].componentInstance;
                    // make current key freshest
                    remove(keys, key);
                    keys.push(key);
                } else {
                    cache[key].componentInstance.$destroy();
                    cache[key] = vnode;
                }

            } 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])
    }
};
//只在开发模式下生效
if (process.env.NODE_ENV === "development") {
    Vue.component('keep-alive', keepAlive);
}

最好不要粘贴大段的代码,尽量把代码分段解释。

@rightaway
Copy link

@sodatea Is it possible to fix this in Vue 2? Or at least for Vue 3?

@mzongx
Copy link

mzongx commented Mar 21, 2024

use transition。eg:
<transition mode="out-in"> <keep-alive :include="cacheView"> <router-view :key="key"></router-view> </keep-alive> </transition>

@zifengb
Copy link

zifengb commented Jul 11, 2024

use transition。eg: <transition mode="out-in"> <keep-alive :include="cacheView"> <router-view :key="key">router-view> keep-alive> transition>

My project use this way,But it does not work~ 😔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests