一個朋友問我應該怎麼從一個樹的 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; }