自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

使用遞歸遍歷并轉(zhuǎn)換樹形數(shù)據(jù)(以TypeScript為例)

開發(fā) 前端
樹形數(shù)據(jù)的遍歷有兩種方法,大家都知道:廣度遍歷和深度遍歷。一般情況下,廣度遍歷是采用隊(duì)列來實(shí)現(xiàn),而深度遍歷剛更適合使用遞歸來實(shí)現(xiàn)。

一個(gè)朋友問我應(yīng)該怎么從一個(gè)樹的 JSON 數(shù)組生成 HTML,使用 <ul> 和 <li> 來構(gòu)建頁面元素。于是我簡單的畫了個(gè)樹型結(jié)構(gòu)圖

 

然后寫了對(duì)應(yīng)的模擬數(shù)據(jù)(JavaScript 對(duì)象)

  1. const data = { 
  2.     name"A"
  3.     nodes: [ 
  4.         { name"B", nodes: [{ name"F" }] }, 
  5.         { name"C" }, 
  6.         { 
  7.             name"D"
  8.             nodes: [ 
  9.                 { name"G" }, 
  10.                 { name"H" }, 
  11.                 { name"I", nodes: [{ name"J" }, { name"K" }] } 
  12.             ] 
  13.         }, 
  14.         { name"E" } 
  15.     ] 
  16. };  

***寫了一個(gè)遞歸,生成了 HTML 的樹型結(jié)構(gòu)。原本是用 JavaScript ES6 寫的,為了表明數(shù)據(jù)結(jié)構(gòu),這里改用 TypeScript 來寫:

  1. interface INode { 
  2.     name: string; 
  3.     nodes?: INode[]; 
  4.  
  5. function makeTree(roots: INode[]): JQuery<HTMLElement> { 
  6.     function makeNode(node: INode): JQuery<HTMLElement> { 
  7.         const $div = $("<div>").text(node.name || ""); 
  8.         const $li = $("<li>").append($div); 
  9.         if (node.nodes && node.nodes.length) { 
  10.             $li.append(makeNodeList(node.nodes)); 
  11.         } 
  12.         return $li; 
  13.     } 
  14.  
  15.     function makeNodeList(nodes: INode[]): JQuery<HTMLElement> { 
  16.         return nodes 
  17.             .map(child => makeNode(child)) 
  18.             .reduce(($ul, $li) => { 
  19.                 return $ul.append($li); 
  20.             }, $("<ul>")); 
  21.     } 
  22.  
  23.     return makeNodeList(roots); 
  24.  

效果還是蠻不錯(cuò)的

 

看看源碼(轉(zhuǎn)譯成 JS 之后的):http://jsfiddle.net/y7bw4yj2/

然后朋友說沒看明白,好吧,那我從頭講起

遍歷方法

樹形數(shù)據(jù)的遍歷有兩種方法,大家都知道:廣度遍歷和深度遍歷。一般情況下,廣度遍歷是采用隊(duì)列來實(shí)現(xiàn),而深度遍歷剛更適合使用遞歸來實(shí)現(xiàn)。

 

廣度遍歷

從圖上大致可以理解廣度遍歷的過程:

  1. 準(zhǔn)備一個(gè)空隊(duì)列;
  2. 將根(單根或多根均可)節(jié)點(diǎn)放到隊(duì)列中;
  3. 從隊(duì)列中取出一個(gè)節(jié)點(diǎn)
  4. 處理(比如打印)這個(gè)節(jié)點(diǎn)
  5. 檢查節(jié)點(diǎn)的子節(jié)點(diǎn),如果有,全部依次添加到隊(duì)列中
  6. 回到第 3 步開始處理,直到隊(duì)列為空(處理完成)
  1. function travelWidely(roots: INode[]) { 
  2.     const queue: INode[] = [...roots]; 
  3.     while (queue.length) { 
  4.         const node = queue.shift()!; 
  5.         // 打印節(jié)點(diǎn)名稱及其子節(jié)點(diǎn)數(shù) 
  6.         console.log(`${node.name} ${node.nodes && node.nodes.length || ""}`); 
  7.         if (node.nodes && node.nodes.length) { 
  8.             queue.push(...node.nodes); 
  9.         } 
  10.     } 
  11.  
  12. // 開始遍歷 
  13. travelWidely([data]);  

const node = queue.shift()!,這后面的 ! 后綴表示聲明其結(jié)果不為 undefined 或 null。這是一個(gè) TypeScript 語法。由于 .shift() 在數(shù)組中沒有元素時(shí)會(huì)返回 undefined,所以其返回類型被聲明為 INode | undefined,由于從邏輯可以保證 .shift() 一定會(huì)返回一個(gè)節(jié)點(diǎn)對(duì)象,所以這里用 ! 后綴忽略類型中的 undefined 部分,使 node 的類型被推導(dǎo)為 INode。

代碼里稍難理解一點(diǎn)的是要注意 queue 的內(nèi)容和長度隨時(shí)在變化。如果想使用 for 代替 while 循環(huán),節(jié)點(diǎn)序號(hào)會(huì)因 .shift() 而不斷變化,所以 i < queue.length 這樣的判斷是錯(cuò)誤的。

深度遍歷

深度遍歷是一個(gè)遞歸過程,遞歸一直是編程的難點(diǎn)。

遞歸是一個(gè)循環(huán)往復(fù)的處理過程,它有兩個(gè)點(diǎn)需要注意:

  • 遞歸調(diào)用點(diǎn),遞歸調(diào)用自己(或另一個(gè)可能會(huì)調(diào)用自己的函數(shù))
  • 遞歸結(jié)束點(diǎn),退出當(dāng)前函數(shù)

以樹節(jié)點(diǎn)為例,我們期望處理過程是處理(打印)一個(gè)樹結(jié)點(diǎn),即 printNode(node: INode)。那么它的

  • 遞歸調(diào)用點(diǎn):如果該節(jié)點(diǎn)有子節(jié)點(diǎn),依次對(duì)子節(jié)點(diǎn)調(diào)用 printNode(children[i])
  • 遞歸結(jié)束點(diǎn):處理完所有子節(jié)點(diǎn)(子節(jié)點(diǎn)數(shù)量是有限的,所以一定會(huì)結(jié)束)

用一段偽代碼描述這一過程

  1. function printNode(node: INode) { 
  2.     // 處理該節(jié)點(diǎn) 
  3.     console.log(node.name); 
  4.      
  5.     // 遞歸調(diào)用點(diǎn):循環(huán)對(duì)子節(jié)點(diǎn)調(diào)用 printNode 
  6.     node.nodes!.forEach(child => printNode(child)); 
  7.      
  8.     // 遞歸結(jié)束點(diǎn):循環(huán)完成,return 
  9.  

上面兩句代碼就完成了遞歸過程,但實(shí)際上情況還要復(fù)雜些,因?yàn)橐幚砣肟诤腿蒎e(cuò)。

  1. // 注意參數(shù)支持傳入單根或多根, 
  2. // 如果像 travelWidely 那樣只支持多根(單根是特例)也是可以的 
  3. function travelDeeply(roots: INode | INode[]) { 
  4.     function printNode(node: INode) { 
  5.         console.log(`${node.name} ${node.nodes && node.nodes.length || ""}`); 
  6.         if (node.nodes && node.nodes.length) { 
  7.             // 依次對(duì)子節(jié)點(diǎn)遞歸調(diào)用 printNode 
  8.             node.nodes.forEach(child => printNode(child)); 
  9.         } 
  10.     } 
  11.  
  12.     // 這里 printNode 和 node => printNode(node) 等價(jià) 
  13.     (Array.isArray(roots) ? roots : [roots]).forEach(printNode); 
  14.  
  15. // 開始遍歷 
  16. travelDeeply(data);  

關(guān)于遞歸,我正好在慕課網(wǎng)上講生成數(shù)據(jù)解決方案的時(shí)候講到了,有興趣可以看看。

遍歷還沒講完

上面兩種遍歷都講到了,但是還沒講完——因?yàn)閮煞N遍歷都是以打印為例,而我們的目的是要生成 DOM 樹。生成 DOM 樹與純打印信息的不同之處在于,我們不僅要使用節(jié)點(diǎn)信息,還要從節(jié)點(diǎn)信息生成 DOM 返回出來。

深度遍歷生成節(jié)點(diǎn)

這次先講深度遍歷,因?yàn)檫f歸更容易實(shí)現(xiàn)。遞歸本身具有層次信息,每進(jìn)入一個(gè)遞歸調(diào)用點(diǎn),就會(huì)深入一層,每離開一個(gè)遞歸結(jié)束點(diǎn),就會(huì)減少一層。所以這個(gè)算法本身能夠保留結(jié)構(gòu)信息,相應(yīng)代碼也會(huì)更容易實(shí)現(xiàn)。而且在本文一開始,就已經(jīng)實(shí)現(xiàn)出來了。

需要注意的一點(diǎn)是那段代碼用了兩個(gè)函數(shù)來完成遞歸過程:

  • makeNode 處理單個(gè)節(jié)點(diǎn),它調(diào)用 makeNodeList 處理子節(jié)點(diǎn)列表
  • makeNodeList 遍歷節(jié)點(diǎn)列表,分別對(duì)其調(diào)用 makeNode 來進(jìn)行處理

makeNode 和 makeNodeList 的相互調(diào)用形成了遞歸,上述兩條都是遞歸調(diào)用點(diǎn),而遞歸結(jié)束點(diǎn)同樣也有兩條:

  • makeNode 處理的節(jié)點(diǎn)沒有子節(jié)點(diǎn)時(shí),不會(huì)調(diào)用 makeNodeList
  • makeNodeList 中的循環(huán)結(jié)束時(shí),不會(huì)再調(diào)用 makeNode

廣度遍歷生成節(jié)點(diǎn)

廣度遍歷的過程是把所有節(jié)點(diǎn)扁平化到一個(gè)隊(duì)列中了,這個(gè)過程是不可逆 的,換句話說,我們在處理過程中丟掉了樹形結(jié)構(gòu)信息。然后我們要生成的 DOM 樹,是需要結(jié)構(gòu)信息的——因此,需要將結(jié)構(gòu)信息附加在每個(gè)節(jié)點(diǎn)上。這里我們把生成的 DOM 和數(shù)據(jù)節(jié)點(diǎn)綁定起來,由 DOM 保存結(jié)構(gòu)信息。為此,需要修改一下節(jié)點(diǎn)類型

  1. interface INode { 
  2.     name: string; 
  3.     nodes?: INode[]; 
  4.     dom: JQuery;    // 附加生成的 DOM 
  5.  
  1. function makeTreeWidely(roots: INode[]): JQuery { 
  2.     // 從一組節(jié)點(diǎn)生成 <ul>,為每個(gè)節(jié)點(diǎn)生成并附加 <li>, 
  3.     // 同時(shí)將 <li> 到到 <ul> 中保存結(jié)構(gòu)信息 
  4.     function makeUl(nodes: INode[]) { 
  5.         return nodes 
  6.             .map(node => { 
  7.                 const $li = $("<li>"
  8.                     .append($("<div>").text(node.name || "")); 
  9.                 node.dom = $li; 
  10.                 return $li; 
  11.             }) 
  12.             .reduce(($ul, $li) => $ul.append($li), $("<ul>")); 
  13.     } 
  14.  
  15.     const $rootUl = makeUl(roots); 
  16.  
  17.     const queue: INode[] = [...roots]; 
  18.     while (queue.length) { 
  19.         const node = queue.shift()!; 
  20.  
  21.         if (node.nodes && node.nodes.length) { 
  22.             const $ul = makeUl(node.nodes); 
  23.             node.dom.append($ul); 
  24.             queue.push(...node.nodes); 
  25.         } 
  26.     } 
  27.     return $rootUl; 
  28.  

雖然這里和上面講遞歸遍歷 printNode 的時(shí)候一樣定義了局部函數(shù)表達(dá)式 makeUl,但這里沒有遞歸,因?yàn)?makeUl 內(nèi)部沒有調(diào)用自身,或者某個(gè)會(huì)調(diào)用 makeUl 的函數(shù)。

但問題還是再深入一點(diǎn),因?yàn)樯厦娴拇a改變了原數(shù)據(jù)。而一般情況下,我們應(yīng)該盡量避免這樣的副作用

沒有副作用的廣度遍歷生成節(jié)點(diǎn)

  1. // 聲明一個(gè)新結(jié)構(gòu),它把 INode 和 DOM 組合在一起。 
  2. // 這個(gè)結(jié)構(gòu)將代替 INode 作為隊(duì)列的元素類型 
  3. interface IDomNode { 
  4.     node: INode; 
  5.     dom: JQuery; 
  6.  
  7. function makeTreeWidely(roots: INode[]): JQuery { 
  8.     // convert 將節(jié)點(diǎn)數(shù)組轉(zhuǎn)換為 IDomNode 數(shù)組, 
  9.     // 同時(shí)還干了原來 makeUl 干的事情,返回一個(gè) $ul 
  10.     function convert(nodes: INode[]) { 
  11.         const domNodes = nodes 
  12.             .map(node => { 
  13.                 const $li = $("<li>"
  14.                     .append($("<div>").text(node.name || "")); 
  15.                 return { 
  16.                     node, 
  17.                     dom: $li 
  18.                 }; 
  19.             }); 
  20.  
  21.         const $ul = domNodes 
  22.             .reduce(($ul, dn) => $ul.append(dn.dom), $("<ul>")); 
  23.  
  24.         // 將兩個(gè)數(shù)組組成一個(gè)元組(對(duì)象)返回 
  25.         return { 
  26.             domNodes, 
  27.             $ul 
  28.         }; 
  29.     } 
  30.  
  31.     // 解析元組,聲明變量 queue 和 $rootUl, 
  32.     // 并分別將 domNodes 和 $ul 的值賦值給 queue 和 $rootUl 兩個(gè)變量 
  33.     const { domNodes: queue, $ul: $rootUl } = convert(roots); 
  34.  
  35.     while (queue.length) { 
  36.         const { node, dom } = queue.shift()!; 
  37.  
  38.         if (node.nodes && node.nodes.length) { 
  39.             const { domNodes, $ul } = convert(node.nodes); 
  40.             dom.append($ul); 
  41.             queue.push(...domNodes); 
  42.         } 
  43.     } 
  44.     return $rootUl; 
  45.  

看療效:http://jsfiddle.net/y7bw4yj2/1/

責(zé)任編輯:龐桂玉 來源: segmentfault
相關(guān)推薦

2022-01-10 12:23:00

TypeScript ESLint前端

2011-07-08 09:55:02

數(shù)據(jù)中心防震

2011-08-04 09:57:03

dbmonsterMySQL

2021-08-02 09:50:47

Vetur源碼SMART

2016-12-20 12:34:46

存儲(chǔ)MySQL流程

2021-05-31 08:00:00

消息隊(duì)列架構(gòu)Rabbit MQ

2018-08-22 16:40:51

前端JavascriptVue

2020-03-03 09:17:27

圖形數(shù)據(jù)庫數(shù)據(jù)庫

2023-06-26 08:43:57

OracleTRACE葉節(jié)點(diǎn)

2022-10-14 08:03:46

圖形數(shù)據(jù)庫系統(tǒng)

2018-01-02 09:00:51

大數(shù)據(jù)營銷王者榮耀

2025-04-07 08:40:00

Python數(shù)據(jù)結(jié)構(gòu)

2022-02-14 14:28:57

驅(qū)動(dòng)開發(fā)鴻蒙系統(tǒng)

2021-04-16 08:20:00

Flink CEP直播監(jiān)控

2009-03-02 16:57:34

LinuxUbuntu配置完全方案

2021-01-14 09:00:00

開發(fā)FedoraUbuntu

2020-03-06 10:05:15

2015-06-25 13:01:28

大數(shù)據(jù)星巴克海底撈

2019-05-20 08:20:40

數(shù)據(jù)集數(shù)據(jù)可視化數(shù)據(jù)

2015-07-01 15:39:52

Ceph云存儲(chǔ)NAS
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)