'Tree view component with drag'n'drop leaks memory

I need to create a TreeView component in Vue.js 3, and I need it to be drag'n'drop editable. The component I came up with is working, but it leaks memory.

Here is the component code, and a usage example:

TreeView.js

var _draggingNode = null
  , _draggingFrom = null
  ;

/**
 * Check whether a node is a descendant of some other node
 * @param {*} node - The node to test
 * @param {*} tree - The ancestor / subtree
 * @returns true if node is a descendant of tree, false otherwise
 */
function isDescendantOf(node, tree) {
  if (node === tree) {
    // Recursion reached the node itself so the node is a child of the original parent
    return true;
  } else if (!tree.children || tree.children.length < 1) {
    // Tree is a leaf, recursion ends an the node is not a descendant
    return false;
  } else {
    return null != tree.children.find(elem => {
      return isDescendantOf(node, elem);
    });
  }
}

var TreeView = {
    //
    props: {
      tree: {
        type: Object,
        required: true
      }
    },
    data() {
      return {
        dragging: null,
        isDraggingOver: false
      }
    },
    computed: {
        hasChildren() {
          return this.tree.children && this.tree.children.length > 0
        }
    },
    //
    methods: {
      dragStart: function (evt, item) {
        _draggingNode = item;
        _draggingFrom = this.tree;
        item.dragging = true;
        evt.stopPropagation();
        item = null;
      },
      dragLeave: function () {
        this.isDraggingOver = false;
      },
      dragOver: function (evt) {
        evt.stopPropagation();
        this.isDraggingOver = true;
        if(_draggingNode === this.tree) {
          //don't drop nodes on themselves
          return true;
        } else if (isDescendantOf(this.tree, _draggingNode)) {
          //parent nodes can't be dropped onto descendants
          return true;
        }
        evt.preventDefault();
        return false;
      },
      dragEnd: function (evt) {
        _draggingNode.dragging = false;
        _draggingNode = null;
        _draggingFrom = null;
        evt.stopPropagation();
      },
      drop: function (evt) {
        var id = _draggingFrom.children.indexOf(_draggingNode);
        _draggingFrom.children.splice(id, 1);
        this.tree.children.push(_draggingNode);
        this.isDraggingOver = false;
        evt.stopPropagation();
      }
    },
    //
    template: 
      `<div class="tree"
        @drop="drop($event)"
        @dragover="dragOver($event)"
        @dragleave="dragLeave()"
        @dragenter.prevent
        :class="{ 'dragging-over': isDraggingOver }">
          <div class="tree-name">
            {{ tree.name }} - {{ hasChildren }}
          </div>
          <ul class="tree-nodes"
            v-if="hasChildren">
              <li draggable="true"
                v-for="(child, index) in tree.children" 
                :key="index"
                :class="{ dragging: child.dragging }"
                @dragstart="dragStart($event, child)"
                @dragend="dragEnd($event)"
                @drop="drop($event, index)"
                class="tree-children ms-3">
                  <v-tree-view :tree="child" />
              </li>
          </ul>
      </div>`
};

export { TreeView }

vue-index.js

import { TreeView } from "TreeView.js"

var _tree = {
    "name": "root",
    "children": [
        { 
            "name" : 1,
            "children": [
                { 
                    "name" : 10,
                    "children": [
                        
                    ]
                },
                { 
                    "name" : 11,
                    "children": [
                        
                    ]
                }
            ]
        },
        {
            "name" : 2,
            "children": [
                { 
                    "name" : 20,
                    "children": [
                        
                    ]
                },
                { 
                    "name" : 21,
                    "children": [
                        
                    ]
                }
            ]
        }
    ]
};

const app = Vue.createApp({
    data() {
        return {
            user: Vue.ref(UserSession),
            tree: _tree
        }
    }
});

app.component('v-tree-view', TreeView);

app.config.errorHandler = (err) => {
    console.error("App error: ", err);
}

app.mount('#app');

index.html

<!DOCTYPE html>
<html>
    <head>
        <title>TreeView - test</title>
        <script src="https://unpkg.com/vue@3"></script>
    </head>
    <body>
        <div id="app">
            <h1>The Tree</h1>
            <v-tree-view :tree="tree" />
        </div>
        <script src="js/vue-index.js" type="module"></script>
    </body>
</html>

Here is the leak after a couple of drag'n'drops: Memory leak detected after some drags and drops

Looks like the "splice" where nodes get removed after drop leave some detached elements that retain other objects.

I'm relatively new to Vue.js so I might have missed something basic, but I have some experience with Knockout js where I managed to resolve this kind of things by cleaning event listeners or components manually when the default behaviour didn't meet my needs. Can I do this in Vuejs? (i.e. in the unMounted method?)



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source