vuejs / composition-api

Composition API plugin for Vue 2

Home Page:https://composition-api.vuejs.org/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

【Bug】Memory Leak cause by toVue3ComponentInstance

dennis-leee opened this issue · comments

[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;
}
  1. 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.