一个朋友问我应该怎么从一个树的 JSON 数组生成 HTML,使用 <ul>
和 <li>
来构建页面元素。因而我简单的画了个树型结构图node
而后写了对应的模拟数据(JavaScript 对象)算法
const data = { name: "A", nodes: [ { name: "B", nodes: [{ name: "F" }] }, { name: "C" }, { name: "D", nodes: [ { name: "G" }, { name: "H" }, { name: "I", nodes: [{ name: "J" }, { name: "K" }] } ] }, { name: "E" } ] };
最后写了一个递归,生成了 HTML 的树型结构。本来是用 JavaScript ES6 写的,为了代表数据结构,这里改用 TypeScript 来写:typescript
interface INode { name: string; nodes?: INode[]; } function makeTree(roots: INode[]): JQuery<HTMLElement> { function makeNode(node: INode): JQuery<HTMLElement> { const $div = $("<div>").text(node.name || ""); const $li = $("<li>").append($div); if (node.nodes && node.nodes.length) { $li.append(makeNodeList(node.nodes)); } return $li; } function makeNodeList(nodes: INode[]): JQuery<HTMLElement> { return nodes .map(child => makeNode(child)) .reduce(($ul, $li) => { return $ul.append($li); }, $("<ul>")); } return makeNodeList(roots); }
效果仍是蛮不错的编程
看看源码(转译成 JS 以后的):http://jsfiddle.net/y7bw4yj2/数组
而后朋友说没看明白,好吧,那我从头讲起数据结构
树形数据的遍历有两种方法,你们都知道:广度遍历和深度遍历。通常状况下,广度遍历是采用队列来实现,而深度遍历刚更适合使用递归来实现。app
从图上大体能够理解广度遍历的过程:dom
function travelWidely(roots: INode[]) { const queue: INode[] = [...roots]; while (queue.length) { const node = queue.shift()!; // 打印节点名称及其子节点数 console.log(`${node.name} ${node.nodes && node.nodes.length || ""}`); if (node.nodes && node.nodes.length) { queue.push(...node.nodes); } } } // 开始遍历 travelWidely([data]);
const node = queue.shift()!
,这后面的!
后缀表示声明其结果不为undefined
或null
。这是一个 TypeScript 语法。因为.shift()
在数组中没有元素时会返回undefined
,因此其返回类型被声明为INode | undefined
,因为从逻辑能够保证.shift()
必定会返回一个节点对象,因此这里用!
后缀忽略类型中的undefined
部分,使node
的类型被推导为INode
。ide
代码里稍难理解一点的是要注意 queue
的内容和长度随时在变化。若是想使用 for
代替 while
循环,节点序号会因 .shift()
而不断变化,因此 i < queue.length
这样的判断是错误的。函数
深度遍历是一个递归过程,递归一直是编程的难点。
递归是一个循环往复的处理过程,它有两个点须要注意:
以树节点为例,咱们指望处理过程是处理(打印)一个树结点,即 printNode(node: INode)
。那么它的
printNode(children[i])
用一段伪代码描述这一过程
function printNode(node: INode) { // 处理该节点 console.log(node.name); // 递归调用点:循环对子节点调用 printNode node.nodes!.forEach(child => printNode(child)); // 递归结束点:循环完成,return }
上面两句代码就完成了递归过程,但实际上状况还要复杂些,由于要处理入口和容错。
// 注意参数支持传入单根或多根, // 若是像 travelWidely 那样只支持多根(单根是特例)也是能够的 function travelDeeply(roots: INode | INode[]) { function printNode(node: INode) { console.log(`${node.name} ${node.nodes && node.nodes.length || ""}`); if (node.nodes && node.nodes.length) { // 依次对子节点递归调用 printNode node.nodes.forEach(child => printNode(child)); } } // 这里 printNode 和 node => printNode(node) 等价 (Array.isArray(roots) ? roots : [roots]).forEach(printNode); } // 开始遍历 travelDeeply(data);
关于递归,我正好在慕课网上讲生成数据解决方案的时候讲到了,有兴趣能够看看。
上面两种遍历都讲到了,可是还没讲完——由于两种遍历都是以打印为例,而咱们的目的是要生成 DOM 树。生成 DOM 树与纯打印信息的不一样之处在于,咱们不只要使用节点信息,还要从节点信息生成 DOM 返回出来。
此次先讲深度遍历,由于递归更容易实现。递归自己具备层次信息,每进入一个递归调用点,就会深刻一层,每离开一个递归结束点,就会减小一层。因此这个算法自己可以保留结构信息,相应代码也会更容易实现。并且在本文一开始,就已经实现出来了。
须要注意的一点是那段代码用了两个函数来完成递归过程:
makeNode
处理单个节点,它调用 makeNodeList
处理子节点列表makeNodeList
遍历节点列表,分别对其调用 makeNode
来进行处理makeNode
和 makeNodeList
的相互调用造成了递归,上述两条都是递归调用点,而递归结束点一样也有两条:
makeNode
处理的节点没有子节点时,不会调用 makeNodeList
makeNodeList
中的循环结束时,不会再调用 makeNode
广度遍历的过程是把全部节点扁平化到一个队列中了,这个过程是不可逆 的,换句话说,咱们在处理过程当中丢掉了树形结构信息。而后咱们要生成的 DOM 树,是须要结构信息的——所以,须要将结构信息附加在每一个节点上。这里咱们把生成的 DOM 和数据节点绑定起来,由 DOM 保存结构信息。为此,须要修改一下节点类型
interface INode { name: string; nodes?: INode[]; dom: JQuery; // 附加生成的 DOM }
function makeTreeWidely(roots: INode[]): JQuery { // 从一组节点生成 <ul>,为每一个节点生成并附加 <li>, // 同时将 <li> 到到 <ul> 中保存结构信息 function makeUl(nodes: INode[]) { return nodes .map(node => { const $li = $("<li>") .append($("<div>").text(node.name || "")); node.dom = $li; return $li; }) .reduce(($ul, $li) => $ul.append($li), $("<ul>")); } const $rootUl = makeUl(roots); const queue: INode[] = [...roots]; while (queue.length) { const node = queue.shift()!; if (node.nodes && node.nodes.length) { const $ul = makeUl(node.nodes); node.dom.append($ul); queue.push(...node.nodes); } } return $rootUl; }
虽然这里和上面讲递归遍历 printNode
的时候同样定义了局部函数表达式 makeUl
,但这里没有递归,由于 makeUl
内部没有调用自身,或者某个会调用 makeUl
的函数。
但问题仍是再深刻一点,由于上面的代码改变了原数据。而通常状况下,咱们应该尽可能避免这样的反作用
// 声明一个新结构,它把 INode 和 DOM 组合在一块儿。 // 这个结构将代替 INode 做为队列的元素类型 interface IDomNode { node: INode; dom: JQuery; } function makeTreeWidely(roots: INode[]): JQuery { // convert 将节点数组转换为 IDomNode 数组, // 同时还干了原来 makeUl 干的事情,返回一个 $ul function convert(nodes: INode[]) { const domNodes = nodes .map(node => { const $li = $("<li>") .append($("<div>").text(node.name || "")); return { node, dom: $li }; }); const $ul = domNodes .reduce(($ul, dn) => $ul.append(dn.dom), $("<ul>")); // 将两个数组组成一个元组(对象)返回 return { domNodes, $ul }; } // 解析元组,声明变量 queue 和 $rootUl, // 并分别将 domNodes 和 $ul 的值赋值给 queue 和 $rootUl 两个变量 const { domNodes: queue, $ul: $rootUl } = convert(roots); while (queue.length) { const { node, dom } = queue.shift()!; if (node.nodes && node.nodes.length) { const { domNodes, $ul } = convert(node.nodes); dom.append($ul); queue.push(...domNodes); } } return $rootUl; }