Skip to content

Commit 95a1b4a

Browse files
committed
perf: menu lazy render children #4812
1 parent 30f87e6 commit 95a1b4a

File tree

17 files changed

+432
-219
lines changed

17 files changed

+432
-219
lines changed

components/dropdown/demo/sub-menu.vue

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ The menu has multiple levels.
2626
<a-menu>
2727
<a-menu-item>1st menu item</a-menu-item>
2828
<a-menu-item>2nd menu item</a-menu-item>
29-
<a-sub-menu key="test" title="sub menu">
29+
<a-sub-menu key="sub1" title="sub menu">
3030
<a-menu-item>3rd menu item</a-menu-item>
3131
<a-menu-item>4th menu item</a-menu-item>
3232
</a-sub-menu>
33-
<a-sub-menu title="disabled sub menu" disabled>
33+
<a-sub-menu key="sub2" title="disabled sub menu" disabled>
3434
<a-menu-item>5d menu item</a-menu-item>
3535
<a-menu-item>6th menu item</a-menu-item>
3636
</a-sub-menu>

components/layout/__tests__/__snapshots__/demo.test.js.snap

+113-42
Large diffs are not rendered by default.

components/menu/__tests__/__snapshots__/demo.test.js.snap

+156-88
Large diffs are not rendered by default.

components/menu/__tests__/index.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ describe('Menu', () => {
5555
{ attachTo: 'body', sync: false },
5656
);
5757
await asyncExpect(() => {
58-
expect($$('.ant-menu-submenu-selected').length).toBe(2);
58+
expect($$('li.ant-menu-submenu-selected').length).toBe(1);
5959
});
6060
});
6161
it('should accept openKeys in mode horizontal', async () => {

components/menu/demo/horizontal.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Horizontal top navigation menu.
3030
</template>
3131
Navigation Two
3232
</a-menu-item>
33-
<a-sub-menu>
33+
<a-sub-menu key="sub1">
3434
<template #icon>
3535
<setting-outlined />
3636
</template>

components/menu/demo/inline-collapsed.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export default defineComponent({
108108
109109
watch(
110110
() => state.openKeys,
111-
(val, oldVal) => {
111+
(_val, oldVal) => {
112112
state.preOpenKeys = oldVal;
113113
},
114114
);

components/menu/demo/template.vue

+4-2
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ Use the single file method to recursively generate menus.
2121
<MenuFoldOutlined v-else />
2222
</a-button>
2323
<a-menu
24-
:default-selected-keys="['1']"
25-
:default-open-keys="['2']"
24+
v-model:openKeys="openKeys"
25+
v-model:selectedKeys="selectedKeys"
2626
mode="inline"
2727
theme="dark"
2828
:inline-collapsed="collapsed"
@@ -119,6 +119,8 @@ export default defineComponent({
119119
list,
120120
collapsed,
121121
toggleCollapsed,
122+
selectedKeys: ref(['1']),
123+
openKeys: ref(['2']),
122124
};
123125
},
124126
});

components/menu/index.en-US.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ More layouts with navigation: [Layout](/components/layout).
2020
<template>
2121
<a-menu>
2222
<a-menu-item>Menu</a-menu-item>
23-
<a-sub-menu title="SubMenu">
23+
<a-sub-menu key="sub1" title="SubMenu">
2424
<a-menu-item>SubMenuItem</a-menu-item>
2525
</a-sub-menu>
2626
</a-menu>

components/menu/index.zh-CN.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/3XZcjGpvK/Menu.svg
2121
<template>
2222
<a-menu>
2323
<a-menu-item>菜单项</a-menu-item>
24-
<a-sub-menu title="子菜单">
24+
<a-sub-menu key="sub1" title="子菜单">
2525
<a-menu-item>子菜单项</a-menu-item>
2626
</a-sub-menu>
2727
</a-menu>

components/menu/src/ItemGroup.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { ExtractPropTypes } from 'vue';
33
import { computed, defineComponent } from 'vue';
44
import PropTypes from '../../_util/vue-types';
55
import { useInjectMenu } from './hooks/useMenuContext';
6+
import { useMeasure } from './hooks/useKeyPath';
67

78
const menuItemGroupProps = {
89
title: PropTypes.VNodeChild,
@@ -18,7 +19,9 @@ export default defineComponent({
1819
setup(props, { slots, attrs }) {
1920
const { prefixCls } = useInjectMenu();
2021
const groupPrefixCls = computed(() => `${prefixCls.value}-item-group`);
22+
const isMeasure = useMeasure();
2123
return () => {
24+
if (isMeasure) return slots.default?.();
2225
return (
2326
<li {...attrs} onClick={e => e.stopPropagation()} class={groupPrefixCls.value}>
2427
<div

components/menu/src/Menu.tsx

+76-55
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Key } from '../../_util/type';
2-
import type { ExtractPropTypes, PropType, UnwrapRef } from 'vue';
2+
import type { ExtractPropTypes, PropType } from 'vue';
33
import { computed, defineComponent, ref, inject, watchEffect, watch, onMounted, unref } from 'vue';
44
import shallowEqual from '../../_util/shallowequal';
55
import type { StoreMenuInfo } from './hooks/useMenuContext';
@@ -24,13 +24,15 @@ import MenuItem from './MenuItem';
2424
import SubMenu from './SubMenu';
2525
import EllipsisOutlined from '@ant-design/icons-vue/EllipsisOutlined';
2626
import { cloneElement } from '../../_util/vnode';
27+
import { OVERFLOW_KEY, PathContext } from './hooks/useKeyPath';
2728

2829
export const menuProps = {
2930
id: String,
3031
prefixCls: String,
3132
disabled: Boolean,
3233
inlineCollapsed: Boolean,
3334
disabledOverflow: Boolean,
35+
forceSubMenuRender: Boolean,
3436
openKeys: Array,
3537
selectedKeys: Array,
3638
activeKey: String, // 内部组件使用
@@ -60,6 +62,7 @@ export type MenuProps = Partial<ExtractPropTypes<typeof menuProps>>;
6062
const EMPTY_LIST: string[] = [];
6163
export default defineComponent({
6264
name: 'AMenu',
65+
inheritAttrs: false,
6366
props: menuProps,
6467
emits: [
6568
'update:openKeys',
@@ -71,7 +74,7 @@ export default defineComponent({
7174
'update:activeKey',
7275
],
7376
slots: ['expandIcon', 'overflowedIndicator'],
74-
setup(props, { slots, emit }) {
77+
setup(props, { slots, emit, attrs }) {
7578
const { prefixCls, direction } = useConfigInject('menu', props);
7679
const store = ref<Record<string, StoreMenuInfo>>({});
7780
const siderCollapsed = inject(SiderCollapsedKey, ref(undefined));
@@ -102,7 +105,7 @@ export default defineComponent({
102105

103106
const activeKeys = ref([]);
104107
const mergedSelectedKeys = ref([]);
105-
const keyMapStore = ref({});
108+
const keyMapStore = ref<Record<Key, StoreMenuInfo>>({});
106109
watch(
107110
store,
108111
() => {
@@ -117,11 +120,9 @@ export default defineComponent({
117120
watchEffect(() => {
118121
if (props.activeKey !== undefined) {
119122
let keys = [];
120-
const menuInfo = props.activeKey
121-
? (keyMapStore.value[props.activeKey] as UnwrapRef<StoreMenuInfo>)
122-
: undefined;
123+
const menuInfo = props.activeKey ? keyMapStore.value[props.activeKey] : undefined;
123124
if (menuInfo && props.activeKey !== undefined) {
124-
keys = [...menuInfo.parentKeys, props.activeKey];
125+
keys = uniq([].concat(unref(menuInfo.parentKeys), props.activeKey));
125126
} else {
126127
keys = [];
127128
}
@@ -139,22 +140,21 @@ export default defineComponent({
139140
{ immediate: true },
140141
);
141142

142-
const selectedSubMenuEventKeys = ref([]);
143-
143+
const selectedSubMenuKeys = ref([]);
144144
watch(
145145
[keyMapStore, mergedSelectedKeys],
146146
() => {
147-
let subMenuParentEventKeys = [];
147+
let subMenuParentKeys = [];
148148
mergedSelectedKeys.value.forEach(key => {
149149
const menuInfo = keyMapStore.value[key];
150150
if (menuInfo) {
151-
subMenuParentEventKeys.push(...unref(menuInfo.parentEventKeys));
151+
subMenuParentKeys = subMenuParentKeys.concat(unref(menuInfo.parentKeys));
152152
}
153153
});
154154

155-
subMenuParentEventKeys = uniq(subMenuParentEventKeys);
156-
if (!shallowEqual(selectedSubMenuEventKeys.value, subMenuParentEventKeys)) {
157-
selectedSubMenuEventKeys.value = subMenuParentEventKeys;
155+
subMenuParentKeys = uniq(subMenuParentKeys);
156+
if (!shallowEqual(selectedSubMenuKeys.value, subMenuParentKeys)) {
157+
selectedSubMenuKeys.value = subMenuParentKeys;
158158
}
159159
},
160160
{ immediate: true },
@@ -321,16 +321,16 @@ export default defineComponent({
321321
triggerSelection(info);
322322
};
323323

324-
const onInternalOpenChange = (eventKey: Key, open: boolean) => {
325-
const { key, childrenEventKeys } = store.value[eventKey];
324+
const onInternalOpenChange = (key: Key, open: boolean) => {
325+
const childrenEventKeys = keyMapStore.value[key].childrenEventKeys;
326326
let newOpenKeys = mergedOpenKeys.value.filter(k => k !== key);
327327

328328
if (open) {
329329
newOpenKeys.push(key);
330330
} else if (mergedMode.value !== 'inline') {
331331
// We need find all related popup to close
332332
const subPathKeys = getChildrenKeys(childrenEventKeys);
333-
newOpenKeys = newOpenKeys.filter(k => !subPathKeys.includes(k));
333+
newOpenKeys = uniq(newOpenKeys.filter(k => !subPathKeys.includes(k)));
334334
}
335335

336336
if (!shallowEqual(mergedOpenKeys, newOpenKeys)) {
@@ -388,9 +388,10 @@ export default defineComponent({
388388
onItemClick: onInternalClick,
389389
registerMenuInfo,
390390
unRegisterMenuInfo,
391-
selectedSubMenuEventKeys,
391+
selectedSubMenuKeys,
392392
isRootMenu: ref(true),
393393
expandIcon,
394+
forceSubMenuRender: computed(() => props.forceSubMenuRender),
394395
});
395396
return () => {
396397
const childList = flattenChildren(slots.default?.());
@@ -415,43 +416,63 @@ export default defineComponent({
415416
const overflowedIndicator = slots.overflowedIndicator?.() || <EllipsisOutlined />;
416417

417418
return (
418-
<Overflow
419-
prefixCls={`${prefixCls.value}-overflow`}
420-
component="ul"
421-
itemComponent={MenuItem}
422-
class={className.value}
423-
role="menu"
424-
id={props.id}
425-
data={wrappedChildList}
426-
renderRawItem={node => node}
427-
renderRawRest={omitItems => {
428-
// We use origin list since wrapped list use context to prevent open
429-
const len = omitItems.length;
430-
431-
const originOmitItems = len ? childList.slice(-len) : null;
432-
433-
return (
434-
<SubMenu
435-
eventKey={Overflow.OVERFLOW_KEY}
436-
title={overflowedIndicator}
437-
disabled={allVisible}
438-
internalPopupClose={len === 0}
439-
>
440-
{originOmitItems}
441-
</SubMenu>
442-
);
443-
}}
444-
maxCount={
445-
mergedMode.value !== 'horizontal' || props.disabledOverflow
446-
? Overflow.INVALIDATE
447-
: Overflow.RESPONSIVE
448-
}
449-
ssr="full"
450-
data-menu-list
451-
onVisibleChange={newLastIndex => {
452-
lastVisibleIndex.value = newLastIndex;
453-
}}
454-
/>
419+
<>
420+
<Overflow
421+
{...attrs}
422+
prefixCls={`${prefixCls.value}-overflow`}
423+
component="ul"
424+
itemComponent={MenuItem}
425+
class={className.value}
426+
role="menu"
427+
id={props.id}
428+
data={wrappedChildList}
429+
renderRawItem={node => node}
430+
renderRawRest={omitItems => {
431+
// We use origin list since wrapped list use context to prevent open
432+
const len = omitItems.length;
433+
434+
const originOmitItems = len ? childList.slice(-len) : null;
435+
436+
return (
437+
<>
438+
<SubMenu
439+
eventKey={OVERFLOW_KEY}
440+
key={OVERFLOW_KEY}
441+
title={overflowedIndicator}
442+
disabled={allVisible}
443+
internalPopupClose={len === 0}
444+
>
445+
{originOmitItems}
446+
</SubMenu>
447+
<PathContext>
448+
<SubMenu
449+
eventKey={OVERFLOW_KEY}
450+
key={OVERFLOW_KEY}
451+
title={overflowedIndicator}
452+
disabled={allVisible}
453+
internalPopupClose={len === 0}
454+
>
455+
{originOmitItems}
456+
</SubMenu>
457+
</PathContext>
458+
</>
459+
);
460+
}}
461+
maxCount={
462+
mergedMode.value !== 'horizontal' || props.disabledOverflow
463+
? Overflow.INVALIDATE
464+
: Overflow.RESPONSIVE
465+
}
466+
ssr="full"
467+
data-menu-list
468+
onVisibleChange={newLastIndex => {
469+
lastVisibleIndex.value = newLastIndex;
470+
}}
471+
/>
472+
<div style={{ display: 'none' }} aria-hidden>
473+
<PathContext>{wrappedChildList}</PathContext>
474+
</div>
475+
</>
455476
);
456477
};
457478
},

components/menu/src/MenuItem.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { flattenChildren, getPropsSlot, isValidElement } from '../../_util/props
22
import PropTypes from '../../_util/vue-types';
33
import type { ExtractPropTypes } from 'vue';
44
import { computed, defineComponent, getCurrentInstance, onBeforeUnmount, ref, watch } from 'vue';
5-
import { useInjectKeyPath } from './hooks/useKeyPath';
5+
import { useInjectKeyPath, useMeasure } from './hooks/useKeyPath';
66
import { useInjectFirstLevel, useInjectMenu } from './hooks/useMenuContext';
77
import { cloneElement } from '../../_util/vnode';
88
import Tooltip from '../../tooltip';
@@ -32,7 +32,7 @@ export default defineComponent({
3232
slots: ['icon', 'title'],
3333
setup(props, { slots, emit, attrs }) {
3434
const instance = getCurrentInstance();
35-
35+
const isMeasure = useMeasure();
3636
const key =
3737
typeof instance.vnode.key === 'symbol' ? String(instance.vnode.key) : instance.vnode.key;
3838
devWarning(
@@ -70,7 +70,6 @@ export default defineComponent({
7070
parentKeys,
7171
isLeaf: true,
7272
};
73-
7473
registerMenuInfo(eventKey, menuInfo);
7574

7675
onBeforeUnmount(() => {
@@ -174,6 +173,7 @@ export default defineComponent({
174173
const directionStyle = useDirectionStyle(computed(() => keysPath.value.length));
175174

176175
return () => {
176+
if (isMeasure) return null;
177177
const title = props.title ?? slots.title?.();
178178
const children = flattenChildren(slots.default?.());
179179
const childrenLength = children.length;

components/menu/src/PopupTrigger.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Trigger from '../../vc-trigger';
22
import type { PropType } from 'vue';
33
import { computed, defineComponent, onBeforeUnmount, ref, watch } from 'vue';
44
import type { MenuMode } from './interface';
5-
import { useInjectMenu } from './hooks/useMenuContext';
5+
import { useInjectForceRender, useInjectMenu } from './hooks/useMenuContext';
66
import { placements, placementsRtl } from './placements';
77
import type { RafFrame } from '../../_util/raf';
88
import raf from '../../_util/raf';
@@ -39,8 +39,9 @@ export default defineComponent({
3939
builtinPlacements,
4040
triggerSubMenuAction,
4141
isRootMenu,
42+
forceSubMenuRender,
4243
} = useInjectMenu();
43-
44+
const forceRender = useInjectForceRender();
4445
const placement = computed(() =>
4546
rtl.value
4647
? { ...placementsRtl, ...builtinPlacements.value }
@@ -91,7 +92,7 @@ export default defineComponent({
9192
mouseEnterDelay={subMenuOpenDelay.value}
9293
mouseLeaveDelay={subMenuCloseDelay.value}
9394
onPopupVisibleChange={onVisibleChange}
94-
forceRender={true}
95+
forceRender={forceRender || forceSubMenuRender.value}
9596
v-slots={{
9697
popup: () => {
9798
return slots.popup?.({ visible: innerVisible.value });

0 commit comments

Comments
 (0)