Commit 87fcd0d21ea78ce916a4f2b9cdcceda5e7866eee

Authored by vben
1 parent 35d2bfc5

perf: optimize lazy loading components

src/components/Container/src/LazyContainer.vue
... ... @@ -12,12 +12,21 @@
12 12 <script lang="ts">
13 13 import type { PropType } from 'vue';
14 14  
15   - import { defineComponent, reactive, onMounted, ref, unref, onUnmounted, toRefs } from 'vue';
  15 + import {
  16 + defineComponent,
  17 + reactive,
  18 + onMounted,
  19 + ref,
  20 + unref,
  21 + onUnmounted,
  22 + toRef,
  23 + toRefs,
  24 + } from 'vue';
16 25  
17 26 import { Skeleton } from 'ant-design-vue';
18 27 import { useRaf } from '/@/hooks/event/useRaf';
19 28 import { useTimeout } from '/@/hooks/core/useTimeout';
20   -
  29 + import { useIntersectionObserver } from '/@/hooks/event/useIntersectionObserver';
21 30 interface State {
22 31 isInit: boolean;
23 32 loading: boolean;
... ... @@ -30,7 +39,7 @@
30 39 // 等待时间,如果指定了时间,不论可见与否,在指定时间之后自动加载
31 40 timeout: {
32 41 type: Number as PropType<number>,
33   - default: 8000,
  42 + default: 0,
34 43 // default: 8000,
35 44 },
36 45 // 组件所在的视口,如果组件是在页面容器内滚动,视口就是该容器
... ... @@ -40,6 +49,7 @@
40 49 >,
41 50 default: () => null,
42 51 },
  52 +
43 53 // 预加载阈值, css单位
44 54 threshold: {
45 55 type: String as PropType<string>,
... ... @@ -51,6 +61,7 @@
51 61 type: String as PropType<'vertical' | 'horizontal'>,
52 62 default: 'vertical',
53 63 },
  64 +
54 65 // 包裹组件的外层容器的标签名
55 66 tag: {
56 67 type: String as PropType<string>,
... ... @@ -62,20 +73,14 @@
62 73 default: 80,
63 74 },
64 75  
65   - // // 是否在不可见的时候销毁
66   - // autoDestory: {
67   - // type: Boolean as PropType<boolean>,
68   - // default: false,
69   - // },
70   -
71 76 // transition name
72 77 transitionName: {
73 78 type: String as PropType<string>,
74 79 default: 'lazy-container',
75 80 },
76 81 },
77   - emits: ['before-init', 'init'],
78   - setup(props, { emit, slots }) {
  82 + emits: ['init'],
  83 + setup(props, { emit }) {
79 84 const elRef = ref<any>(null);
80 85 const state = reactive<State>({
81 86 isInit: false,
... ... @@ -83,17 +88,10 @@
83 88 intersectionObserverInstance: null,
84 89 });
85 90  
86   - immediateInit();
87 91 onMounted(() => {
  92 + immediateInit();
88 93 initIntersectionObserver();
89 94 });
90   - onUnmounted(() => {
91   - // Cancel the observation before the component is destroyed
92   - if (state.intersectionObserverInstance) {
93   - const el = unref(elRef);
94   - state.intersectionObserverInstance.unobserve(el.$el);
95   - }
96   - });
97 95  
98 96 // If there is a set delay time, it will be executed immediately
99 97 function immediateInit() {
... ... @@ -105,9 +103,6 @@
105 103 }
106 104  
107 105 function init() {
108   - // At this point, the skeleton component is about to be switched
109   - emit('before-init');
110   - // At this point you can prepare to load the resources of the lazy-loaded component
111 106 state.loading = true;
112 107  
113 108 requestAnimationFrameFn(() => {
... ... @@ -120,9 +115,7 @@
120 115 // Prevent waiting too long without executing the callback
121 116 // Set the maximum waiting time
122 117 useTimeout(() => {
123   - if (state.isInit) {
124   - return;
125   - }
  118 + if (state.isInit) return;
126 119 callback();
127 120 }, props.maxWaitingTime || 80);
128 121  
... ... @@ -132,12 +125,10 @@
132 125 }
133 126  
134 127 function initIntersectionObserver() {
135   - const { timeout, direction, threshold, viewport } = props;
136   - if (timeout) {
137   - return;
138   - }
  128 + const { timeout, direction, threshold } = props;
  129 + if (timeout) return;
139 130 // According to the scrolling direction to construct the viewport margin, used to load in advance
140   - let rootMargin;
  131 + let rootMargin: string = '0px';
141 132 switch (direction) {
142 133 case 'vertical':
143 134 rootMargin = `${threshold} 0px`;
... ... @@ -146,35 +137,26 @@
146 137 rootMargin = `0px ${threshold}`;
147 138 break;
148 139 }
  140 +
149 141 try {
150   - // Observe the intersection of the viewport and the component container
151   - state.intersectionObserverInstance = new window.IntersectionObserver(
152   - intersectionHandler,
153   - {
154   - rootMargin,
155   - root: viewport,
156   - threshold: [0, Number.MIN_VALUE, 0.01],
157   - }
158   - );
159   -
160   - const el = unref(elRef);
161   -
162   - state.intersectionObserverInstance.observe(el.$el);
  142 + const { stop, observer } = useIntersectionObserver({
  143 + rootMargin,
  144 + target: toRef(elRef.value, '$el'),
  145 + onIntersect: (entries: any[]) => {
  146 + const isIntersecting = entries[0].isIntersecting || entries[0].intersectionRatio;
  147 + if (isIntersecting) {
  148 + init();
  149 + if (observer) {
  150 + stop();
  151 + }
  152 + }
  153 + },
  154 + root: toRef(props, 'viewport'),
  155 + });
163 156 } catch (e) {
164 157 init();
165 158 }
166 159 }
167   - // Cross-condition change handling function
168   - function intersectionHandler(entries: any[]) {
169   - const isIntersecting = entries[0].isIntersecting || entries[0].intersectionRatio;
170   - if (isIntersecting) {
171   - init();
172   - if (state.intersectionObserverInstance) {
173   - const el = unref(elRef);
174   - state.intersectionObserverInstance.unobserve(el.$el);
175   - }
176   - }
177   - }
178 160 return {
179 161 elRef,
180 162 ...toRefs(state),
... ...
src/hooks/core/types.ts deleted 100644 → 0
1   -import type { VNode, Ref } from 'vue';
2   -import type { ModalFuncProps } from 'ant-design-vue/lib/modal/index';
3   -
4   -export type Fn<T> = () => T;
5   -export type AnyFn<T> = (...arg: any) => T;
6   -export type PromiseFn<T> = (...arg: any) => Promise<T>;
7   -export type CancelFn = () => void;
8   -export interface DebounceAndThrottleOptions {
9   - // 立即执行
10   - immediate?: boolean;
11   -
12   - // 是否为debounce
13   - debounce?: boolean;
14   - // 只执行一次
15   - once?: boolean;
16   -}
17   -
18   -export type DebounceAndThrottleProcedure<T extends unknown[]> = (...args: T) => unknown;
19   -
20   -export type DebounceAndThrottleProcedureResult<T extends unknown[]> = [
21   - DebounceAndThrottleProcedure<T>,
22   - CancelFn
23   -];
24   -
25   -export type TimeoutResult = [Ref<boolean>, Fn<void>, Fn<void>];
26   -
27   -export type TimeoutFnResult = [Fn<void>, Fn<void>, Ref<boolean>];
28   -
29   -export interface PromiseState {
30   - loading: boolean;
31   - error: Error | null;
32   - result: any;
33   - done: boolean;
34   -}
35   -export type MessageType = 'success' | 'warning' | 'info' | 'error';
36   -
37   -export interface CloseEventHandler {
38   - /**
39   - * Triggers when a message is being closed
40   - *
41   - * @param instance The message component that is being closed
42   - */
43   - (instance: MessageComponent): void;
44   -}
45   -
46   -/** Message Component */
47   -export declare class MessageComponent {
48   - /** Close the Loading instance */
49   - close(): void;
50   -}
51   -
52   -export type MessageMethods = {
53   - [key in MessageType]?: (options: MessageOptions | string) => MessageComponent; // Note that "key in".
54   -};
55   -
56   -/** Options used in Message */
57   -export interface MessageOptions {
58   - title: string;
59   - /** Message text */
60   - message: string | VNode;
61   -
62   - /** Message type */
63   - type?: MessageType;
64   -
65   - /** Custom icon's class, overrides type */
66   - iconClass?: string;
67   -
68   - /** Custom class name for Message */
69   - customClass?: string;
70   -
71   - /** Display duration, millisecond. If set to 0, it will not turn off automatically */
72   - duration?: number;
73   -
74   - /** Whether to show a close button */
75   - showClose?: boolean;
76   -
77   - /** Whether to center the text */
78   - center?: boolean;
79   -
80   - /** Whether message is treated as HTML string */
81   - dangerouslyUseHTMLString?: boolean;
82   -
83   - /** Callback function when closed with the message instance as the parameter */
84   - onClose?: CloseEventHandler;
85   -
86   - /** Set the distance to the top of viewport. Default is 20 px. */
87   - offset?: number;
88   -}
89   -export interface ModalOptionsEx extends Omit<ModalFuncProps, 'iconType'> {
90   - iconType: 'warning' | 'success' | 'error' | 'info';
91   -}
92   -export type ModalOptionsPartial = Partial<ModalOptionsEx> & Pick<ModalOptionsEx, 'content'>;
src/hooks/core/useCounter.ts 0 → 100644
  1 +import { ref } from 'vue';
  2 +
  3 +export function useCounter(initialValue = 0) {
  4 + const count = ref(initialValue);
  5 +
  6 + const inc = (delta = 1) => (count.value += delta);
  7 + const dec = (delta = 1) => (count.value -= delta);
  8 + const get = () => count.value;
  9 + const set = (val: number) => (count.value = val);
  10 + const reset = (val = initialValue) => {
  11 + initialValue = val;
  12 + return set(val);
  13 + };
  14 +
  15 + return { count, inc, dec, get, set, reset };
  16 +}
... ...
src/hooks/core/useDebounce.ts
1   -import type {
2   - DebounceAndThrottleOptions,
3   - DebounceAndThrottleProcedureResult,
4   - DebounceAndThrottleProcedure,
5   -} from './types';
  1 +export interface DebounceAndThrottleOptions {
  2 + // 立即执行
  3 + immediate?: boolean;
  4 +
  5 + // 是否为debounce
  6 + debounce?: boolean;
  7 + // 只执行一次
  8 + once?: boolean;
  9 +}
  10 +export type CancelFn = () => void;
  11 +
  12 +export type DebounceAndThrottleProcedure<T extends unknown[]> = (...args: T) => unknown;
  13 +
  14 +export type DebounceAndThrottleProcedureResult<T extends unknown[]> = [
  15 + DebounceAndThrottleProcedure<T>,
  16 + CancelFn
  17 +];
  18 +
6 19 import {
7 20 // throttle,
8 21 useThrottle,
... ...
src/hooks/core/useThrottle.ts
1   -import type {
2   - DebounceAndThrottleOptions,
3   - DebounceAndThrottleProcedureResult,
4   - DebounceAndThrottleProcedure,
5   -} from './types';
  1 +export interface DebounceAndThrottleOptions {
  2 + // 立即执行
  3 + immediate?: boolean;
  4 +
  5 + // 是否为debounce
  6 + debounce?: boolean;
  7 + // 只执行一次
  8 + once?: boolean;
  9 +}
  10 +export type CancelFn = () => void;
  11 +
  12 +export type DebounceAndThrottleProcedure<T extends unknown[]> = (...args: T) => unknown;
  13 +
  14 +export type DebounceAndThrottleProcedureResult<T extends unknown[]> = [
  15 + DebounceAndThrottleProcedure<T>,
  16 + CancelFn
  17 +];
6 18  
7 19 import { isFunction } from '/@/utils/is';
8 20 export function throttle<T extends unknown[]>(
... ...
src/hooks/core/useTimeout.ts
1   -import type { TimeoutFnResult, Fn } from './types';
2   -
3 1 import { isFunction } from '/@/utils/is';
4   -import { watch } from 'vue';
  2 +import { Ref, watch } from 'vue';
5 3  
6 4 import { useTimeoutRef } from '/@/hooks/core/useTimeoutRef';
7 5  
  6 +type TimeoutFnResult = [Fn<void>, Fn<void>, Ref<boolean>];
  7 +
8 8 export function useTimeout(handle: Fn<any>, wait: number): TimeoutFnResult {
9 9 if (!isFunction(handle)) {
10 10 throw new Error('handle is not Function!');
... ...
src/hooks/core/useTimeoutRef.ts
1   -import type { TimeoutResult } from './types';
2   -
3   -import { ref } from 'vue';
  1 +import { Ref, ref } from 'vue';
4 2 import { tryOnUnmounted } from '/@/utils/helper/vueHelper';
  3 +export type TimeoutResult = [Ref<boolean>, Fn<void>, Fn<void>];
5 4 export function useTimeoutRef(wait: number): TimeoutResult {
6 5 const readyRef = ref(false);
7 6  
... ...
src/hooks/event/types.ts deleted 100644 → 0
1   -export type Fn<T> = () => T;
src/hooks/event/useEventHub.ts
1 1 import { tryOnUnmounted } from '/@/utils/helper/vueHelper';
2   -import {} from 'vue';
3 2 import EventHub from '/@/utils/eventHub';
4 3 const eventHub = new EventHub();
5 4 export function useEventHub(): EventHub {
... ...
src/hooks/event/useIntersectionObserver.ts 0 → 100644
  1 +import { Ref, watchEffect, ref } from 'vue';
  2 +
  3 +interface IntersectionObserverProps {
  4 + target: Ref<Element | null | undefined>;
  5 + root?: Ref<Element | null | undefined>;
  6 + onIntersect: IntersectionObserverCallback;
  7 + rootMargin?: string;
  8 + threshold?: number;
  9 +}
  10 +
  11 +export function useIntersectionObserver({
  12 + target,
  13 + root,
  14 + onIntersect,
  15 + rootMargin = '0px',
  16 + threshold = 0.1,
  17 +}: IntersectionObserverProps) {
  18 + let cleanup = () => {};
  19 + const observer: Ref<Nullable<IntersectionObserver>> = ref(null);
  20 + const stopEffect = watchEffect(() => {
  21 + cleanup();
  22 +
  23 + observer.value = new IntersectionObserver(onIntersect, {
  24 + root: root ? root.value : null,
  25 + rootMargin,
  26 + threshold,
  27 + });
  28 +
  29 + const current = target.value;
  30 +
  31 + current && observer.value.observe(current);
  32 +
  33 + cleanup = () => {
  34 + if (observer.value) {
  35 + observer.value.disconnect();
  36 + target.value && observer.value.unobserve(target.value);
  37 + }
  38 + };
  39 + });
  40 +
  41 + return {
  42 + observer,
  43 + stop: () => {
  44 + cleanup();
  45 + stopEffect();
  46 + },
  47 + };
  48 +}
... ...
src/hooks/event/useNow.ts 0 → 100644
  1 +import { ref } from 'vue';
  2 +import { tryOnUnmounted } from '/@/utils/helper/vueHelper';
  3 +
  4 +function getTimestamp() {
  5 + return +Date.now();
  6 +}
  7 +
  8 +export function useNow() {
  9 + const now = ref(getTimestamp());
  10 + let started = false;
  11 +
  12 + const update = () => {
  13 + requestAnimationFrame(() => {
  14 + now.value = getTimestamp();
  15 + if (started) update();
  16 + });
  17 + };
  18 +
  19 + const start = () => {
  20 + if (!started) {
  21 + started = true;
  22 + update();
  23 + }
  24 + };
  25 +
  26 + const stop = () => {
  27 + started = false;
  28 + };
  29 +
  30 + start();
  31 +
  32 + tryOnUnmounted(stop);
  33 +
  34 + return now;
  35 +}
... ...
src/hooks/event/useRaf.ts
... ... @@ -49,7 +49,6 @@ if (isServer) {
49 49 };
50 50 }
51 51 }
52   -
53 52 export function useRaf() {
54 53 // if (getCurrentInstance()) {
55 54 // onUnmounted(() => {
... ...
src/hooks/web/useMessage.tsx
1   -import type { ModalOptionsEx, ModalOptionsPartial } from '/@/hooks/core/types';
2 1 import type { ModalFunc, ModalFuncProps } from 'ant-design-vue/lib/modal/Modal';
3 2  
4 3 import { Modal, message as Message, notification } from 'ant-design-vue';
... ... @@ -6,6 +5,11 @@ import { InfoCircleFilled, CheckCircleFilled, CloseCircleFilled } from &#39;@ant-des
6 5  
7 6 import { useSetting } from '/@/hooks/core/useSetting';
8 7  
  8 +export interface ModalOptionsEx extends Omit<ModalFuncProps, 'iconType'> {
  9 + iconType: 'warning' | 'success' | 'error' | 'info';
  10 +}
  11 +export type ModalOptionsPartial = Partial<ModalOptionsEx> & Pick<ModalOptionsEx, 'content'>;
  12 +
9 13 interface ConfirmOptions {
10 14 info: ModalFunc;
11 15 success: ModalFunc;
... ...