递归组件 vue,vue中的递归组件

  递归组件 vue,vue中的递归组件

  Vue.js中的递归组件是一个可以调用自身的组件。递归组件通常用于在博客上显示评论、形状菜单或嵌套菜单。本文主要介绍Vue3 TypeScript中递归菜单组件实现的相关信息。有需要的朋友可以参考一下。

  

目录

  前言

  要求

  实现

  首次渲染

  单击菜单项。

  风格辨别

  默认突出显示

  数据源更改导致的错误

  完全码

  App.vue

  摘要

  

前言

  我很久没见到我的朋友了。最近刚入职一家新公司,需求很满。平时真的没时间写文章,更新频率也会变慢。

  周末在家闲着无聊,突然弟弟来紧急求助,说是腾讯面试,对方给了我一个Vue的递归菜单实现,回来找我回复。

  本周恰好是周晓。没想过出去玩,就在家写代码。我看了看需求,确实很复杂。我需要好好利用递归组件,正好利用这一点。

  我有机会总结一篇关于在Vue3 TS中实现递归组件的文章。

  

需求

  可以先在Github页面预览一下效果。

  这是要求。后端将返回一个可能具有无限级别的菜单字符串,格式如下:

  [

  {

  id: 1,

  父亲id: 0,

  状态:1,

  名称:“生命科学竞赛”,

  _child: [

  {

  id: 2,

  father_id: 1,

  状态:1,

  名称:野外实习课,

  _ child: [{id: 3,father _ id: 2,status: 1,name: botany}],

  },

  {

  id: 7,

  father_id: 1,

  状态:1,

  名称:科研类别,

  _child: [

  {ID: 8,Father _ ID: 7,状态:1,姓名:‘植物学与植物生理学’},

  {ID: 9,Father _ ID: 7,状态:1,姓名:‘动物学与动物生理学’},

  {id: 10,父亲id: 7,状态:1,名称:“微生物学”},

  {id: 11,father _ id: 7,状态:1,名称:生态 },

  ],

  },

  {id: 71,父亲id: 1,状态:1,姓名:添加 },

  ],

  },

  {

  身份证号:56,

  父亲id: 0,

  状态:1,

  姓名:与考研有关,

  _child: [

  {id: 57,father _ id: 56,status: 1,name: politics},

  {id: 58,father _ id: 56,状态:1,姓名:外语 },

  ],

  },

  ]

  1.如果每一层的菜单元素都有_child属性,那么在选择该菜单后,我们将继续显示该项目的所有子菜单,并预览动画:

  2.单击任何一个级别,您需要将菜单的完整id链接传输到最外层,以请求父组件的数据。比如点击科研类。然后你需要带上它的第一个子菜单植物学和植物生理学的id,以及它的母菜单生命科学竞赛的id,也就是[1,7,8]。

  3.每一层的风格也可以自己定制。

  

实现

  显然,这是递归组件的要求。在设计递归组件时,我们应该首先考虑数据到视图的映射。

  在后端返回的数据中,数组的每一层可以分别对应一个菜单项,然后数组的每一层对应视图中的一行。在当前层的菜单中,通过点击选择的菜单的子菜单将作为子菜单数据,并移交给递归的NestMenu组件。直到某一层的高亮菜单中没有子菜单,递归才会终止。

  因为需求要求每一层的样式可能不一样,所以我们每次调用递归组件,都需要从父组件的props中获取一个Depth1代表级别,并继续将这个depth 1传递给递归的NestMenu组件。

  以上是要点,然后实现编码。

  首先看看NestMenu组件的模板部分的一般结构:

  模板

  div class=wrap

  div class=菜单换行

  差异

  class=菜单项

  v-for=数据中的菜单项

  {{menuItem.name}}/div

  /div

  嵌套菜单

  :key=activeId

  :data=子菜单

  :深度=深度1

  /嵌套菜单

  /div

  /模板

  正如我们在设计中所预期的,menu-wrap表示当前菜单层,nest-menu是组件本身,负责递归呈现子组件。

  

首次渲染

  第一次获取整个菜单的数据时,我们需要将每个菜单的选中项默认设置为第一个子菜单。由于很可能是异步获取的,所以我们最好观察这些数据来做这个操作。

  //当菜单数据源发生变化时,默认选择当前级别的第一项。

  const activeId=ref number null(null)

  手表(

  ()=props.data,

  (新数据)={

  如果(!activeId.value) {

  if (newData newData.length) {

  activeId.value=newData[0]。编号

  }

  }

  },

  {

  即时:真的,

  }

  )

  现在让我们从顶层开始。第一层的activeId设置为生命科学竞赛的Id。请注意,我们传递给递归子组件(即生命科学竞赛的子组件)的数据是通过子菜单获得的,这是一个计算属性:

  const getActiveSubMenu=()={

  返回data . find(({ id })=id===activeid . value)。_儿童

  }

  const subMenu=computed(getActiveSubMenu)

  就这样,我得到了生命科学竞赛的孩子,把数据作为子组件传递下去。

  

点击菜单项

  回到之前的需求设计,点击菜单项后,无论点击哪一层,都需要通过emit将完整的id链接传递到最外层,所以这里我们需要再做一些处理:

  /**

  *递归收集子菜单中第一项的id

  */

  const getSubIds=(child)={

  const subIds=[]

  常量导线=(数据)={

  if (data data.length) {

  const first=data[0]

  subIds.push(first.id)

  遍历(首先。_child)

  }

  }

  遍历(子)

  返回子id

  }

  const on menuItem click=(menuItem)={

  const newActiveId=menuItem.id

  if (newActiveId!==activeId.value) {

  activeId.value=newActiveId

  const child=getActiveSubMenu()

  const subIds=getSubIds(child)

  //拼接子菜单的默认首项id,并发送给父组件emit。

  context.emit(change ,[newActiveId,子id])

  }

  }

  由于我们之前的规则是点击新建菜单后默认选择子菜单中的第一项,所以这里我们也递归找到子菜单数据中的第一项,放在subIds中,直到底部。

  注意context.emit (change ,[newid,下属])这里;这里是向上发出事件。如果这个菜单是一个中级菜单,那么它的父组件也是NestMenu。当父级递归调用NestMenu组件时,我们需要监听这个change事件。

  嵌套菜单

  :key=activeId

  v-if=activeId!==null

  :data=getActiveSubMenu()

  :深度=深度1

  @change=onSubActiveIdChange

  /嵌套菜单

  父菜单收到子菜单的change事件后该怎么办?没错,需要进一步向上传递:

  const onSubActiveIdChange=(ids)={

  context.emit(change ,[activeId.value])。concat(ids))

  }

  在这里,您只需要将当前的activeId拼接到数组的前面,然后继续向上传递它。

  这样,点击菜单后,任何一层的组件都会将所有子层的默认活动id与自己的活动id拼接,然后逐层发射。而每一级以上的父菜单都会在前面拼出它的activeId,就像接力一样。

  最后,我们可以轻松获得应用程序级组件中的完整id链接:

  模板

  nest-menu:data= menu @ change= activeIdsChange /

  /模板

  导出默认值{

  方法:{

  activeIdsChange(ids) {

  this.ids=ids

  Console.log(当前选择的id路径,ids);

  },

  },

  

样式区分

  由于我们每次调用递归组件,都会放入深度1,那么我们就可以通过将这个数字拼接到类名的后面来实现样式区分。

  模板

  div class=wrap

  div class= menu-wrap :class= ` menu-wrap-$ { depth } `

  div class=菜单项“{menuItem.name}}/div

  /div

  嵌套菜单/

  /div

  /模板

  风格。菜单-换行-0 {

  背景:# ffccc7

  }。菜单-换行-1 {

  背景:# fff7e6

  }。菜单-换行-2 {

  背景:# fcffe6

  }

  /风格

  

默认高亮

  上面的代码写好之后,在没有默认值的情况下,处理需求就足够了。这时面试官说产品要求这个组件通过传入任意级别的id默认显示高亮。

  事实上,这不会打败我们。让我们稍微修改一下代码。在父组件中,我们假设通过url参数或任何方式获得一个activeId。首先,我们通过深度优先遍历找到这个id的所有父id。

  const activeId=7

  const findPath=(menus,targetId)={

  让身份证

  const traverse=(子菜单,prev)={

  if (ids) {

  返回

  }

  如果(!子菜单){

  返回

  }

  subMenus.forEach((子菜单)={

  if (subMenu.id===activeId) {

  ids=[.上一个,活动Id]

  返回

  }

  遍历(子菜单。_child,[.prev,subMenu.id])

  })

  }

  遍历(菜单,[])

  返回id

  }

  const ids=findPath(data,activeId)

  这里我选择递归时取上一层的id,找到目标id后就可以轻松拼接出完整的父子id数组。

  然后,我们将构造的id作为activeIds传递给NestMenu。这时候NestMenu就会改变设计,变成“受控组件”。它的渲染状态是由我们外层传输的数据控制的。

  所以我们在初始化参数的时候需要改变值逻辑,优先考虑activeIds[深度],在点击菜单项的时候,在最外层的页面组件中接收到change事件的时候,要同步改变activeIds的数据。这样,NestMenu接收到的数据如果传递就不会混淆。

  模板

  nest-menu:data= data :defaultActiveIds= ids @ change= activeIdsChange /

  /模板

  NestMenu初始化时,处理默认值,优先处理从数组中获取的id值。

  设置(props: IProps,上下文){

  const { depth=0,activeIds }=props

  /**

  *在这里,activeIds也可能是异步获取的,所以使用watch来保证初始化。

  */

  const activeId=ref number null undefined(null);

  手表(

  ()=activeIds,

  (新活动id)={

  if(新活动id){

  const new activeid=new activeids[depth];

  if (newActiveId) {

  activeId.value=newActiveId

  }

  }

  },

  {

  即时:真的,

  }

  );

  }

  这样,如果在activeIds数组中找不到它,默认情况下它仍然是null。在观察菜单数据变化的逻辑中,如果activeId为null,则初始化为第一个子菜单的Id。

  手表(

  ()=props.data,

  (新数据)={

  如果(!activeId.value) {

  if (newData newData.length) {

  activeId.value=newData[0]。编号

  }

  }

  },

  {

  即时:真的,

  }

  )

  当最外层的页面容器侦听更改事件时,同步数据源:

  模板

  nest-menu:data= data :activeIds= ids @ change= activeIdsChange /

  /模板

  脚本

  从“vue”导入{ ref };

  导出默认值{

  名称:“应用程序”,

  setup() {

  const activeIdsChange=(newIds)={

  ids.value=newIds

  };

  返回{

  身份证,

  activeIdsChange,

  };

  },

  };

  /脚本

  这样,当从外部导入activeIds时,就可以控制整个NestMenu的高亮逻辑。

  

数据源变动引发的 bug

  这时候面试官对你的App文件做了一些修改,然后演示了这样一个bug:

  此逻辑被添加到App.vue的设置功能中:

  onMounted(()={

  setTimeout(()={

  menu.value=[data[0]]。切片()

  }, 1000)

  })

  也就是组件渲染完成后一秒,菜单最外层只剩下一个项目。此时面试官在一秒钟内点击最外层的第二项,数据源改变后这个组件会报错:

  这是因为数据源已经更改,但是组件内部的activeId状态仍然停留在一个不再存在的Id上。

  这将导致子菜单的computed属性的计算出错。

  我们对观察数据的这个逻辑观察数据源做一点小小的改动:

  手表(

  ()=props.data,

  (新数据)={

  如果(!activeId.value) {

  if (newData newData.length) {

  activeId.value=newData[0].编号

  }

  }

  //如果当前层级的数据中遍历无法找到“活动Id”的值说明这个值失效了

  //把它调整成数据源中第一个子菜单项的编号

  如果(!道具。数据。find(({ id })=id===activeid。值)){

  activeId.value=props.data?[0].编号

  }

  },

  {

  即时:真的,

  //在观测到数据变动之后同步执行这样会防止渲染发生错乱

  刷新:"同步",

  }

  )

  注意这里的刷新:"同步"很关键,Vue3对于看到数据源变动之后触发回收这一行为,默认是以邮政也就是渲染之后再执行的,但是在当前的需求下,如果我们用错误的activeId去渲染,就会直接导致报错了,所以我们需要手动把这个看变成一个同步行为。

  这下再也不用担心数据源变动导致渲染错乱了。

  

完整代码

  

App.vue

  模板

  nest-menu:data= data :activeIds= ids @ change= activeIdsChange /

  /模板

  脚本

  从“vue”导入{ ref };

  从""导入NestMenu ./组件/嵌套菜单。vue ;

  从导入数据.菜单。js’;

  从导入{ getSubIds } ./util ;

  导出默认值{

  名称:"应用程序",

  setup() {

  //假设默认选中编号为七

  const activeId=7;

  const findPath=(menus,targetId)={

  让本能冲动

  常量导线=(子菜单,prev)={

  if (ids) {

  返回;

  }

  如果(!子菜单){

  返回;

  }

  子菜单. forEach((子菜单)={

  if (subMenu.id===activeId) {

  ids=[.prev,activeId];

  返回;

  }

  遍历(子菜单. child,[.上一个,子菜单。id]);

  });

  };

  遍历(菜单,[]);

  返回id;

  };

  const ids=ref(findPath(data,activeId));

  const activeIdsChange=(newIds)={

  ids.value=newIds

  console.log(当前选中的编号路径,newIds);

  };

  返回{

  身份证,

  activeIdsChange,

  数据,

  };

  },

  组件:{

  NestMenu,

  },

  };

  /脚本

  NestMenu.vue

  模板

  div class=wrap

  div class= menu-wrap :class= ` menu-wrap-$ { depth } `

  差异

   class=菜单项

  v-for=数据中的菜单项

  :class= getActiveClass(menuitem。id)

  @ click= onMenuItemClick(menuItem)

  :key=menuItem.id

  {{menuItem.name}}/div

  /div

  嵌套菜单

  :key=activeId

  v-if=子菜单。长度

  :数据=子菜单

  :深度=深度1

  :activeIds=activeIds

  @change=onSubActiveIdChange

  /嵌套菜单

  /div

  /模板

  脚本语言

  从“vue”导入{ watch,ref,onMounted,computed }。

  导入数据自./menu ;

  接口IProps {

  数据:数据的类型;

  深度:数字;

  activeIds?数字[];

  }

  导出默认值{

  名称: NestMenu ,

  道具:[数据,深度,活动id ],

  设置(道具:IProps,上下文){

  const { depth=0,activeIds,data }=props

  /**

  * 这里activeIds也可能是异步获取到的所以用看保证初始化

  */

  const activeId=ref number null undefined(null);

  手表(

  ()=activeIds,

  (新活动id)={

  如果(新活动id){

  const new activeid=new activeids[depth];

  if (newActiveId) {

  activeId.value=newActiveId

  }

  }

  },

  {

  即时:真的,

  刷新:"同步"

  }

  );

  /**

  * 菜单数据源发生变化的时候默认选中当前层级的第一项

  */

  手表(

  ()=props.data,

  (新数据)={

  如果(!activeId.value) {

  if (newData newData.length) {

  activeId.value=newData[0].id;

  }

  }

  //如果当前层级的数据中遍历无法找到“活动Id”的值说明这个值失效了

  //把它调整成数据源中第一个子菜单项的编号

  如果(!道具。数据。find(({ id })=id===activeid。值)){

  activeId.value=props.data?[0].id;

  }

  },

  {

  即时:真的,

  //观察到数据变化后同步执行。这将防止渲染混乱。

  刷新:“同步”,

  }

  );

  const on menuItem click=(menuItem)={

  const newActiveId=menuItem.id

  if (newActiveId!==activeId.value) {

  activeId.value=newActiveId

  const child=getActiveSubMenu();

  const subIds=getSubIds(child);

  //拼接子菜单的默认首项id,并发送给父组件emit。

  context.emit(change ,[newActiveId,subIds]);

  }

  };

  /**

  *在接收子组件的activeId更新的同时

  *需要充当中介来通知父组件activeId已更新。

  */

  const onSubActiveIdChange=(ids)={

  context.emit(change ,[activeId.value])。concat(ids));

  };

  const getActiveSubMenu=()={

  返回props.data?find(({ id })=id===activeid . value)。_ child

  };

  const subMenu=computed(getActiveSubMenu);

  /**

  *风格相关

  */

  const getActiveClass=(id)={

  if (id===activeId.value) {

  返回“菜单-活动”;

  }

  返回“”;

  };

  /**

  *递归收集子菜单中第一项的id

  */

  const getSubIds=(child)={

  const subIds=[];

  常量导线=(数据)={

  if (data data.length) {

  const first=data[0];

  subids . push(first . id);

  遍历(首先。_ child);

  }

  };

  遍历(子);

  返回子id;

  };

  返回{

  深度,

  activeId,

  子菜单,

  onMenuItemClick,

  onSubActiveIdChange,

  getActiveClass,

  };

  },

  };

  /脚本

  风格。换行{

  填充:12px 0;

  }。菜单换行{

  显示器:flex

  flex-wrap:缠绕;

  }。菜单-换行-0 {

  背景:# ffccc7

  }。菜单-换行-1 {

  背景:# fff7e6

  }。菜单-换行-2 {

  背景:# fcffe6

  }。菜单项{

  左边距:16px

  光标:指针;

  空白:nowrap

  }。菜单-活动{

  颜色:# f5222d

  }

  /风格

  源代码

  github.com/sl1673495/v…

  

总结

  一个递归菜单组件,说起来容易,说起来难,有它的难处。如果不了解Vue的异步渲染和观察策略,中间的bug可能会困扰我们很久。因此,适当地学习原理是非常必要的。

  在开发通用组件时,我们必须注意传入数据源的时序(同步和异步)。对于异步传入的数据,要利用好watch API来观察变化,做相应的操作。并考虑数据源的改变是否会与组件中原来保存的状态发生冲突,在适当的时候做好清理工作。

  还有一个小问题。当我在NestMenu组件中观察数据源时,我选择这样做:

  手表((()=props . data);

  而不是解构然后观察:

  const { data }=props

  手表(()=数据);

  两者有区别吗?这又是一个深度面试问题。

  关于Vue3 TypeScript实现递归菜单组件的这篇文章到此为止。关于Vue3 TypeScript的递归菜单组件的更多信息,请搜索我们以前的文章或继续浏览下面的相关文章。希望大家以后能多多支持我们!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: