可以这么说,小到一个生命体,大到整个宇宙,世间万物皆是树。
现实生活中最常见的树的例子是家谱,或是公司的组织架构图。
树是一种非顺序数据结构,一种分层数据的抽象模型,它对于存储需要快速查找的数据非常有用。
一个树结构包含一系列存在父子关系的节点。每个节点都有一个父节点(除了顶部的第一个 节点)以及零个或多个子节点。
JS里面没有像Array一样直接的一个Tree对象,也没有处理Tree的函数,比如:遍历、查找、插入、删除等等,因此我们得自己造轮子
1、each
就像Array数组一样,遍历是其他很多的操作的前提,Tree也是一样
while循环栈方式遍历,类似Array.forEach:
/**
* 遍历树结构方法(while循环栈)
* @param {Object|Array} data 传入的数据
* @param {function} handler 回调函数,处理每个节点,参数为当前节点对象,返回false会终止遍历
* @param {String} mode 广度优先搜索/深度优先搜索(wide,deep),默认:广度优先
*/
each: function (data, handler, mode) {
var stack = [].concat(data);
var node, children, handleRes;
while (stack.length) {
node = stack.shift();
children = node.children
handleRes = handler && handler(node);
if (handleRes === false) break;
if (Array.isArray(children)) {
mode == 'deep' ?
[].unshift.apply(stack, children) :
[].push.apply(stack, children);
}
}
}
递归+栈配合方式遍历:
/**
* 遍历树结构方法(递归+栈配合)
* @param {Object|Array} data 传入的数据
* @param {function} handler 回调函数,处理每个节点,参数为当前节点对象,返回false会终止遍历
* @param {String} mode 广度优先/深度优先(wide,deep),默认:广度优先
*/
eachRecursive: function (data, handler, mode) {
var stack = [], index = -1, next, isBreak;
var isDeepMode = mode === 'deep';
var recursive = function (data) {
var node, nodes = [].concat(data), children;
for (var i = 0; i < nodes.length; i++) {
node = nodes[i]
children = node.children || []
isBreak = handler.call(node, node)
if (isBreak === false) break
if (children.length) {
[].push.apply(stack, children)
isDeepMode && recursive(children)
}
index++
}
next = stack[index]
if (isBreak !== false && !isDeepMode && next) recursive(next)
}
recursive(data)
}
特点:
2、map
遍历树并返回新的tree方法,利用递归进行遍历,类似Array.map
/**
* 遍历树并返回新的tree方法,利用递归进行遍历
* @param {Object|Array} data 传入的数据
* @param {function} handler 处理函数,入参为当前节点
* @return {Object|Array} 新的tree数据
*/
map: function (data, handler) {
var stack = [].concat(data);
var getNodes = (data, handler) => {
return data.map(v => {
let node = handler.call(data, v)
if (Array.isArray(v.children) && v.children.length) {
node.children = getNodes(v.children, handler)
}
return node
})
}
return getNodes(stack, handler)
}
3、find
查找树并返回结果方法,类似Array.find
/**
* 查找树并返回结果方法,类似Array.find
* @param {Object|Array} data 传入的数据
* @param {function} handler 处理函数,入参为当前节点
* @return {Object} node节点或null
*/
find: function (data, handler) {
var result = null
this.each(data, node => {
if (handler.call(data, node)) {
result = node
return false
}
})
return result
}
4、filter
过滤树节点方法,类似Array.filter
/**
* 过滤树节点方法,类似Array.filter
* @param {Object|Array} data 传入的数据
* @param {function} handler 处理函数,入参为当前节点
* @return {Object} node节点
*/
filter: function (data, handler) {
var result = [], handleRes
this.each(data, node => {
handleRes = handler.call(data, node)
if (handleRes === false) return false
if (handleRes) result.push(node)
})
return result
}
5、some
匹配某一个节点并返回匹配结果的方法,类似Array.some
/**
* 匹配某一个节点并返回匹配结果的方法,类似Array.some
* @param {Object|Array} data 传入的数据
* @param {function} handler 处理函数,入参为当前节点
* @return {Object} Boolean
*/
some: function (data, handler) {
var result = false
this.each(data, node => {
if (handler.call(data, node)) {
result = true
return false
}
})
return result
}
6、every
匹配每一个节点并返回匹配结果的方法,类似Array.every
/**
* 匹配每一个节点并返回匹配结果的方法,类似Array.every
* @param {Object|Array} data 传入的数据
* @param {function} handler 处理函数,入参为当前节点
* @return {Object} Boolean
*/
every: function (data, handler) {
var result = true
this.each(data, node => {
if (!handler.call(data, node)) {
result = false
return false
}
})
return result
}
7、flat
树的扁平化方法(树 --> 数组),不改变原数据,返回新的数组
/**
* 树的扁平化方法(树 --> 数组),不改变原数据,返回新的数组
* @param {Object} data 传入的数据
* @param {String} mode 广度优先搜索/深度优先搜索,默认:广度优先(wide,deep)
* @param {function} callback 回调函数,处理每个节点,参数为当前节点对象
* @return {Array} result 新的数组
*/
flat: function (data, callback, mode) {
var result = [];
var handleNode = function (node, children) {
callback && callback.apply(node, [node, children]);
result.push(node);
}
this.each(data, handleNode, mode);
return result;
}
8、reduce
树累加器,改变原数据
/**
* 树累加器,改变原数据
* @param {Array|Object} data 数组或对象
* @param {Object} fields 键值对的对象,键:在data新建的键名,值:data里的字段值
*/
reduce: function (data, fieldsObj, callback) {
if (!fieldsObj) return data;
var handleNode = function (node, children) {
// 累加当前节点
for (var key in fieldsObj) {
node[key] = node[key] || [];
node[key].push(node[fieldsObj[key]]);
}
// 回调处理
// callback && callback(node, children
callback && callback.apply(this, arguments);
// 累加children
children && children.length && children.forEach(function (item) {
for (var key in fieldsObj) {
item[key] = [].concat(node[key]);
}
});
};
this.each(data, handleNode);
}
9、completer
补全父级链方法,传入任意节点,通过已有的数据查找并发回包括所有父级的集合
/**
* 补全父级链方法,传入任意节点,通过已有的数据查找并发回包括所有父级的集合
* @param {Array} nodes 节点id数组
* @param {Array} data 原始数据(未转换tree)
* @return {Array} result 包括所有父级的集合
*/
completer: function (nodeIds, data) {
if (!data) return [];
if (!Array.isArray(nodeIds) || !nodeIds.length) return [];
var result = [];
var resultMap = {};
var dataMap = data.reduce((total, curr) => {
total[curr.id] = curr
return total
}, {})
var getNode = function (id) {
var node = dataMap[id];
if (!node || resultMap[id]) return
resultMap[id] = 1;
result.push(node);
getNode(node.parentId);
};
// 生成结果
nodeIds.forEach(function (id) {
getNode(id);
});
return result;
}
10、append
插入节点方法,传入任意节点ID,在其下插入子节点
/**
* 插入节点方法,传入任意节点ID,在其下插入子节点
* @param {String} nodeId 节点id
* @param {Array} children 插入的字节点
*/
append: function (nodeId, children) {
if (nodeId == undefined) return;
if (!Array.isArray(children) || !children.length) return;
var node = this.find(n => n.id === nodeId)
if (!node) return;
if (!node.children) node.children = [];
[].push.apply(node.children, children)
}
11、TreeFrom
数组 --> 树,利用堆栈转换,不改变数组本身,返回新的对象,支持乱序,类似Array.from
一次遍历实现:
/**
* 数组 --> 树,利用堆栈转换,不改变数组本身,返回新的数组或对象,支持乱序
* @param {Array} data 数组
* @return {Array|Object} 数组或对象
* 原数据要求:top顶点的parentId必须为0或者null/undefined
*/
TreeFrom: function (data) {
if (!data || !data.length) return {};
var copy = data.map(function (v) {
return Object.assign({}, v)
});
var id, pid, node, parent;
var hash = {};
var tops = [];
copy.forEach(function (item) {
id = item.id;
pid = item.parentId;
node = hash[id]
// 如果已存提前存在,证明是父亲节点,先连接他的孩子,支持乱序的重点
if (node) item.children = node.children;
hash[id] = item;
if (pid) {
parent = hash[pid] = hash[pid] || {};
parent.children = (parent.children || []).concat(item);
} else {
tops.push(item);
}
});
return tops;
}
二次遍历实现:
/**
* 数组 --> 树,利用堆栈转换,不改变数组本身,返回新的对象,支持乱序
* @param {Array} data 数组
* @return {Object} 数组或对象
* 原数据要求:top顶点的parentId必须为0或者null/undefined
*/
TreeFrom2: function (data) {
if (!data || !data.length) return {};
var hash = {};
var tops = [];
var node, parent
var copy = data.map(function (v) {
return Object.assign({}, v)
});
// 罗列所有节点
copy.forEach(function (item) {
hash[item.id] = item;
});
// console.log('hash --> ', hash)
// 连接所有节点
for (var item in hash) {
node = hash[item];
parent = hash[node.parentId];
if (!parent) tops.push(node);
else parent.children = (parent.children || []).concat(node);
};
return tops;
}