日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
使用遞歸遍歷并轉(zhuǎn)換樹形數(shù)據(jù)(以TypeScript為例)

一個朋友問我應(yīng)該怎么從一個樹的 JSON 數(shù)組生成 HTML,使用

  • 來構(gòu)建頁面元素。于是我簡單的畫了個樹型結(jié)構(gòu)圖

    創(chuàng)新互聯(lián)長期為上1000家客戶提供的網(wǎng)站建設(shè)服務(wù),團(tuán)隊從業(yè)經(jīng)驗10年,關(guān)注不同地域、不同群體,并針對不同對象提供差異化的產(chǎn)品和服務(wù);打造開放共贏平臺,與合作伙伴共同營造健康的互聯(lián)網(wǎng)生態(tài)環(huán)境。為香洲企業(yè)提供專業(yè)的網(wǎng)站建設(shè)、成都網(wǎng)站設(shè)計,香洲網(wǎng)站改版等技術(shù)服務(wù)。擁有十多年豐富建站經(jīng)驗和眾多成功案例,為您定制開發(fā)。

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

     
     
     
     
    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. }; 

    ***寫了一個遞歸,生成了 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 {
    6.     function makeNode(node: INode): JQuery {
    7.         const $div = $("
      ").text(node.name || "");
    8.         const $li = $("
    9. ").append($div);
    10.         if (node.nodes && node.nodes.length) {
    11.             $li.append(makeNodeList(node.nodes));
    12.         }
    13.         return $li;
    14.     }
    15.     function makeNodeList(nodes: INode[]): JQuery {
    16.         return nodes
    17.             .map(child => makeNode(child))
    18.             .reduce(($ul, $li) => {
    19.                 return $ul.append($li);
    20.             }, $("
        "));
      •     }
      •     return makeNodeList(roots);

    效果還是蠻不錯的

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

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

    遍歷方法

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

    廣度遍歷

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

    1. 準(zhǔn)備一個空隊列;
    2. 將根(單根或多根均可)節(jié)點放到隊列中;
    3. 從隊列中取出一個節(jié)點
    4. 處理(比如打印)這個節(jié)點
    5. 檢查節(jié)點的子節(jié)點,如果有,全部依次添加到隊列中
    6. 回到第 3 步開始處理,直到隊列為空(處理完成)
     
     
     
     
    1. function travelWidely(roots: INode[]) {
    2.     const queue: INode[] = [...roots];
    3.     while (queue.length) {
    4.         const node = queue.shift()!;
    5.         // 打印節(jié)點名稱及其子節(jié)點數(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。這是一個 TypeScript 語法。由于 .shift() 在數(shù)組中沒有元素時會返回 undefined,所以其返回類型被聲明為 INode | undefined,由于從邏輯可以保證 .shift() 一定會返回一個節(jié)點對象,所以這里用 ! 后綴忽略類型中的 undefined 部分,使 node 的類型被推導(dǎo)為 INode。

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

    深度遍歷

    深度遍歷是一個遞歸過程,遞歸一直是編程的難點。

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

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

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

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

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

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

    上面兩句代碼就完成了遞歸過程,但實際上情況還要復(fù)雜些,因為要處理入口和容錯。

     
     
     
     
    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.             // 依次對子節(jié)點遞歸調(diào)用 printNode
    8.             node.nodes.forEach(child => printNode(child));
    9.         }
    10.     }
    11.     // 這里 printNode 和 node => printNode(node) 等價
    12.     (Array.isArray(roots) ? roots : [roots]).forEach(printNode);
    13. }
    14. // 開始遍歷
    15. travelDeeply(data); 

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

    遍歷還沒講完

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

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

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

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

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

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

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

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

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

     
     
     
     
    1. interface INode {
    2.     name: string;
    3.     nodes?: INode[];
    4.     dom: JQuery;    // 附加生成的 DOM
     
     
     
     
    1. function makeTreeWidely(roots: INode[]): JQuery {
    2.     // 從一組節(jié)點生成 
        ,為每個節(jié)點生成并附加 
      • ,
      •     // 同時將 
      •  到到 
           中保存結(jié)構(gòu)信息
        •     function makeUl(nodes: INode[]) {
        •         return nodes
        •             .map(node => {
        •                 const $li = $("
        • ")
        •                     .append($("
          ").text(node.name || ""));
        •                 node.dom = $li;
        •                 return $li;
        •             })
        •             .reduce(($ul, $li) => $ul.append($li), $("
            "));
          •     }
          •     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ù)表達(dá)式 makeUl,但這里沒有遞歸,因為 makeUl 內(nèi)部沒有調(diào)用自身,或者某個會調(diào)用 makeUl 的函數(shù)。

    但問題還是再深入一點,因為上面的代碼改變了原數(shù)據(jù)。而一般情況下,我們應(yīng)該盡量避免這樣的副作用

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

     
     
     
     
    1. // 聲明一個新結(jié)構(gòu),它把 INode 和 DOM 組合在一起。
    2. // 這個結(jié)構(gòu)將代替 INode 作為隊列的元素類型
    3. interface IDomNode {
    4.     node: INode;
    5.     dom: JQuery;
    6. }
    7. function makeTreeWidely(roots: INode[]): JQuery {
    8.     // convert 將節(jié)點數(shù)組轉(zhuǎn)換為 IDomNode 數(shù)組,
    9.     // 同時還干了原來 makeUl 干的事情,返回一個 $ul
    10.     function convert(nodes: INode[]) {
    11.         const domNodes = nodes
    12.             .map(node => {
    13.                 const $li = $("
    14. ")
    15.                     .append($("
      ").text(node.name || ""));
    16.                 return {
    17.                     node,
    18.                     dom: $li
    19.                 };
    20.             });
    21.         const $ul = domNodes
    22.             .reduce(($ul, dn) => $ul.append(dn.dom), $("
        "));
      •         // 將兩個數(shù)組組成一個元組(對象)返回
      •         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;

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


    本文標(biāo)題:使用遞歸遍歷并轉(zhuǎn)換樹形數(shù)據(jù)(以TypeScript為例)
    瀏覽路徑:http://www.dlmjj.cn/article/cojoghs.html