/** * 将列表转换为树节点 * 注:该函数默认树的根节点只有一个,如果有多个,则返回一个数组 * @param {Array.<Object>} list 树节点列表 * @param {Object} [options] 其他选项 * @param {Function} [options.isRoot] 判断节点是否为根节点。默认根节点的父节点为空 * @param {Function} [options.bridge=returnItself] 桥接函数,默认返回自身 * @returns {Object|Array.<String>} 树节点,或是树节点列表 */ export function listToTree( list, { isRoot = node => !node.parentId, bridge = returnItself } = {}, ) { const res = list.reduce((root, _sub) => { if (isRoot(sub)) { root.push(sub) return root } const sub = bridge(_sub) for (let _parent of list) { const parent = bridge(_parent) if (sub.parentId === parent.id) { parent.child = parent.child || [] parent.child.push(sub) return root } } return root }, []) // 根据顶级节点的数量决定如何返回 const len = res.length if (len === 0) return {} if (len === 1) return res[0] return res }
抽取通用的树结构遍历逻辑
首先,明确一点,树结构的完全遍历是通用的,大致实现基本如下
遍历顶级树节点
遍历树节点的子节点列表
递归调用函数并传入子节点
/** * 返回第一个参数的函数 * 注:一般可以当作返回参数自身的函数,如果你只关注第一个参数的话 * @param {Object} obj 任何对象 * @returns {Object} 传入的第一个参数 */ export function returnItself(obj) { return obj } /** * 遍历并映射一棵树的每个节点 * @param {Object} root 树节点 * @param {Object} [options] 其他选项 * @param {Function} [options.before=returnItself] 遍历子节点之前的操作。默认返回自身 * @param {Function} [options.after=returnItself] 遍历子节点之后的操作。默认返回自身 * @param {Function} [options.paramFn=(node, args) => []] 递归的参数生成函数。默认返回一个空数组 * @returns {INode} 递归遍历后的树节点 */ export function treeMapping( root, { before = returnItself, after = returnItself, paramFn = (node, ...args) => [], } = {}, ) { /** * 遍历一颗完整的树 * @param {INode} node 要遍历的树节点 * @param {...Object} [args] 每次递归遍历时的参数 */ function _treeMapping(node, ...args) { // 之前的操作 let _node = before(node, ...args) const childs = _node.child if (arrayValidator.isEmpty(childs)) { return _node } // 产生一个参数 const len = childs.length for (let i = 0; i < len; i++) { childs[i] = _treeMapping(childs[i], ...paramFn(_node, ...args)) } // 之后的操作 return after(_node, ...args) } return _treeMapping(root) }
使用 treeMapping 遍历树并打印
const tree = { uid: 1, childrens: [ { uid: 2, parent: 1, childrens: [{ uid: 3, parent: 2 }, { uid: 4, parent: 2 }], }, { uid: 5, parent: 1, childrens: [{ uid: 6, parent: 5 }, { uid: 7, parent: 5 }], }, ], } // 桥接函数 const bridge = bridge({ id: 'uid', parentId: 'parent', child: 'childrens', }) treeMapping(tree, { // 进行桥接抹平差异 before: bridge, // 之后打印每一个 after(node) { console.log(node) }, })
实现树转列表
当然,我们亦可使用 treeMapping 简单的实现 treeToList,当然,这里考虑了是否计算全路径,毕竟还是要考虑性能的!
/** * 将树节点转为树节点列表 * @param {Object} root 树节点 * @param {Object} [options] 其他选项 * @param {Boolean} [options.calcPath=false] 是否计算节点全路径,默认为 false * @param {Function} [options.bridge=returnItself] 桥接函数,默认返回自身 * @returns {Array.<Object>} 树节点列表 */ export function treeToList( root, { calcPath = false, bridge = returnItself } = {}, ) { const res = [] treeMapping(root, { before(_node, parentPath) { const node = bridge(_node) // 是否计算全路径 if (calcPath) { node.path = (parentPath ? parentPath + ',' : '') + node.id } // 此时追加到数组中 res.push(node) return node }, paramFn: node => (calcPath ? [node.path] : []), }) return res }
现在,我们可以转换任意树结构为列表了