浏览代码

feat(tree): 不维护seq连号,减少数据更新

vian 5 年之前
父节点
当前提交
add76a8f63
共有 3 个文件被更改,包括 128 次插入54 次删除
  1. 2 0
      tree/.eslintrc.js
  2. 82 52
      tree/src/index.ts
  3. 44 2
      tree/tests/tree.ts

+ 2 - 0
tree/.eslintrc.js

@@ -38,5 +38,7 @@ module.exports = {
     '@typescript-eslint/no-shadow': 'error',
     'no-continue': 'off',
     'no-param-reassign': 'off',
+    'no-plusplus': 'off',
+    'class-methods-use-this': 'off',
   },
 };

+ 82 - 52
tree/src/index.ts

@@ -45,6 +45,7 @@ export class Tree {
 
   readonly rootID: string = '-1';
 
+  // 默认初始顺序号。在对树结构进行操作后,没必要保证seq连号,只需保证正确排序就行,以减少需要更新的节点。
   readonly seqStartIndex = 0;
 
   // ID与原始数据条目映射
@@ -199,7 +200,51 @@ export class Tree {
     }
   }
 
-  // 准备插入节点,插入节点前,计算出需要更新的数据。
+  // 递归获取节点
+  getNodesPosterity(treeNodes: TreeNode[], includeSelf = true): TreeNode[] {
+    const rst: TreeNode[] = [];
+    const pushNodes = (nodes: TreeNode[]): void => {
+      nodes.forEach(node => {
+        if (includeSelf || !treeNodes.includes(node)) {
+          rst.push(node);
+        }
+        const children = this.findChilren(node.ID);
+        if (children.length) {
+          pushNodes(children);
+        }
+      });
+    };
+    pushNodes(treeNodes);
+    return rst;
+  }
+
+  // 节选中点块是否是可操作的节点块,选中一批节点进行升降、上下移的时候可能需要用到这个判断。
+  // 以第一个节点为基准,如果后续的节点深度均大于于于等于第一个节点的深度,且节点间是连续的,则此为可操作节点块
+  isOperable(nodes: TreeNode[]): boolean {
+    const baseDepth = nodes[0].ctx.depth();
+    for (let i = 0; i < nodes.length; i++) {
+      const node = nodes[i];
+      const depth = node.ctx.depth();
+      if (depth < baseDepth) {
+        return false;
+      }
+      const nextRowNode = nodes[i + 1];
+      if (nextRowNode && node.ctx.row() + 1 !== nextRowNode.ctx.row()) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  // 从节点块中获取相同深度的节点
+  sameDepthNodes(nodes: TreeNode[], depth: number | None): TreeNode[] {
+    if (!depth) {
+      depth = nodes[0].ctx.depth();
+    }
+    return nodes.filter(node => node.ctx.depth() === depth);
+  }
+
+  // 准备插入节点,插入节点前,计算出需要更新的数据。(可能会直接覆盖插入数据的seq)
   // 可调用完此方法后,将需要更新、插入的数据提交至数据库,成功响应后调用插入节点更新缓存的方法
   prepareInsert(rawData: TreeRaw[]): UpdateData[] {
     const updateData: UpdateData[] = [];
@@ -257,35 +302,17 @@ export class Tree {
     return this.data;
   }
 
-  // 准备删除,获取删除时,需要更新的节点
-  prepareDelete(deleteNodes: TreeNode[]): UpdateData[] {
-    const updateData: UpdateData[] = [];
-    // 获取去掉删除节点后,未删除同层节点需要更新seq的数据
-    const parentIDList = Tree.getParentIDList(deleteNodes);
-    parentIDList.forEach(parentID => {
-      const brothers = this.parentMap[parentID];
-      const tempBrothers = brothers.filter(node => !deleteNodes.includes(node));
-      Tree.sort(tempBrothers);
-      tempBrothers.forEach((node, index) => {
-        if (node.seq !== index) {
-          updateData.push({
-            ID: node.ID,
-            update: { seq: index },
-          });
-        }
-      });
-    });
-    return updateData;
+  // 准备删除,返回所有需要删除的节点,包括嵌套节点
+  prepareDelete(deleteNodes: TreeNode[]): TreeNode[] {
+    return this.getNodesPosterity(deleteNodes);
   }
 
   /**
    * 删除节点
    * @param {TreeNode[]} treeNodes - 要删除的节点,不需要包含嵌套节点
-   * @param {UpdateData[]} updateData - 要更新的数据
    * @return {TreeNode[]} 返回删除节点后,树的data
    */
-  delete(treeNodes: TreeNode[], updateData: UpdateData[]): TreeNode[] {
-    this.updateValue(updateData);
+  delete(treeNodes: TreeNode[]): TreeNode[] {
     const allDeletedNodes: TreeNode[] = [];
     // 递归删除节点
     const deleteNodes = (nodes: TreeNode[]): void => {
@@ -317,7 +344,8 @@ export class Tree {
     return this.data;
   }
 
-  prepareDeleteByID(IDList: string[]): UpdateData[] {
+  // IDList 返回所有需要删除的节点,包括嵌套节点
+  prepareDeleteByID(IDList: string[]): TreeNode[] {
     const deleteNodes: TreeNode[] = [];
     IDList.forEach(ID => {
       const node = this.find(ID);
@@ -330,11 +358,10 @@ export class Tree {
 
   /**
    * 根据ID删除节点
-   * @param {string[]} IDList - 要删除的节点的ID列表
-   * @param {UpdateData[]} updateData - 要更新的数据
+   * @param {string[]} IDList - 要删除的节点的ID列表(不包含嵌套节点ID)
    * @return {TreeNode[]} - 返回被删除的所有节点
    */
-  deleteByID(IDList: string[], updateData: UpdateData[]): TreeNode[] {
+  deleteByID(IDList: string[]): TreeNode[] {
     const deleteNodes: TreeNode[] = [];
     IDList.forEach(ID => {
       const node = this.find(ID);
@@ -342,11 +369,11 @@ export class Tree {
         deleteNodes.push(node);
       }
     });
-    this.delete(deleteNodes, updateData);
+    this.delete(deleteNodes);
     return deleteNodes;
   }
 
-  // 准备上移节点块(连续的兄弟节点)
+  // 准备上移节点块(连续的兄弟节点),注意节点的seq可能不连号
   prepareUpMove(nodes: TreeNode[]): UpdateData[] {
     const updateData: UpdateData[] = [];
     const firstNode = nodes[0];
@@ -354,20 +381,23 @@ export class Tree {
     if (!firstNodePrev) {
       return [];
     }
+    let tempSeq = firstNodePrev.seq;
     nodes.forEach(node => {
+      const orgSeq = node.seq;
       updateData.push({
         ID: node.ID,
-        update: { seq: node.seq - 1 },
+        update: { seq: tempSeq },
       });
+      tempSeq = orgSeq;
     });
     updateData.push({
       ID: firstNodePrev.ID,
-      update: { seq: firstNodePrev.seq + nodes.length },
+      update: { seq: tempSeq },
     });
     return updateData;
   }
 
-  // 准备下移节点块(连续的兄弟节点)
+  // 准备下移节点块(连续的兄弟节点),注意节点的seq可能不连号
   prepareDownMove(nodes: TreeNode[]): UpdateData[] {
     const updateData: UpdateData[] = [];
     const lastNode = nodes[nodes.length - 1];
@@ -375,15 +405,19 @@ export class Tree {
     if (!lastNodeNext) {
       return [];
     }
-    nodes.forEach(node => {
+    let tempSeq = lastNodeNext.seq;
+    for (let i = nodes.length - 1; i >= 0; i--) {
+      const node = nodes[i];
+      const orgSeq = node.seq;
       updateData.push({
         ID: node.ID,
-        update: { seq: node.seq + 1 },
+        update: { seq: tempSeq },
       });
-    });
+      tempSeq = orgSeq;
+    }
     updateData.push({
       ID: lastNodeNext.ID,
-      update: { seq: lastNodeNext.seq - nodes.length },
+      update: { seq: tempSeq },
     });
     return updateData;
   }
@@ -394,7 +428,7 @@ export class Tree {
     this.reSortData(nodes);
   }
 
-  // 准备升级节点块(连续的兄弟节点)
+  // 准备升级节点块(连续的兄弟节点),不维护seq连号
   prepareUpLevel(nodes: TreeNode[]): UpdateData[] {
     const updateData: UpdateData[] = [];
     const firstNode = nodes[0];
@@ -411,12 +445,17 @@ export class Tree {
       });
     });
     const parentNextBrothers = parent.ctx.nextBrothers();
-    parentNextBrothers.forEach((node, index) => {
-      updateData.push({
-        ID: node.ID,
-        update: { seq: baseSeq + index + nodes.length },
+    // 因为seq可能是不连号的,如果上移的最末节点seq,小于下一节点的seq,那就不更新所有下兄弟节点的seq,减少更新的数据量
+    const lastNodeCurSeq = updateData[updateData.length - 1].update.seq;
+    const firstBrohter = parentNextBrothers[0];
+    if (lastNodeCurSeq && lastNodeCurSeq >= firstBrohter.seq) {
+      parentNextBrothers.forEach((node, index) => {
+        updateData.push({
+          ID: node.ID,
+          update: { seq: baseSeq + index + nodes.length },
+        });
       });
-    });
+    }
     // 最末节点的所有后兄弟节点,成为最末节点的子节点
     const lastNodeNextBrothers = lastNode.ctx.nextBrothers();
     lastNodeNextBrothers.forEach((node, index) => {
@@ -450,11 +489,10 @@ export class Tree {
     this.resortDataByID([orgParentID, newParentID, lastNode.ID]);
   }
 
-  // 准备降级节点块(连续的兄弟节点)
+  // 准备降级节点块(连续的兄弟节点),不维护seq连号
   prepareDownLevel(nodes: TreeNode[]): UpdateData[] {
     const updateData: UpdateData[] = [];
     const firstNode = nodes[0];
-    const lastNode = nodes[nodes.length - 1];
     const prevNode = this.findPrev(firstNode.ID);
     if (!prevNode) {
       return [];
@@ -470,14 +508,6 @@ export class Tree {
         update: { parentID: prevNode.ID, seq: baseSeq + index },
       });
     });
-    // 最末节点的后兄弟节点们,重新设置seq
-    const lastNodeNextBrothers = lastNode.ctx.nextBrothers();
-    lastNodeNextBrothers.forEach((node, index) => {
-      updateData.push({
-        ID: node.ID,
-        update: { seq: prevNode.seq + 1 + index },
-      });
-    });
     return updateData;
   }
 

+ 44 - 2
tree/tests/tree.ts

@@ -72,6 +72,48 @@ describe('Tree', () => {
     const node = tree.findPrev('6');
     expect(node).to.have.property('ID', '7');
   });
+
+  it('getNodesPosterity', () => {
+    const node1 = tree.find('1');
+    const node2 = tree.find('2');
+    if (node1 && node2) {
+      const nodes = tree.getNodesPosterity([node1, node2]);
+      const IDList = getIDList(nodes);
+      expect(IDList).to.have.ordered.members(['1', '7', '8', '6', '2', '9']);
+
+      const exNodes = tree.getNodesPosterity([node1, node2], false);
+      const exIDList = getIDList(exNodes);
+      expect(exIDList).to.have.ordered.members(['7', '8', '6', '9']);
+    }
+  });
+
+  it('isOperable', () => {
+    const node1 = tree.find('1');
+    const node2 = tree.find('2');
+    const node3 = tree.find('3');
+    const node6 = tree.find('6');
+    const node7 = tree.find('7');
+    const node8 = tree.find('8');
+    if (node1 && node2 && node3 && node6 && node7 && node8) {
+      expect(tree.isOperable([node1, node2, node3])).to.be.equal(false);
+      expect(tree.isOperable([node1, node3, node2])).to.be.equal(false);
+
+      expect(tree.isOperable([node7, node6, node8])).to.be.equal(false);
+      expect(tree.isOperable([node7, node8, node6])).to.be.equal(true);
+    }
+  });
+
+  it('sameDepthNodes-empty-depth', () => {
+    const nodes = tree.sameDepthNodes(tree.data, null);
+    const IDList = getIDList(nodes);
+    expect(IDList).to.have.ordered.members(['1', '3', '2', '4', '5']);
+  });
+
+  it('sameDepthNodes-specific-depth', () => {
+    const nodes = tree.sameDepthNodes(tree.data, 1);
+    const IDList = getIDList(nodes);
+    expect(IDList).to.have.ordered.members(['7', '6', '9']);
+  });
 });
 
 describe('Tree change', () => {
@@ -178,8 +220,8 @@ describe('Tree change', () => {
   it('delete', () => {
     const tree = new Tree(cloneDeep(rawData));
     const deleteNodes: TreeNode[] = [tree.find('1') as TreeNode];
-    const updateData = tree.prepareDelete(deleteNodes);
-    tree.delete(deleteNodes, updateData);
+    // const updateData = tree.prepareDelete(deleteNodes);
+    tree.delete(deleteNodes);
     const IDList = tree.data.map(item => item.ID);
     expect(IDList).to.have.ordered.members(['3', '2', '9', '4', '5']);
   });