一個vuepress配置問題,引發的js遞迴演算法思考

程式設計師王天發表於2023-10-15

前言

這兩天在嘗試用語雀+ vuepress + github 搭建個人部落格。

小破站地址 :王天的 web 進階之路

語雀作為編輯器,釋出文件推送 github,再自動打包部署,大概流程如下。

問題

我使用的elog外掛批次匯出語雀文件。elog採用的配置是所有文章平鋪匯出,沒有按照語雀知識庫目錄生成markdown,這導致 vuepress 側邊欄無法和語雀一致,如下圖。

image.png
上圖,左側是語雀知識庫,右側是匯出到 vuepress 展示的效果,很明顯沒有目錄這很影響閱讀體驗呀

解決

在查閱 vuepress 文件後,發現配置silderbar.ts可以自定義側邊欄目錄,配置引數如下:

export default {
  theme: defaultTheme({
    // 可摺疊的側邊欄
    sidebar: {
      "/web/": [
        {
          text: "王天的web進階手冊",
          collapsible: true, // 目錄是否摺疊
          children: ["/reference/cli.md", "/reference/config.md"], // 文件目錄
        },
        {
          text: "王天的魔法工具箱",
          collapsible: true,
          children: [
            "/reference/bundler/vite.md",
            "/reference/bundler/webpack.md",
          ],
        },
      ],
    },
  }),
};

遞迴生成選單

配置sidebar.ts 可以修改左側選單,但是一個個手動修改這忒麻煩了啊啊啊啊。那如何批次生產選單配置項呢?

遞迴函式呀呀呀呀呀呀 ????

elog 在同步語雀文件時,會自動建立elog.cache.json快取檔案,在 vueprss 專案根目錄中檢視。

開啟elog.cache.json檔案,我們能看到語雀文件知識庫的資料結構

"catalog": [
    {
      "type": "DOC",
      "title": "前言",
      "uuid": "17Os-_V_hcS37KOD",
      "url": "wqbpyf5083qc7ho8",
      "prev_uuid": "",
      "sibling_uuid": "dmQSRn6AXUBSg96x",
      "child_uuid": "",
      "parent_uuid": "",
      "doc_id": 141216125,
      "level": 0,
      "id": 141216125,
      "open_window": 1,
      "visible": 1
    }
  ]

catlog 屬性是文件快取資料,關鍵欄位:

  • type:值為'DOC' 是文章、值為 TITLE 則為目錄
  • uuid:文章 id
  • prent_uuid:父節點的 uuid

我們們根據以上引數,編寫遞迴函式, 將elog.cache.json的一維陣列,遞迴生成 vuepress 側邊欄配置資料
程式碼如下:

function genYuqueRoute() {
  // 引數1:遍歷陣列
  // 引數2:父選單id
  const deep = (arrlist, parantId) => {
    let forList: any[] = [];
    arrlist.forEach((element) => {
      // 選單id不一致,跳出迴圈呼叫
      if (element.parent_uuid !== parantId) return;
      // 如果是TITLE型別新增配置項
      if (element.type === "TITLE") {
        forList.push({
          text: element.title,
          collapsible: true,
          children: deep(arrlist, element.uuid),
        });
        // 如果是DOC 型別追加檔案地址
      } else {
        forList.push(element.url + ".md");
      }
    });
    return forList;
  };
  return deep(catalog, "");
}

效果

image.png

敲重點啦!

遞迴函式本質上是一個在回撥自身的函式,用於改造資料結構,重點在於跳出迴圈的機制,否則陷入死迴圈啦

DFS vs BFS ?

什麼是 DFS 、BFS ?

  • DFS 深度優先搜尋:可以用於找到一條路徑、判斷圖中是否存在迴圈、拓撲排序、生成連通分量等。
  • BFS 廣度優先搜尋:可以用於找到最短路徑、生成最小生成樹、進行網路分析等。

:::danger
??‍♀️ 簡單理解為,橫向 、豎向 遍歷據狀結構

  • 深度優先搜尋,對資料結構的橫向執行,從第一行遍歷子節點、葉子節點,依次直到最後一行。
  • 廣度優先搜尋,對資料結構的豎向執行,把樹結構平面鋪開、以層級數為列數,從第一列依次執行。
    :::

將深度搜尋、廣度搜尋代入到生活場景更容易理解。

我們們先看一個家庭關係樹狀圖,爺爺奶奶是一級屬性、父母叔伯二級、孫子孫女三級屬性、重孫們是四級屬性,以此類推。形成一個家庭關係樹狀圖。

假如奶奶過八十大壽,按輩分來,首先是父母叔伯這一輩祝壽,其次是孫子孫女輩分,最後重孫們,以此類推,這個豎向執行的祝壽過程就是廣度優先搜尋

那過年走親戚的話,我們們沒有俺輩分,去分批的吧?至少我們老家不是的,都是一去一家子呢。那這個橫線執行的過程,就是深度優先搜尋。

深度優先搜尋(DFS)示例程式碼:

從 A 節點依次取出資料

// 圖的鄰接表表示
const graph = {
  A: ["B", "C"],
  B: ["D", "E"],
  C: ["F", "G"],
  D: [],
  E: [],
  F: [],
  G: [],
};

// 使用深度優先搜尋遍歷圖
function dfs(graph, start) {
  const visited = new Set(); // 儲存已訪問節點的集合

  function traverse(node) {
    visited.add(node); // 將當前節點標記為已訪問
    console.log(node); // 列印遍歷的節點

    const neighbors = graph[node]; // 獲取當前節點的鄰居節點
    for (const neighbor of neighbors) {
      // 遍歷當前節點的鄰居節點
      if (!visited.has(neighbor)) {
        // 如果鄰居節點未被訪問過
        traverse(neighbor); // 遞迴遍歷鄰居節點
      }
    }
  }

  traverse(start); // 從起始節點開始進行深度優先搜尋
  return visited; // 返回所有已訪問的節點
}

輸出結果:

dfs(graph, "A"); // 對圖進行深度優先搜尋,從起始節點 'A' 開始,並列印遍歷結果
// A
// B
// D
// E
// C
// F
// G

在上述程式碼中,圖使用鄰接表表示,dfs 函式使用遞迴方式實現了深度優先搜尋。從起始節點 'A' 開始,遞迴訪問其鄰居節點,並在訪問時輸出節點的值。

廣度優先搜尋(BFS)示例程式碼:

// 廣度搜尋 BFS
let graph = {
  A: ["B", "C"],
  B: ["A", "C", "D"],
  C: ["A", "D", "E"],
  D: ["B", "C", "E"],
  E: ["C", "D", "F"],
  F: ["E", "W"],
  W: ["C"],
};

function bfs(graph, startPoint) {
  let queue = []; // 用於儲存待訪問節點的佇列
  let result = []; // 儲存遍歷結果的陣列

  queue.push(startPoint); // 將起始節點新增到佇列
  result.push(startPoint); // 將起始節點新增到遍歷結果

  while (queue.length > 0) {
    // 當佇列不為空時進行迴圈
    let point = queue.shift(); // 取出佇列中的第一個節點作為當前節點
    let nodes = graph[point]; // 獲取當前節點的所有鄰居節點
    for (let node of nodes) {
      // 遍歷當前節點的鄰居節點
      if (result.includes(node)) continue; // 如果鄰居節點已經在遍歷結果中,則跳過
      result.push(node); // 將鄰居節點新增到遍歷結果中
      queue.push(node); // 將鄰居節點新增到佇列中,以便後續訪問其鄰居節點
    }
  }

  return result; // 返回遍歷結果
}

console.log(bfs(graph, "B")); // 執行廣度優先搜尋,從起始節點 'B' 開始,並輸出遍歷結果

在上述程式碼中,圖使用鄰接表表示,bfs 函式使用佇列實現了廣度優先搜尋。從起始節點 'A' 開始,將其加入佇列並標記為已訪問,然後依次從佇列中取出節點,並訪問其鄰居節點,同時將鄰居節點加入佇列中,直到佇列為空。

案例

深度優先搜尋(DFS)和廣度優先搜尋(BFS)在前端專案中有許多實際的應用場景。下面有兩個常見的前端開發專案案例

1、元件樹遍歷

在前端開發中,經常會有需要對元件樹進行遍歷的場景,例如渲染元件、查詢元件等。下面是一個使用 DFS 進行元件樹遍歷的示例:

function dfs_component_traversal(component) {
  console.log(component); // 處理當前元件

  if (component.children) {
    for (const child of component.children) {
      dfs_component_traversal(child); // 遞迴遍歷子元件
    }
  }
}

以上的程式碼展示了一個使用深度優先搜尋進行元件樹遍歷的函式。我們可以根據元件的層級關係,從根元件開始遞迴地遍歷每個元件及其子元件,以實現對整個元件樹的遍歷和操作。

這個演算法可以幫助我們在前端專案中處理元件之間的關係,例如渲染元件、查詢相關元件等。透過對元件樹的深度遍歷,我們可以有序地處理元件及其子元件,並執行相應的操作。

2、頁面導航

在前端開發中,頁面導航是一個常見的需求。我們可以使用廣度優先搜尋來實現頁面導航功能,以確保按照層級關係有序地展示頁面。

function bfs_page_navigation(page) {
  const queue = [page]; // 使用佇列作為輔助資料結構來進行廣度優先搜尋

  while (queue.length > 0) {
    const current = queue.shift(); // 移除佇列頭部元素作為當前頁面
    console.log(current); // 處理當前頁面

    for (const child of current.children) {
      queue.push(child); // 將子頁面加入佇列
    }
  }
}

以上程式碼展示了一個使用廣度優先搜尋進行頁面導航的函式。在這個函式中,我們使用佇列作為輔助資料結構來進行廣度優先搜尋。透過不斷將子頁面加入佇列,並按照佇列中的順序處理每個頁面,可以實現按照層級關係有序地導航頁面。

3、DFS + BFS 綜合案例

const root = {
  value: 1,
  children: [
    {
      value: 2,
      children: [],
    },
    {
      value: 3,
      children: [
        {
          value: 7,
          children: [
            {
              value: 8,
              children: [],
            },
          ],
        },
      ],
    },
    {
      value: 4,
      children: [
        {
          value: 6,
          children: [],
        },
      ],
    },
  ],
};

// 在深度優先搜尋 - 堆
// 我們首先處理當前節點,然後遞迴地處理每個子節點、直到葉子節點(沒有子節點的節點),最後依次遍歷完成
const digui = (node) => {
  console.log(node.value);
  if (node.children) {
    for (const children of node.children) {
      digui(children);
    }
  }
};
// 廣度優先搜尋-棧,把多維樹結構,取出來平鋪,依次訪問。
// 在廣度優先搜尋中,我們使用佇列來儲存待訪問的節點,確保按照層級順序進行遍歷。
// 每次從佇列中取出隊頭節點,處理該節點後,將其鄰居節點(子節點)入隊,以便後續遍歷。這樣,就可以依次訪問所有節點,並保持層級順序。

function breadthFirstSearch(root) {
  if (!root) {
    return;
  }

  const queue = []; // 建立一個空佇列,用於存放待訪問的節點
  queue.push(root); // 將根節點入隊

  while (queue.length !== 0) {
    // 當佇列不為空時迴圈執行以下步驟
    const current = queue.shift(); // 出隊隊頭節點作為當前節點
    console.log(current.value); // 進行二次加工或其他操作,這裡簡單地輸出節點的值

    for (const child of current.children) {
      // 遍歷當前節點的鄰居節點(子節點)
      queue.push(child); // 將未訪問過的鄰居節點入隊
    }
  }
}
console.log(digui(root));

console.log(breadthFirstSearch(root));

總結

遞迴函式本質上是一個在回撥自身的函式,用於改造資料結構,重點在於跳出迴圈的機制,否則陷入死迴圈啦

深度優先搜尋(DFS)的原理很簡單:我們從起始節點開始,沿著一條路徑不斷向下探索,直到達到終點或者無法繼續為止。如果遇到終點,就找到了一條路徑;如果無法繼續,則回溯到上一個節點,然後嘗試探索其他路徑。這個過程會遞迴地進行,或者使用棧來儲存節點的順序。

相比之下,廣度優先搜尋(BFS)的原理稍微有些不同:我們從起始節點開始,逐層地訪問其鄰居節點。也就是說,我們首先訪問起始節點的鄰居節點,然後是鄰居節點的鄰居節點,依此類推,直到遍歷完所有節點或者找到目標節點為止。為了遍歷節點的順序,我們使用佇列資料結構。

讀者朋友好呀,我是王天~

嘗試做過很多事情,汽修專業肄業生,半路出道的野生程式設計師、前端講師、新手作者,最終還是喜歡寫程式碼、樂於用文字記錄熱衷分享~

如文章有錯誤或者不嚴謹的地方,期待給於指正,萬分感謝。

如果喜歡或者 有所啟發,歡迎 star,對作者也是一種鼓勵。

微信:「wangtian3111」,加我進王天唯一的讀者群。

個人部落格:https://itwangtian.com

相關文章