递归组件 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的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。