91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

怎么用Vue3+TypeScript實現遞歸菜單組件

發布時間:2021-08-24 15:08:34 來源:億速云 閱讀:255 作者:chen 欄目:開發技術

本篇內容主要講解“怎么用Vue3+TypeScript實現遞歸菜單組件”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“怎么用Vue3+TypeScript實現遞歸菜單組件”吧!

目錄
  • 前言

  • 需求

  • 實現

    • 首次渲染

    • 點擊菜單項

    • 樣式區分

    • 默認高亮

    • 數據源變動引發的 bug

  • 完整代碼

    • App.vue

  • 總結

    前言

    小伙伴們好久不見,最近剛入職新公司,需求排的很滿,平常是實在沒時間寫文章了,更新頻率會變得比較慢。

    周末在家閑著無聊,突然小弟過來緊急求助,說是面試騰訊的時候,對方給了個 Vue 的遞歸菜單要求實現,回來找我復盤。

    正好這周是小周,沒想著出去玩,就在家寫寫代碼吧,我看了一下需求,確實是比較復雜,需要利用好遞歸組件,正好趁著這

    個機會總結一篇 Vue3 + TS 實現遞歸組件的文章。

    需求

    可以先在Github Pages 中預覽一下效果。

    需求是這樣的,后端會返回一串可能有無限層級的菜單,格式如下:

    [
      {
        id: 1,
        father_id: 0,
        status: 1,
        name: '生命科學競賽',
        _child: [
          {
            id: 2,
            father_id: 1,
            status: 1,
            name: '野外實習類',
            _child: [{ id: 3, father_id: 2, status: 1, name: '植物學' }],
          },
          {
            id: 7,
            father_id: 1,
            status: 1,
            name: '科學研究類',
            _child: [
              { id: 8, father_id: 7, status: 1, name: '植物學與植物生理學' },
              { id: 9, father_id: 7, status: 1, name: '動物學與動物生理學' },
              { id: 10, father_id: 7, status: 1, name: '微生物學' },
              { id: 11, father_id: 7, status: 1, name: '生態學' },
            ],
          },
          { id: 71, father_id: 1, status: 1, name: '添加' },
        ],
      },
      {
        id: 56,
        father_id: 0,
        status: 1,
        name: '考研相關',
        _child: [
          { id: 57, father_id: 56, status: 1, name: '政治' },
          { id: 58, father_id: 56, status: 1, name: '外國語' },
        ],
      },
    ]

    1、每一層的菜單元素如果有 _child 屬性,這一項菜單被選中以后就要繼續展示這一項的所有子菜單,預覽一下動圖:

    怎么用Vue3+TypeScript實現遞歸菜單組件

    2、并且點擊其中的任意一個層級,都需要把菜單的 完整的 id 鏈路 傳遞到最外層,給父組件請求數據用。比如點擊了 科學研究類。那么向外 emit 的時候還需要帶上它的第一個子菜單 植物學與植物生理學 的 id,以及它的父級菜單 生命科學競賽 的 id,也就是 [1, 7, 8]。

    3、每一層的樣式還可以自己定制。

    實現

    這很顯然是一個遞歸組件的需求,在設計遞歸組件的時候,我們要先想清楚數據到視圖的映射。

    在后端返回的數據中,數組的每一層可以分別對應一個菜單項,那么數組的層則就對應視圖中的一行,當前這層的菜單中,被點擊選中 的那一項菜單的 child 就會被作為子菜單數據,交給遞歸的 NestMenu 組件,直到某一層的高亮菜單不再有 child,則遞歸終止。

    怎么用Vue3+TypeScript實現遞歸菜單組件

    由于需求要求每一層的樣式可能是不同的,所以再每次調用遞歸組件的時候,我們都需要從父組件的 props 中拿到一個 depth 代表層級,并且把這個 depth + 1 繼續傳遞給遞歸的 NestMenu 組件。

    重點主要就是這些,接下來編碼實現。

    先看 NestMenu 組件的 template 部分的大致結構:

    <template>
      <div class="wrap">
        <div class="menu-wrap">
          <div
            class="menu-item"
            v-for="menuItem in data"
          >{{menuItem.name}}</div>
        </div>
        <nest-menu
          :key="activeId"
          :data="subMenu"
          :depth="depth + 1"
        ></nest-menu>
      </div>
    </template>

    和我們預想設計中的一樣, menu-wrap 代表當前菜單層, nest-menu 則就是組件本身,它負責遞歸的渲染子組件。

    首次渲染

    在第一次獲取到整個菜單的數據的時候,我們需要先把每層菜單的選中項默認設置為第一個子菜單,由于它很可能是異步獲取的,所以我們最好是 watch 這個數據來做這個操作。

    // 菜單數據源發生變化的時候 默認選中當前層級的第一項
    const activeId = ref<number | null>(null)
    
    watch(
      () => props.data,
      (newData) => {
        if (!activeId.value) {
          if (newData && newData.length) {
            activeId.value = newData[0].id
          }
        }
      },
      {
        immediate: true,
      }
    )

    現在我們從最上層開始講起,第一層的 activeId 被設置成了 生命科學競賽 的 id,注意我們傳遞給遞歸子組件的 data ,也就是 生命科學競賽 的 child,是通過 subMenu 獲取到的,它是一個計算屬性:

    const getActiveSubMenu = () => {
      return data.find(({ id }) => id === activeId.value)._child
    }
    const subMenu = computed(getActiveSubMenu)

    這樣,就拿到了 生命科學競賽 的 child,作為子組件的數據傳遞下去了。

    點擊菜單項

    回到之前的需求設計,在點擊了菜單項后,無論點擊的是哪層,都需要把完整的 id 鏈路通過 emit 傳遞到最外層去,所以這里我們需要多做一些處理:

    /**
     * 遞歸收集子菜單第一項的 id
     */
    const getSubIds = (child) => {
      const subIds = []
      const traverse = (data) => {
        if (data && data.length) {
          const first = data[0]
          subIds.push(first.id)
          traverse(first._child)
        }
      }
      traverse(child)
      return subIds
    }
    
    const onMenuItemClick = (menuItem) => {
      const newActiveId = menuItem.id
      if (newActiveId !== activeId.value) {
        activeId.value = newActiveId
        const child = getActiveSubMenu()
        const subIds = getSubIds(child)
        // 把子菜單的默認第一項 ids 也拼接起來 向父組件 emit
        context.emit('change', [newActiveId, ...subIds])
      }
    }

    由于我們之前定的規則是,點擊了新的菜單以后默認選中子菜單的第一項,所以這里我們也遞歸去找子菜單數據里的第一項,放到 subIds 中,直到最底層。

    注意這里的 context.emit("change", [newId, ...subIds]);,這里是把事件向上 emit,如果這個菜單是中間層級的菜單,那么它的父組件也是 NestMenu,我們需要在父層級遞歸調用 NestMenu 組件的時候監聽這個 change 事件。

    <nest-menu
        :key="activeId"
        v-if="activeId !== null"
        :data="getActiveSubMenu()"
        :depth="depth + 1"
        @change="onSubActiveIdChange"
    ></nest-menu>

    在父層級的菜單接受到了子層級的菜單的 change 事件后,需要怎么做呢?沒錯,需要進一步的再向上傳遞:

    const onSubActiveIdChange = (ids) => {
      context.emit('change', [activeId.value].concat(ids))
    }

    這里就只需要簡單的把自己當前的 activeId 拼接到數組的最前面,再繼續向上傳遞即可。

    這樣,任意一層的組件點擊了菜單后,都會先用自己的 activeId 拼接好所有子層級的默認 activeId,再一層層向上 emit。并且向上的每一層父菜單都會把自己的 activeId 拼在前面,就像接力一樣。

    最后,我們在應用層級的組件里,就可以輕松的拿到完整的 id 鏈路:

    <template>
      <nest-menu :data="menu" @change="activeIdsChange" />
    </template>
    
    export default {
      methods: {
        activeIdsChange(ids) {
          this.ids = ids;
          console.log("當前選中的id路徑", ids);
      },
    },

    樣式區分

    由于我們每次調用遞歸組件的時候,都會把 depth + 1,那么就可以通過把這個數字拼接到類名后面來實現樣式區分了。

    <template>
      <div class="wrap">
        <div class="menu-wrap" :class="`menu-wrap-${depth}`">
          <div class="menu-item">{{menuItem.name}}</div>
        </div>
        <nest-menu />
      </div>
    </template>
    
    <style>
    .menu-wrap-0 {
      background: #ffccc7;
    }
    
    .menu-wrap-1 {
      background: #fff7e6;
    }
    
    .menu-wrap-2 {
      background: #fcffe6;
    }
    </style>

    默認高亮

    上面的代碼寫完后,應對沒有默認值時的需求已經足夠了,這時候面試官說,產品要求這個組件能通過傳入任意一個層級的 id 來默認展示高亮。

    其實這也難不倒我們,稍微改造一下代碼,在父組件里假設我們通過 url 參數或者任意方式拿到了一個 activeId,先通過深度優先遍歷的方式查找到這個 id 的所有父級。

    const activeId = 7
    
    const findPath = (menus, targetId) => {
      let ids
    
      const traverse = (subMenus, prev) => {
        if (ids) {
          return
        }
        if (!subMenus) {
          return
        }
        subMenus.forEach((subMenu) => {
          if (subMenu.id === activeId) {
            ids = [...prev, activeId]
            return
          }
          traverse(subMenu._child, [...prev, subMenu.id])
        })
      }
    
      traverse(menus, [])
    
      return ids
    }
    
    const ids = findPath(data, activeId)

    這里我選擇在遞歸的時候帶上上一層的 id,在找到了目標 id 以后就能輕松的拼接處完整的父子 id 數組。

    然后我們把構造好的 ids 作為 activeIds 傳遞給 NestMenu,此時這時候 NestMenu 就要改變一下設計,成為一個「受控組件」,它的渲染狀態是受我們外層傳遞的數據控制的。

    所以我們需要在初始化參數的時候改變一下取值邏輯,優先取 activeIds[depth] ,并且在點擊菜單項的時候,要在最外層的頁面組件中,接收到 change 事件時,把 activeIds 的數據同步改變。這樣繼續傳遞下去才不會導致 NestMenu 接收到的數據混亂。

    <template>
      <nest-menu :data="data" :defaultActiveIds="ids" @change="activeIdsChange" />
    </template>

    NestMenu 初始化的時候,對有默認值的情況做一下處理,優先使用數組中取到的 id 值。

    setup(props: IProps, context) {
      const { depth = 0, activeIds } = props;
    
      /**
       * 這里 activeIds 也可能是異步獲取到的 所以用 watch 保證初始化
       */
      const activeId = ref<number | null | undefined>(null);
      watch(
        () => activeIds,
        (newActiveIds) => {
          if (newActiveIds) {
            const newActiveId = newActiveIds[depth];
            if (newActiveId) {
              activeId.value = newActiveId;
            }
          }
        },
        {
          immediate: true,
        }
      );
    }

    這樣,如果 activeIds 數組中取不到的話,默認還是 null,在 watch 到菜單數據變化的邏輯中,如果 activeId 是 null 的話,會被初始化為第一個子菜單的 id。

    watch(
      () => props.data,
      (newData) => {
        if (!activeId.value) {
          if (newData && newData.length) {
            activeId.value = newData[0].id
          }
        }
      },
      {
        immediate: true,
      }
    )

    在最外層頁面容器監聽到 change 事件的時候,要把數據源同步一下:

    <template>
      <nest-menu :data="data" :activeIds="ids" @change="activeIdsChange" />
    </template>
    
    <script>
    import { ref } from "vue";
    
    export default {
      name: "App",
      setup() {
        const activeIdsChange = (newIds) => {
          ids.value = newIds;
        };
    
        return {
          ids,
          activeIdsChange,
        };
      },
    };
    </script>

    如此一來,外部傳入 activeIds 的時候,就可以控制整個 NestMenu 的高亮選中邏輯了。

    數據源變動引發的 bug

    這時候,面試官對著你的 App 文件稍作改動,然后演示了這樣一個 bug:

    App.vue 的 setup 函數中加了這樣的一段邏輯:

    onMounted(() => {
      setTimeout(() => {
        menu.value = [data[0]].slice()
      }, 1000)
    })

    也就是說,組件渲染完成后過了一秒,菜單的最外層只剩下一項了,這時候面試官在一秒之內點擊了最外層的第二項,這個組件在數據源改變之后,會報錯:

    怎么用Vue3+TypeScript實現遞歸菜單組件

    這是因為數據源已經改變了,但是組件內部的 activeId 狀態依然停留在了一個已經不存在了的 id 上。

    這會導致 subMenu 這個 computed 屬性在計算時出錯。

    我們對 watch data 觀測數據源的這段邏輯稍加改動:

    watch(
      () => props.data,
      (newData) => {
        if (!activeId.value) {
          if (newData && newData.length) {
            activeId.value = newData[0].id
          }
        }
        // 如果當前層級的 data 中遍歷無法找到 `activeId` 的值 說明這個值失效了
        // 把它調整成數據源中第一個子菜單項的 id
        if (!props.data.find(({ id }) => id === activeId.value)) {
          activeId.value = props.data?.[0].id
        }
      },
      {
        immediate: true,
        // 在觀測到數據變動之后 同步執行 這樣會防止渲染發生錯亂
        flush: 'sync',
      }
    )

    注意這里的 flush: "sync" 很關鍵,Vue3 對于 watch 到數據源變動之后觸發 callback 這一行為,默認是以 post 也就是渲染之后再執行的,但是在當前的需求下,如果我們用錯誤的 activeId 去渲染,就會直接導致報錯了,所以我們需要手動把這個 watch 變成一個同步行為。

    這下再也不用擔心數據源變動導致渲染錯亂了。

    怎么用Vue3+TypeScript實現遞歸菜單組件

    完整代碼

    App.vue

    <template>
      <nest-menu :data="data" :activeIds="ids" @change="activeIdsChange" />
    </template>
    
    <script>
    import { ref } from "vue";
    import NestMenu from "./components/NestMenu.vue";
    import data from "./menu.js";
    import { getSubIds } from "./util";
    
    export default {
      name: "App",
      setup() {
        // 假設默認選中 id 為 7
        const activeId = 7;
    
        const findPath = (menus, targetId) => {
          let ids;
    
          const traverse = (subMenus, prev) => {
            if (ids) {
              return;
            }
            if (!subMenus) {
              return;
            }
            subMenus.forEach((subMenu) => {
              if (subMenu.id === activeId) {
                ids = [...prev, activeId];
                return;
              }
              traverse(subMenu._child, [...prev, subMenu.id]);
            });
          };
    
          traverse(menus, []);
    
          return ids;
        };
    
        const ids = ref(findPath(data, activeId));
    
        const activeIdsChange = (newIds) => {
          ids.value = newIds;
          console.log("當前選中的id路徑", newIds);
        };
    
        return {
          ids,
          activeIdsChange,
          data,
        };
      },
      components: {
        NestMenu,
      },
    };
    </script>

    NestMenu.vue

    <template>
      <div class="wrap">
        <div class="menu-wrap" :class="`menu-wrap-${depth}`">
          <div
            class="menu-item"
            v-for="menuItem in data"
            :class="getActiveClass(menuItem.id)"
            @click="onMenuItemClick(menuItem)"
            :key="menuItem.id"
          >{{menuItem.name}}</div>
        </div>
        <nest-menu
          :key="activeId"
          v-if="subMenu && subMenu.length"
          :data="subMenu"
          :depth="depth + 1"
          :activeIds="activeIds"
          @change="onSubActiveIdChange"
        ></nest-menu>
      </div>
    </template>
    
    <script lang="ts">
    import { watch, ref, onMounted, computed } from "vue";
    import data from "../menu";
    
    interface IProps {
      data: typeof data;
      depth: number;
      activeIds?: number[];
    }
    
    export default {
      name: "NestMenu",
      props: ["data", "depth", "activeIds"],
      setup(props: IProps, context) {
        const { depth = 0, activeIds, data } = props;
    
        /**
         * 這里 activeIds 也可能是異步獲取到的 所以用 watch 保證初始化
         */
        const activeId = ref<number | null | undefined>(null);
        watch(
          () => activeIds,
          (newActiveIds) => {
            if (newActiveIds) {
              const newActiveId = newActiveIds[depth];
              if (newActiveId) {
                activeId.value = newActiveId;
              }
            }
          },
          {
            immediate: true,
            flush: 'sync'
          }
        );
    
        /**
         * 菜單數據源發生變化的時候 默認選中當前層級的第一項
         */
        watch(
          () => props.data,
          (newData) => {
            if (!activeId.value) {
              if (newData && newData.length) {
                activeId.value = newData[0].id;
              }
            }
            // 如果當前層級的 data 中遍歷無法找到 `activeId` 的值 說明這個值失效了
            // 把它調整成數據源中第一個子菜單項的 id
            if (!props.data.find(({ id }) => id === activeId.value)) {
              activeId.value = props.data?.[0].id;
            }
          },
          {
            immediate: true,
            // 在觀測到數據變動之后 同步執行 這樣會防止渲染發生錯亂
            flush: "sync",
          }
        );
    
        const onMenuItemClick = (menuItem) => {
          const newActiveId = menuItem.id;
          if (newActiveId !== activeId.value) {
            activeId.value = newActiveId;
            const child = getActiveSubMenu();
            const subIds = getSubIds(child);
            // 把子菜單的默認第一項 ids 也拼接起來 向父組件 emit
            context.emit("change", [newActiveId, ...subIds]);
          }
        };
        /**
         * 接受到子組件更新 activeId 的同時
         * 需要作為一個中介告知父組件 activeId 更新了
         */
        const onSubActiveIdChange = (ids) => {
          context.emit("change", [activeId.value].concat(ids));
        };
        const getActiveSubMenu = () => {
          return props.data?.find(({ id }) => id === activeId.value)._child;
        };
        const subMenu = computed(getActiveSubMenu);
    
        /**
         * 樣式相關
         */
        const getActiveClass = (id) => {
          if (id === activeId.value) {
            return "menu-active";
          }
          return "";
        };
    
        /**
         * 遞歸收集子菜單第一項的 id
         */
        const getSubIds = (child) => {
          const subIds = [];
          const traverse = (data) => {
            if (data && data.length) {
              const first = data[0];
              subIds.push(first.id);
              traverse(first._child);
            }
          };
          traverse(child);
          return subIds;
        };
    
        return {
          depth,
          activeId,
          subMenu,
          onMenuItemClick,
          onSubActiveIdChange,
          getActiveClass,
        };
      },
    };
    </script>
    
    <style>
    .wrap {
      padding: 12px 0;
    }
    
    .menu-wrap {
      display: flex;
      flex-wrap: wrap;
    }
    
    .menu-wrap-0 {
      background: #ffccc7;
    }
    
    .menu-wrap-1 {
      background: #fff7e6;
    }
    
    .menu-wrap-2 {
      background: #fcffe6;
    }
    
    .menu-item {
      margin-left: 16px;
      cursor: pointer;
      white-space: nowrap;
    }
    
    .menu-active {
      color: #f5222d;
    }
    </style>

    源碼地址

    github.com/sl1673495/v…

    總結

    一個遞歸的菜單組件,說簡單也簡單,說難也有它的難點。如果我們不理解 Vue 的異步渲染和觀察策略,可能中間的 bug 就會困擾我們許久。所以適當學習原理還是挺有必要的。

    在開發通用組件的時候,一定要注意數據源的傳入時機(同步、異步),對于異步傳入的數據,要利用好 watch 這個 API 去觀測變動,做相應的操作。并且要考慮數據源的變化是否會和組件內原來保存的狀態沖突,在適當的時機要做好清理操作。

    另外留下一個小問題,我在 NestMenu 組件 watch 數據源的時候,選擇這樣去做:

    watch((() => props.data);

    而不是解構后再去觀測:

    const { data } = props;
    watch(() => data);

    到此,相信大家對“怎么用Vue3+TypeScript實現遞歸菜單組件”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!

    向AI問一下細節

    免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

    AI

    古浪县| 昌吉市| 定结县| 汤阴县| 涿鹿县| 高碑店市| 新安县| 双峰县| 宣恩县| 舟山市| 孟村| 兴文县| 柳河县| 博乐市| 广丰县| 于田县| 福安市| 西乌| 洪洞县| 瓮安县| 富裕县| 呼图壁县| 万安县| 高阳县| 静安区| 德化县| 雅安市| 米林县| 岫岩| 潞西市| 碌曲县| 昭苏县| 龙里县| 阿坝县| 咸丰县| 霞浦县| 涞源县| 岳普湖县| 崇礼县| 浪卡子县| 寿光市|