一個(gè)朋友問(wèn)我應(yīng)該怎么從一個(gè)樹(shù)的 JSON 數(shù)組生成 HTML,使用 <ul>
和 <li>
來(lái)構(gòu)建頁(yè)面元素。于是我簡(jiǎn)單的畫(huà)了個(gè)樹(shù)型結(jié)構(gòu)圖
精河網(wǎng)站制作公司哪家好,找創(chuàng)新互聯(lián)建站!從網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)站建設(shè)、微信開(kāi)發(fā)、APP開(kāi)發(fā)、成都響應(yīng)式網(wǎng)站建設(shè)公司等網(wǎng)站項(xiàng)目制作,到程序開(kāi)發(fā),運(yùn)營(yíng)維護(hù)。創(chuàng)新互聯(lián)建站從2013年成立到現(xiàn)在10年的時(shí)間,我們擁有了豐富的建站經(jīng)驗(yàn)和運(yùn)維經(jīng)驗(yàn),來(lái)保證我們的工作的順利進(jìn)行。專注于網(wǎng)站建設(shè)就選創(chuàng)新互聯(lián)建站。
然后寫(xiě)了對(duì)應(yīng)的模擬數(shù)據(jù)(JavaScript 對(duì)象)
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" }
]
};
最后寫(xiě)了一個(gè)遞歸,生成了 HTML 的樹(shù)型結(jié)構(gòu)。原本是用 JavaScript ES6 寫(xiě)的,為了表明數(shù)據(jù)結(jié)構(gòu),這里改用 TypeScript 來(lái)寫(xiě):
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);
}
效果還是蠻不錯(cuò)的
看看源碼(轉(zhuǎn)譯成 JS 之后的):http://jsfiddle.net/y7bw4yj2/
然后朋友說(shuō)沒(méi)看明白,好吧,那我從頭講起
樹(shù)形數(shù)據(jù)的遍歷有兩種方法,大家都知道:廣度遍歷和深度遍歷。一般情況下,廣度遍歷是采用隊(duì)列來(lái)實(shí)現(xiàn),而深度遍歷剛更適合使用遞歸來(lái)實(shí)現(xiàn)。
從圖上大致可以理解廣度遍歷的過(guò)程:
function travelWidely(roots: INode[]) {
const queue: INode[] = [...roots];
while (queue.length) {
const node = queue.shift()!;
// 打印節(jié)點(diǎn)名稱及其子節(jié)點(diǎn)數(shù)
console.log(`${node.name} ${node.nodes && node.nodes.length || ""}`);
if (node.nodes && node.nodes.length) {
queue.push(...node.nodes);
}
}
}
// 開(kāi)始遍歷
travelWidely([data]);
const node = queue.shift()!
,這后面的!
后綴表示聲明其結(jié)果不為undefined
或null
。這是一個(gè) TypeScript 語(yǔ)法。由于.shift()
在數(shù)組中沒(méi)有元素時(shí)會(huì)返回undefined
,所以其返回類型被聲明為INode | undefined
,由于從邏輯可以保證.shift()
一定會(huì)返回一個(gè)節(jié)點(diǎn)對(duì)象,所以這里用!
后綴忽略類型中的undefined
部分,使node
的類型被推導(dǎo)為INode
。
代碼里稍難理解一點(diǎn)的是要注意 queue
的內(nèi)容和長(zhǎng)度隨時(shí)在變化。如果想使用 for
代替 while
循環(huán),節(jié)點(diǎn)序號(hào)會(huì)因 .shift()
而不斷變化,所以 i < queue.length
這樣的判斷是錯(cuò)誤的。
深度遍歷是一個(gè)遞歸過(guò)程,遞歸一直是編程的難點(diǎn)。
遞歸是一個(gè)循環(huán)往復(fù)的處理過(guò)程,它有兩個(gè)點(diǎn)需要注意:
以樹(shù)節(jié)點(diǎn)為例,我們期望處理過(guò)程是處理(打印)一個(gè)樹(shù)結(jié)點(diǎn),即 printNode(node: INode)
。那么它的
printNode(children[i])
用一段偽代碼描述這一過(guò)程
function printNode(node: INode) {
// 處理該節(jié)點(diǎn)
console.log(node.name);
// 遞歸調(diào)用點(diǎn):循環(huán)對(duì)子節(jié)點(diǎn)調(diào)用 printNode
node.nodes!.forEach(child => printNode(child));
// 遞歸結(jié)束點(diǎn):循環(huán)完成,return
}
上面兩句代碼就完成了遞歸過(guò)程,但實(shí)際上情況還要復(fù)雜些,因?yàn)橐幚砣肟诤腿蒎e(cuò)。
// 注意參數(shù)支持傳入單根或多根,
// 如果像 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) {
// 依次對(duì)子節(jié)點(diǎn)遞歸調(diào)用 printNode
node.nodes.forEach(child => printNode(child));
}
}
// 這里 printNode 和 node => printNode(node) 等價(jià)
(Array.isArray(roots) ? roots : [roots]).forEach(printNode);
}
// 開(kāi)始遍歷
travelDeeply(data);
關(guān)于遞歸,我正好在慕課網(wǎng)上講生成數(shù)據(jù)解決方案的時(shí)候講到了,有興趣可以看看。
上面兩種遍歷都講到了,但是還沒(méi)講完——因?yàn)閮煞N遍歷都是以打印為例,而我們的目的是要生成 DOM 樹(shù)。生成 DOM 樹(shù)與純打印信息的不同之處在于,我們不僅要使用節(jié)點(diǎn)信息,還要從節(jié)點(diǎn)信息生成 DOM 返回出來(lái)。
這次先講深度遍歷,因?yàn)檫f歸更容易實(shí)現(xiàn)。遞歸本身具有層次信息,每進(jìn)入一個(gè)遞歸調(diào)用點(diǎn),就會(huì)深入一層,每離開(kāi)一個(gè)遞歸結(jié)束點(diǎn),就會(huì)減少一層。所以這個(gè)算法本身能夠保留結(jié)構(gòu)信息,相應(yīng)代碼也會(huì)更容易實(shí)現(xiàn)。而且在本文一開(kāi)始,就已經(jīng)實(shí)現(xiàn)出來(lái)了。
需要注意的一點(diǎn)是那段代碼用了兩個(gè)函數(shù)來(lái)完成遞歸過(guò)程:
makeNode
處理單個(gè)節(jié)點(diǎn),它調(diào)用 makeNodeList
處理子節(jié)點(diǎn)列表makeNodeList
遍歷節(jié)點(diǎn)列表,分別對(duì)其調(diào)用 makeNode
來(lái)進(jìn)行處理makeNode
和 makeNodeList
的相互調(diào)用形成了遞歸,上述兩條都是遞歸調(diào)用點(diǎn),而遞歸結(jié)束點(diǎn)同樣也有兩條:
makeNode
處理的節(jié)點(diǎn)沒(méi)有子節(jié)點(diǎn)時(shí),不會(huì)調(diào)用 makeNodeList
makeNodeList
中的循環(huán)結(jié)束時(shí),不會(huì)再調(diào)用 makeNode
廣度遍歷的過(guò)程是把所有節(jié)點(diǎn)扁平化到一個(gè)隊(duì)列中了,這個(gè)過(guò)程是不可逆 的,換句話說(shuō),我們?cè)谔幚磉^(guò)程中丟掉了樹(shù)形結(jié)構(gòu)信息。然后我們要生成的 DOM 樹(shù),是需要結(jié)構(gòu)信息的——因此,需要將結(jié)構(gòu)信息附加在每個(gè)節(jié)點(diǎn)上。這里我們把生成的 DOM 和數(shù)據(jù)節(jié)點(diǎn)綁定起來(lái),由 DOM 保存結(jié)構(gòu)信息。為此,需要修改一下節(jié)點(diǎn)類型
interface INode {
name: string;
nodes?: INode[];
dom: JQuery; // 附加生成的 DOM
}
function makeTreeWidely(roots: INode[]): JQuery {
// 從一組節(jié)點(diǎn)生成 <ul>,為每個(gè)節(jié)點(diǎn)生成并附加 <li>,
// 同時(shí)將 <li> 到到 <ul> 中保存結(jié)構(gòu)信息
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
的時(shí)候一樣定義了局部函數(shù)表達(dá)式 makeUl
,但這里沒(méi)有遞歸,因?yàn)?makeUl
內(nèi)部沒(méi)有調(diào)用自身,或者某個(gè)會(huì)調(diào)用 makeUl
的函數(shù)。
但問(wèn)題還是再深入一點(diǎn),因?yàn)樯厦娴拇a改變了原數(shù)據(jù)。而一般情況下,我們應(yīng)該盡量避免這樣的副作用
// 聲明一個(gè)新結(jié)構(gòu),它把 INode 和 DOM 組合在一起。
// 這個(gè)結(jié)構(gòu)將代替 INode 作為隊(duì)列的元素類型
interface IDomNode {
node: INode;
dom: JQuery;
}
function makeTreeWidely(roots: INode[]): JQuery {
// convert 將節(jié)點(diǎn)數(shù)組轉(zhuǎn)換為 IDomNode 數(shù)組,
// 同時(shí)還干了原來(lái) makeUl 干的事情,返回一個(gè) $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>"));
// 將兩個(gè)數(shù)組組成一個(gè)元組(對(duì)象)返回
return {
domNodes,
$ul
};
}
// 解析元組,聲明變量 queue 和 $rootUl,
// 并分別將 domNodes 和 $ul 的值賦值給 queue 和 $rootUl 兩個(gè)變量
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;
}
看療效:http://jsfiddle.net/y7bw4yj2/1/
新聞標(biāo)題:使用遞歸遍歷并轉(zhuǎn)換樹(shù)形數(shù)據(jù)(以TypeScript為例)
URL鏈接:http://m.newbst.com/article6/pgcsig.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供移動(dòng)網(wǎng)站建設(shè)、網(wǎng)頁(yè)設(shè)計(jì)公司、定制網(wǎng)站、Google、微信小程序、網(wǎng)站導(dǎo)航
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)