предисловие
Давно это было, друзья. Я только недавно присоединился к новой компании, и спрос очень полный. Обычно у меня действительно нет времени писать статьи, и частота обновлений станет медленнее.
Мне было скучно дома на выходных, и вдруг мой младший брат пришел срочно просить о помощи, он сказал, что во время интервью с Tencent собеседник дал рекурсивное меню 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: '外国语' },
],
},
]
- Элементы меню каждого слоя, если таковые имеются
_childАтрибуты,Этот пункт меню выбранВ дальнейшем мы продолжим отображать все подменю этого пункта и превью анимации:
-
И нажмите на любой из уровней, вам нужно изменить менюполный
idссылка на сайтПередается на самый внешний уровень для запроса данных от родительского компонента. например, щелчок科学研究类. затем наружуemitтакже нужно вывести свое первое подменю植物学与植物生理学изid, и его родительское меню生命科学竞赛ид, то есть[1, 7, 8]. -
Стиль каждого слоя также можно настроить.
выполнить
Очевидно, что это требование для рекурсивных компонентов.При разработке рекурсивных компонентов мы должны сначала подумать о сопоставлении данных с представлениями.
В данных, возвращаемых бэкендом, каждому слою массива может соответствовать пункт меню, тогда слой массива соответствует строке в представлении.В текущем меню этого слоянажал, чтобы выбратьэтого менюchildОн будет использоваться как данные подменю и передаваться рекурсивномуNestMenuкомпоненты, пока в выделенном меню определенного уровня больше не будетchild, рекурсия завершается.
Поскольку требования требуют, чтобы стили каждого слоя могли быть разными, каждый раз, когда вызывается рекурсивный компонент, нам нужно начинать с родительского компонента.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, обратите внимание на 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Полный массив родительских и дочерних идентификаторов можно легко объединить позже.
Затем мы строим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Логика выделена.
Ошибки, вызванные изменением источника данных.
В это время интервьюер вносит небольшое изменение в ваш файл приложения, а затем демонстрирует такой баг:
App.vuesetupВ функцию добавлена следующая логика:
onMounted(() => {
setTimeout(() => {
menu.value = [data[0]].slice()
}, 1000)
})
То есть через одну секунду после завершения рендеринга компонента в самом внешнем слое меню остается только один элемент.В это время интервьюер щелкает второй крайний элемент в течение одной секунды, и этот компонент изменяется, когда данные источник изменен, после чего будет выдано сообщение об ошибке:
Это связано с тем, что источник данных изменился, но внутри компонента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становится синхронным поведением.
Теперь вам не нужно беспокоиться об изменениях источника данных, которые вызывают путаницу при визуализации.
полный код
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>
Адрес источника
Суммировать
Компонент рекурсивного меню прост и удобен, а у сложного есть свои трудности. Если мы не понимаем стратегию асинхронного рендеринга и наблюдения Vue, ошибки в середине могут преследовать нас долгое время. Поэтому необходимо правильно изучить принципы.
При разработке общих компонентов обязательно обратите внимание на время поступления источника данных (синхронный, асинхронный) и эффективно используйте асинхронные входящие данные.watchЭтот API отслеживает изменения и выполняет соответствующие операции. И необходимо учитывать, не будет ли изменение источника данных конфликтовать с состоянием, изначально сохраненным в компоненте, и подчищать в нужный момент.
Также оставив небольшой вопрос, я вNestMenuкомпонентыwatchисточник данных, выберите это:
watch((() => props.data);
Вместо того, чтобы деконструировать, а затем наблюдать:
const { data } = props;
watch(() => data);
Есть ли разница между ними? Это еще один вопрос для глубинного интервью.
Путь к разработке отличных компонентов еще очень долог, и вы можете оставить свое мнение в комментариях~
Набор персонала
Bytedance выдвинул его внутренне. Клиентская инфраструктура - это команда инфраструктуры терминалов Bytedance. Это инфраструктурный отдел для мобильных, веб, настольных компьютеров и других терминальных предприятий полной бизнес-линии Bytedance. R & D Эффективность и опыт размещения платформ, инструментов, фреймворков и специальные технические Возможности. Области исследований. И т. Д. В Пекине есть центры НИОКР, Шанхай, Гуанчжоу, Шэньчжэнь и Ханчжоу.
Шанхай одноклассники здесь кнопка доставки, приходите в наш отдел, и я делаю коллеги ~
В других регионах (Пекин, Шанхай, Гуанчжоу, Шэньчжэнь и Ханчжоу) вы также можете искать нужное направление деятельности и место работы, и вы можете напрямую доставить его по ссылке ниже.
Школьный набор студентов смотрите здесь:
Ссылка на доставку:job.toutiao.com/s/JhRV7nC
❤️ Всем спасибо
1. Если эта статья была вам полезна, пожалуйста, поддержите ее лайком, ваш "лайк" - движущая сила моего творчества.
2. Подпишитесь на официальный аккаунт «Front-end from advanced to accept», чтобы добавить меня в друзья, я втяну вас в «Front-end группу расширенного обмена», все смогут общаться и добиваться прогресса вместе.