Просмотр исходного кода

fix: 同层树节点太多的情况下,报栈溢出的错误问题

vian 1 день назад
Родитель
Сommit
46917bbd60
1 измененных файлов с 335 добавлено и 7 удалено
  1. 335 7
      public/web/id_tree.js

+ 335 - 7
public/web/id_tree.js

@@ -25,6 +25,7 @@ var idTree = {
                 return null;
                 return null;
             },
             },
             reSortNodes: function (nodes, recursive) {
             reSortNodes: function (nodes, recursive) {
+
                 var temp = [], first;
                 var temp = [], first;
                 var findFirstNode = function (nodes) {
                 var findFirstNode = function (nodes) {
                     return tools.findNode(nodes, function (node) {
                     return tools.findNode(nodes, function (node) {
@@ -32,14 +33,24 @@ var idTree = {
                     });
                     });
                 };
                 };
                 var moveNode = function (node, orgArray, newArray, newIndex) {
                 var moveNode = function (node, orgArray, newArray, newIndex) {
-                    var next;
-                    orgArray.splice(orgArray.indexOf(node), 1);
-                    newArray.splice(newIndex, 0, node);
-                    if (node.getNextSiblingID() !== -1) {
-                        next = node.nextSibling;
-                        if (next && (orgArray.indexOf(next) >= 0)) {
-                            moveNode(next, orgArray, newArray, newIndex + 1);
+                    var current = node, currentIndex, next;
+                    // 不能递归:同级节点很多时,即使链正确也会超过浏览器调用栈。
+                    while (current) {
+                        currentIndex = orgArray.indexOf(current);
+                        if (currentIndex < 0) {
+                            break;
+                        }
+                        orgArray.splice(currentIndex, 1);
+                        newArray.splice(newIndex, 0, current);
+                        newIndex++;
+
+                        next = current.nextSibling;
+                        // next 不在当前同级数组中,说明它已处理、跨层、缺失、自指或成环。
+                        // 停止当前链,剩余节点交给外层 while 继续显示。
+                        if (!next || orgArray.indexOf(next) < 0) {
+                            break;
                         }
                         }
+                        current = next;
                     }
                     }
                 };
                 };
                 if (nodes.length === 0) {
                 if (nodes.length === 0) {
@@ -926,6 +937,323 @@ var idTree = {
             });
             });
         };
         };
 
 
+        /**
+         * 检查原始树数据,并给出可用于修复数据的 updateDatas。
+         * 最好在 loadDatas 前调用 newCheck(datas),避免错误的关系影响树的构建。
+         * 不传 datas 时检查当前树中保存的原始 data;本方法不会修改原数据。
+         */
+        Tree.prototype.newCheck = function (datas) {
+            var that = this;
+            var idField = this.setting.id;
+            var pidField = this.setting.pid;
+            var nidField = this.setting.nid;
+            var rootId = this.setting.rootId;
+            var source = Array.isArray(datas) ? datas : Object.keys(this.nodes).map(function (key) {
+                return that.nodes[key].data;
+            });
+            var problems = [];
+            var updateMap = Object.create(null);
+            var nodeMap = Object.create(null);
+            var duplicateMap = Object.create(null);
+            var duplicates = [];
+            var stackRisks = [];
+            var records = [];
+
+            var valueKey = function (value) {
+                return String(value);
+            };
+            var sameValue = function (left, right) {
+                return left == right;
+            };
+            var addProblem = function (type, record, field, currentValue, suggestedValue, message) {
+                problems.push({
+                    type: type,
+                    id: record ? record.id : undefined,
+                    dataIndex: record ? record.index : undefined,
+                    rowNumber: record ? record.index + 1 : undefined,
+                    // 保留对象引用供控制台展开,不能 JSON.stringify:Node 包含 tree 循环引用。
+                    nodeData: record ? record.data : null,
+                    field: field || '',
+                    currentValue: currentValue,
+                    suggestedValue: suggestedValue,
+                    message: message
+                });
+            };
+            var suggestUpdate = function (record, field, value) {
+                var key = valueKey(record.id);
+                if (!updateMap[key]) {
+                    updateMap[key] = {};
+                    updateMap[key][idField] = record.id;
+                }
+                updateMap[key][field] = value;
+            };
+
+            source.forEach(function (data, index) {
+                // 兼容 newCheck(rawDatas) 和 newCheck(tree.items) 两种调用方式。
+                // Tree Node 中包含 node.tree.nodes -> node 的循环关系,只检查其原始 data。
+                var rawData = data && typeof data.getID === 'function' && data.data ? data.data : data;
+                var record = {
+                    data: rawData,
+                    id: rawData[idField],
+                    pid: rawData[pidField],
+                    nid: rawData[nidField],
+                    index: index
+                };
+                var key = valueKey(record.id);
+                if (nodeMap[key]) {
+                    var firstRecord = nodeMap[key];
+                    duplicateMap[key] = true;
+                    duplicates.push({
+                        id: record.id,
+                        first: {
+                            dataIndex: firstRecord.index,
+                            rowNumber: firstRecord.index + 1,
+                            data: firstRecord.data
+                        },
+                        duplicate: {
+                            dataIndex: record.index,
+                            rowNumber: record.index + 1,
+                            data: record.data
+                        }
+                    });
+                    addProblem('duplicate-id', record, idField, record.id, undefined,
+                        'ID ' + record.id + ' 重复:datas[' + firstRecord.index + '](第 ' +
+                        (firstRecord.index + 1) + ' 条)和 datas[' + record.index + '](第 ' +
+                        (record.index + 1) + ' 条)是两个不同节点,请修改其中一个节点的 ID');
+                    return;
+                }
+                nodeMap[key] = record;
+                records.push(record);
+            });
+
+            // 先修正无效父节点,再检查父子关系是否成环。
+            records.forEach(function (record) {
+                var parent = nodeMap[valueKey(record.pid)];
+                record.fixedPid = record.pid;
+                if (sameValue(record.pid, rootId)) {
+                    return;
+                }
+                if (sameValue(record.pid, record.id) || !parent) {
+                    record.fixedPid = rootId;
+                    suggestUpdate(record, pidField, rootId);
+                    addProblem(sameValue(record.pid, record.id) ? 'parent-self' : 'parent-missing',
+                        record, pidField, record.pid, rootId,
+                        sameValue(record.pid, record.id) ? '父节点指向自身,应改为根节点' : '父节点不存在,应改为根节点');
+                }
+            });
+
+            records.forEach(function (record) {
+                var current = record;
+                var path = [];
+                var pathIndex = Object.create(null);
+                while (current && !sameValue(current.fixedPid, rootId)) {
+                    var currentKey = valueKey(current.id);
+                    if (pathIndex[currentKey] !== undefined) {
+                        var breaker = path[path.length - 1];
+                        var cycle = path.slice(pathIndex[currentKey]).map(function (item) {
+                            return item.id;
+                        });
+                        breaker.fixedPid = rootId;
+                        suggestUpdate(breaker, pidField, rootId);
+                        addProblem('parent-cycle', breaker, pidField, breaker.pid, rootId,
+                            '父节点形成环:' + cycle.join(' -> ') + ',建议断开此节点并改为根节点');
+                        break;
+                    }
+                    pathIndex[currentKey] = path.length;
+                    path.push(current);
+                    current = nodeMap[valueKey(current.fixedPid)];
+                }
+            });
+
+            // 按修正后的父节点分组,在每组内恢复为一条完整、无环、无分叉的兄弟链。
+            var groups = Object.create(null);
+            records.forEach(function (record) {
+                var groupKey = valueKey(record.fixedPid);
+                if (!groups[groupKey]) {
+                    groups[groupKey] = [];
+                }
+                groups[groupKey].push(record);
+            });
+
+            Object.keys(groups).forEach(function (groupKey) {
+                var siblings = groups[groupKey];
+                var siblingMap = Object.create(null);
+                var nextMap = Object.create(null);
+                var incoming = Object.create(null);
+                var visited = Object.create(null);
+                var ordered = [];
+                var stackRiskLimit = 1000;
+
+                siblings.forEach(function (record) {
+                    siblingMap[valueKey(record.id)] = record;
+                    incoming[valueKey(record.id)] = 0;
+                });
+                siblings.forEach(function (record) {
+                    var next = sameValue(record.nid, rootId) ? null : nodeMap[valueKey(record.nid)];
+                    if (next && siblingMap[valueKey(next.id)] && !sameValue(next.id, record.id)) {
+                        nextMap[valueKey(record.id)] = next;
+                        incoming[valueKey(next.id)]++;
+                    } else {
+                        nextMap[valueKey(record.id)] = null;
+                        if (!sameValue(record.nid, rootId)) {
+                            addProblem(next && sameValue(next.id, record.id) ? 'next-self' : 'next-invalid',
+                                record, nidField, record.nid, undefined,
+                                next ? 'NextSiblingID 指向不同父节点下的节点或自身' : 'NextSiblingID 指向不存在的节点');
+                        }
+                    }
+                });
+
+                Object.keys(incoming).forEach(function (key) {
+                    if (incoming[key] > 1) {
+                        var target = siblingMap[key];
+                        addProblem('next-branch', target, nidField, target.id, undefined,
+                            '有 ' + incoming[key] + ' 个节点同时指向该节点,兄弟链产生分叉');
+                    }
+                });
+
+                var appendChain = function (start) {
+                    var current = start;
+                    var chain = [];
+                    var chainIndex = Object.create(null);
+                    while (current && !visited[valueKey(current.id)]) {
+                        chainIndex[valueKey(current.id)] = chain.length;
+                        chain.push(current);
+                        visited[valueKey(current.id)] = true;
+                        ordered.push(current);
+                        current = nextMap[valueKey(current.id)];
+                    }
+                    if (current && chainIndex[valueKey(current.id)] !== undefined) {
+                        var cycleStart = chainIndex[valueKey(current.id)];
+                        var cycleRecords = chain.slice(cycleStart);
+                        var cycleEnd = cycleRecords[cycleRecords.length - 1];
+                        addProblem('next-cycle', cycleEnd, nidField, cycleEnd.nid, undefined,
+                            '兄弟节点的 NextSiblingID 形成环:' + cycleRecords.map(function (item) {
+                                return item.id;
+                            }).concat([current.id]).join(' -> ') + ',请按 updateDatas 修复');
+                    }
+                    if (chain.length > stackRiskLimit) {
+                        stackRisks.push({
+                            parentId: start.fixedPid,
+                            chainLength: chain.length,
+                            firstNodeId: chain[0].id,
+                            lastNodeId: chain[chain.length - 1].id,
+                            message: '同一父节点下连续兄弟链有 ' + chain.length +
+                                ' 个节点,旧版 moveNode 逐节点递归会导致 Maximum call stack size exceeded;请使用迭代版 moveNode'
+                        });
+                    }
+                };
+                siblings.forEach(function (record) {
+                    if (incoming[valueKey(record.id)] === 0) {
+                        appendChain(record);
+                    }
+                });
+                // 剩余节点属于环,或处于已合并链的未访问部分;按原数据顺序接到末尾。
+                siblings.forEach(function (record) {
+                    appendChain(record);
+                });
+
+                ordered.forEach(function (record, index) {
+                    var expectedNid = index === ordered.length - 1 ? rootId : ordered[index + 1].id;
+                    if (!sameValue(record.nid, expectedNid)) {
+                        suggestUpdate(record, nidField, expectedNid);
+                        addProblem('next-order', record, nidField, record.nid, expectedNid,
+                            '兄弟链不连续,按可恢复顺序修改 NextSiblingID');
+                    }
+                });
+            });
+
+            var updateDatas = Object.keys(updateMap).map(function (key) {
+                return { type: 'update', data: updateMap[key] };
+            });
+            var result = {
+                valid: problems.length === 0,
+                canAutoFix: Object.keys(duplicateMap).length === 0,
+                problems: problems,
+                duplicates: duplicates,
+                stackRisks: stackRisks,
+                updateDatas: updateDatas
+            };
+
+            if (typeof console !== 'undefined') {
+                if (result.valid) {
+                    console.log('树结构检查通过,共 ' + records.length + ' 个节点');
+                } else {
+                    console.group('树结构检查:发现 ' + problems.length + ' 个问题');
+                    console.table(problems);
+                    duplicates.forEach(function (item, index) {
+                        console.group('重复 ID 问题 ' + (index + 1) + ':ID = ' + item.id);
+                        console.error('以下两个节点的 ID 相同,必须修改其中一个:');
+                        console.log('节点 A:datas[' + item.first.dataIndex + '],第 ' +
+                            item.first.rowNumber + ' 条原始数据', item.first.data);
+                        console.log('节点 B:datas[' + item.duplicate.dataIndex + '],第 ' +
+                            item.duplicate.rowNumber + ' 条原始数据', item.duplicate.data);
+                        console.groupEnd();
+                    });
+                    console.log('建议修改数据(newCheck 只生成建议,不会自动修改):');
+                    console.table(updateDatas.map(function (item) { return item.data; }));
+                    if (!result.canAutoFix) {
+                        console.warn('存在重复 ID,必须先人工分配新 ID,其他修改建议才可安全应用');
+                    }
+                    console.groupEnd();
+                }
+                if (stackRisks.length) {
+                    console.group('树排序递归栈风险:发现 ' + stackRisks.length + ' 条过长兄弟链');
+                    console.table(stackRisks);
+                    console.warn('这不一定是数据错误;旧版递归 moveNode 会因此堆栈溢出,改为迭代实现即可正常显示');
+                    console.groupEnd();
+                }
+            }
+            return result;
+        };
+
+        //检查树结构数据有没问题
+        Tree.prototype.check = function (roots) {
+            return isValid(roots);
+            function isValid(nodes) {
+                for (let node of nodes) {
+                    if (node.data.ParentID != -1 &&
+                        (!node.parent || node.parent.data.ID !== node.data.ParentID)) {
+                        console.log(`${node.serialNo() + 1}:${node.data.name} parent对应错误`);
+                        return false;
+                    }
+                    if (node.data.ParentID == -1 && node.parent) {
+                        console.log(`${node.serialNo() + 1}:${node.data.name} 不应有parent`);
+                        return false;
+                    }
+                    if (node.data.NextSiblingID != -1 &&
+                        (!node.nextSibling || node.nextSibling.data.ID !== node.data.NextSiblingID)) {
+                        console.log(`${node.serialNo() + 1}:${node.data.name} next对应错误`);
+                        return false;
+                    }
+                    if (node.data.NextSiblingID == -1 && node.nextSibling) {
+                        console.log(`${node.serialNo() + 1}:${node.data.name} 不应有next`);
+                        return false;
+                    }
+                    let sameDepthNodes = node.parent ? node.parent.children : roots,
+                        nodeIdx = sameDepthNodes.indexOf(node),
+                        nextIdx = sameDepthNodes.indexOf(node.nextSibling);
+                    if (nodeIdx != -1 && nextIdx != -1 && nodeIdx > nextIdx) {
+                        console.log(`${node.serialNo() + 1}:${node.data.name} node索引大于next索引`);
+                        return false;
+                    }
+                    // nextSibling跟parent children的下一节点对应不上
+                    if (nodeIdx != -1 &&
+                        (nodeIdx === sameDepthNodes.length - 1 && nextIdx != -1) ||
+                        (nodeIdx !== sameDepthNodes.length - 1 && nodeIdx + 1 !== nextIdx)) {
+                        console.log(`${node.serialNo() + 1}:${node.data.name} nextSibling与树显示的下一节点对应不上`);
+                        return false;
+                    }
+                    if (node.children.length) {
+                        let v = isValid(node.children);
+                        if (!v) {
+                            return false;
+                        }
+                    }
+                }
+                return true;
+            }
+        };
+
         return new Tree(setting);
         return new Tree(setting);
     },
     },
     updateType: { update: 'update', new: 'new', delete: 'delete' }
     updateType: { update: 'update', new: 'new', delete: 'delete' }