vuejs / composition-api

Composition API plugin for Vue 2
https://composition-api.vuejs.org/
MIT License
4.19k stars 343 forks source link

【Bug】Memory Leak cause by toVue3ComponentInstance #991

Closed dennis-leee closed 5 months ago

dennis-leee commented 7 months ago

[Cause] i found the logic in toVue3ComponentInstance function it use a WeakMap to cache to the map of Vue2 instance to fake-Vue3 instance But the fake-Vue3 instance has a property named proxy that references to the Vue2 instance. This proxy forms a strong reference, which prevent Vue2 instance to be GC, And If the Vue2 instance is not GC, then the weakmap mapping will not be automatically deleted, then the fake-vue3 instance will keep by the WeakMap obj, also the strong reference to vue2 instance will be keep too

image

This is the result of the Memlab memory leak test memlab修复后edit弹窗快照

[Affect] All instances created after Vue-composition-API registration

[Solution] there are to way to fix it;

  1. replace all strong reference of vue2 instance with WeakRef
    function toVue3ComponentInstance(vm) {
    if (instanceMapCache.has(vm)) {
    console.info('get vm from cache');
    return instanceMapCache.get(vm);
    }
    // use WeakRef to reference vue2 instance
    let weakRef = new WeakRef(vm);
    const vmProxy = {
    get proxy() {
        return weakRef.deref() || {};
    },
    set proxy(val) {
        const vm = weakRef.deref() || {};
        const newVal = { ...vm, ...val };
        weakRef = new WeakRef(newVal);
    }
    }.proxy;
    var instance = {
    proxy: vmProxy,
    update: vmProxy.$forceUpdate,
    type: vmProxy.$options,
    uid: vmProxy._uid,
    // $emit is defined on prototype and it expected to be bound
    emit: vmProxy.$emit.bind(vmProxy),
    parent: null,
    root: null, // to be immediately set
    };
    bindCurrentScopeToVM(instance);
    // map vm.$props =
    var instanceProps = [
    'data',
    'props',
    'attrs',
    'refs',
    'vnode',
    'slots',
    ];
    instanceProps.forEach(function(prop) {
    proxy(instance, prop, {
      get: function() {
        return vmProxy['$'.concat(prop)];
      },
    });
    });
    proxy(instance, 'isMounted', {
    get: function() {
      // @ts-expect-error private api
      return vmProxy._isMounted;
    },
    });
    proxy(instance, 'isUnmounted', {
    get: function() {
      // @ts-expect-error private api
      return vmProxy._isDestroyed;
    },
    });
    proxy(instance, 'isDeactivated', {
    get: function() {
      // @ts-expect-error private api
      return vmProxy._inactive;
    },
    });
    proxy(instance, 'emitted', {
    get: function() {
      // @ts-expect-error private api
      return vmProxy._events;
    },
    });
    instanceMapCache.set(vm, instance);
    vm.$on('hook:destroyed', function() { console.info('composition api cache has vm', instanceMapCache.has(vm))  });
    if (vmProxy.$parent) {
    instance.parent = toVue3ComponentInstance(vm.$parent);
    }
    if (vmProxy.$root) {
    instance.root = toVue3ComponentInstance(vm.$root);
    }
    return instance;
    }
  2. delete the map manually
    function toVue3ComponentInstance(vm) {
    ...
    instanceMapCache.set(vm, instance);
    // add this line
    vm.$on('hook:destroyed', function() { instanceMapCache.delete(vm) });
    ...
    }

The first method is not very elegant, so I recommend using the second method

[Other] Since this library has stopped updating now, I just fix it in my own project.

hope this can help u if u meet the problem too.

github-actions[bot] commented 5 months ago

Stale issue message