Przeglądaj źródła

perf: optimize lazy loading components

vben 4 lat temu
rodzic
commit
87fcd0d21e

+ 36 - 54
src/components/Container/src/LazyContainer.vue

@@ -12,12 +12,21 @@
 <script lang="ts">
   import type { PropType } from 'vue';
 
-  import { defineComponent, reactive, onMounted, ref, unref, onUnmounted, toRefs } from 'vue';
+  import {
+    defineComponent,
+    reactive,
+    onMounted,
+    ref,
+    unref,
+    onUnmounted,
+    toRef,
+    toRefs,
+  } from 'vue';
 
   import { Skeleton } from 'ant-design-vue';
   import { useRaf } from '/@/hooks/event/useRaf';
   import { useTimeout } from '/@/hooks/core/useTimeout';
-
+  import { useIntersectionObserver } from '/@/hooks/event/useIntersectionObserver';
   interface State {
     isInit: boolean;
     loading: boolean;
@@ -30,7 +39,7 @@
       // 等待时间,如果指定了时间,不论可见与否,在指定时间之后自动加载
       timeout: {
         type: Number as PropType<number>,
-        default: 8000,
+        default: 0,
         // default: 8000,
       },
       // 组件所在的视口,如果组件是在页面容器内滚动,视口就是该容器
@@ -40,6 +49,7 @@
         >,
         default: () => null,
       },
+
       // 预加载阈值, css单位
       threshold: {
         type: String as PropType<string>,
@@ -51,6 +61,7 @@
         type: String as PropType<'vertical' | 'horizontal'>,
         default: 'vertical',
       },
+
       // 包裹组件的外层容器的标签名
       tag: {
         type: String as PropType<string>,
@@ -62,20 +73,14 @@
         default: 80,
       },
 
-      // // 是否在不可见的时候销毁
-      // autoDestory: {
-      //   type: Boolean as PropType<boolean>,
-      //   default: false,
-      // },
-
       // transition name
       transitionName: {
         type: String as PropType<string>,
         default: 'lazy-container',
       },
     },
-    emits: ['before-init', 'init'],
-    setup(props, { emit, slots }) {
+    emits: ['init'],
+    setup(props, { emit }) {
       const elRef = ref<any>(null);
       const state = reactive<State>({
         isInit: false,
@@ -83,17 +88,10 @@
         intersectionObserverInstance: null,
       });
 
-      immediateInit();
       onMounted(() => {
+        immediateInit();
         initIntersectionObserver();
       });
-      onUnmounted(() => {
-        // Cancel the observation before the component is destroyed
-        if (state.intersectionObserverInstance) {
-          const el = unref(elRef);
-          state.intersectionObserverInstance.unobserve(el.$el);
-        }
-      });
 
       // If there is a set delay time, it will be executed immediately
       function immediateInit() {
@@ -105,9 +103,6 @@
       }
 
       function init() {
-        // At this point, the skeleton component is about to be switched
-        emit('before-init');
-        // At this point you can prepare to load the resources of the lazy-loaded component
         state.loading = true;
 
         requestAnimationFrameFn(() => {
@@ -120,9 +115,7 @@
         // Prevent waiting too long without executing the callback
         // Set the maximum waiting time
         useTimeout(() => {
-          if (state.isInit) {
-            return;
-          }
+          if (state.isInit) return;
           callback();
         }, props.maxWaitingTime || 80);
 
@@ -132,12 +125,10 @@
       }
 
       function initIntersectionObserver() {
-        const { timeout, direction, threshold, viewport } = props;
-        if (timeout) {
-          return;
-        }
+        const { timeout, direction, threshold } = props;
+        if (timeout) return;
         // According to the scrolling direction to construct the viewport margin, used to load in advance
-        let rootMargin;
+        let rootMargin: string = '0px';
         switch (direction) {
           case 'vertical':
             rootMargin = `${threshold} 0px`;
@@ -146,35 +137,26 @@
             rootMargin = `0px ${threshold}`;
             break;
         }
+
         try {
-          // Observe the intersection of the viewport and the component container
-          state.intersectionObserverInstance = new window.IntersectionObserver(
-            intersectionHandler,
-            {
-              rootMargin,
-              root: viewport,
-              threshold: [0, Number.MIN_VALUE, 0.01],
-            }
-          );
-
-          const el = unref(elRef);
-
-          state.intersectionObserverInstance.observe(el.$el);
+          const { stop, observer } = useIntersectionObserver({
+            rootMargin,
+            target: toRef(elRef.value, '$el'),
+            onIntersect: (entries: any[]) => {
+              const isIntersecting = entries[0].isIntersecting || entries[0].intersectionRatio;
+              if (isIntersecting) {
+                init();
+                if (observer) {
+                  stop();
+                }
+              }
+            },
+            root: toRef(props, 'viewport'),
+          });
         } catch (e) {
           init();
         }
       }
-      // Cross-condition change handling function
-      function intersectionHandler(entries: any[]) {
-        const isIntersecting = entries[0].isIntersecting || entries[0].intersectionRatio;
-        if (isIntersecting) {
-          init();
-          if (state.intersectionObserverInstance) {
-            const el = unref(elRef);
-            state.intersectionObserverInstance.unobserve(el.$el);
-          }
-        }
-      }
       return {
         elRef,
         ...toRefs(state),

+ 0 - 92
src/hooks/core/types.ts

@@ -1,92 +0,0 @@
-import type { VNode, Ref } from 'vue';
-import type { ModalFuncProps } from 'ant-design-vue/lib/modal/index';
-
-export type Fn<T> = () => T;
-export type AnyFn<T> = (...arg: any) => T;
-export type PromiseFn<T> = (...arg: any) => Promise<T>;
-export type CancelFn = () => void;
-export interface DebounceAndThrottleOptions {
-  // 立即执行
-  immediate?: boolean;
-
-  // 是否为debounce
-  debounce?: boolean;
-  // 只执行一次
-  once?: boolean;
-}
-
-export type DebounceAndThrottleProcedure<T extends unknown[]> = (...args: T) => unknown;
-
-export type DebounceAndThrottleProcedureResult<T extends unknown[]> = [
-  DebounceAndThrottleProcedure<T>,
-  CancelFn
-];
-
-export type TimeoutResult = [Ref<boolean>, Fn<void>, Fn<void>];
-
-export type TimeoutFnResult = [Fn<void>, Fn<void>, Ref<boolean>];
-
-export interface PromiseState {
-  loading: boolean;
-  error: Error | null;
-  result: any;
-  done: boolean;
-}
-export type MessageType = 'success' | 'warning' | 'info' | 'error';
-
-export interface CloseEventHandler {
-  /**
-   * Triggers when a message is being closed
-   *
-   * @param instance The message component that is being closed
-   */
-  (instance: MessageComponent): void;
-}
-
-/** Message Component */
-export declare class MessageComponent {
-  /** Close the Loading instance */
-  close(): void;
-}
-
-export type MessageMethods = {
-  [key in MessageType]?: (options: MessageOptions | string) => MessageComponent; // Note that "key in".
-};
-
-/** Options used in Message */
-export interface MessageOptions {
-  title: string;
-  /** Message text */
-  message: string | VNode;
-
-  /** Message type */
-  type?: MessageType;
-
-  /** Custom icon's class, overrides type */
-  iconClass?: string;
-
-  /** Custom class name for Message */
-  customClass?: string;
-
-  /** Display duration, millisecond. If set to 0, it will not turn off automatically */
-  duration?: number;
-
-  /** Whether to show a close button */
-  showClose?: boolean;
-
-  /** Whether to center the text */
-  center?: boolean;
-
-  /** Whether message is treated as HTML string */
-  dangerouslyUseHTMLString?: boolean;
-
-  /** Callback function when closed with the message instance as the parameter */
-  onClose?: CloseEventHandler;
-
-  /** Set the distance to the top of viewport. Default is 20 px. */
-  offset?: number;
-}
-export interface ModalOptionsEx extends Omit<ModalFuncProps, 'iconType'> {
-  iconType: 'warning' | 'success' | 'error' | 'info';
-}
-export type ModalOptionsPartial = Partial<ModalOptionsEx> & Pick<ModalOptionsEx, 'content'>;

+ 16 - 0
src/hooks/core/useCounter.ts

@@ -0,0 +1,16 @@
+import { ref } from 'vue';
+
+export function useCounter(initialValue = 0) {
+  const count = ref(initialValue);
+
+  const inc = (delta = 1) => (count.value += delta);
+  const dec = (delta = 1) => (count.value -= delta);
+  const get = () => count.value;
+  const set = (val: number) => (count.value = val);
+  const reset = (val = initialValue) => {
+    initialValue = val;
+    return set(val);
+  };
+
+  return { count, inc, dec, get, set, reset };
+}

+ 18 - 5
src/hooks/core/useDebounce.ts

@@ -1,8 +1,21 @@
-import type {
-  DebounceAndThrottleOptions,
-  DebounceAndThrottleProcedureResult,
-  DebounceAndThrottleProcedure,
-} from './types';
+export interface DebounceAndThrottleOptions {
+  // 立即执行
+  immediate?: boolean;
+
+  // 是否为debounce
+  debounce?: boolean;
+  // 只执行一次
+  once?: boolean;
+}
+export type CancelFn = () => void;
+
+export type DebounceAndThrottleProcedure<T extends unknown[]> = (...args: T) => unknown;
+
+export type DebounceAndThrottleProcedureResult<T extends unknown[]> = [
+  DebounceAndThrottleProcedure<T>,
+  CancelFn
+];
+
 import {
   // throttle,
   useThrottle,

+ 17 - 5
src/hooks/core/useThrottle.ts

@@ -1,8 +1,20 @@
-import type {
-  DebounceAndThrottleOptions,
-  DebounceAndThrottleProcedureResult,
-  DebounceAndThrottleProcedure,
-} from './types';
+export interface DebounceAndThrottleOptions {
+  // 立即执行
+  immediate?: boolean;
+
+  // 是否为debounce
+  debounce?: boolean;
+  // 只执行一次
+  once?: boolean;
+}
+export type CancelFn = () => void;
+
+export type DebounceAndThrottleProcedure<T extends unknown[]> = (...args: T) => unknown;
+
+export type DebounceAndThrottleProcedureResult<T extends unknown[]> = [
+  DebounceAndThrottleProcedure<T>,
+  CancelFn
+];
 
 import { isFunction } from '/@/utils/is';
 export function throttle<T extends unknown[]>(

+ 3 - 3
src/hooks/core/useTimeout.ts

@@ -1,10 +1,10 @@
-import type { TimeoutFnResult, Fn } from './types';
-
 import { isFunction } from '/@/utils/is';
-import { watch } from 'vue';
+import { Ref, watch } from 'vue';
 
 import { useTimeoutRef } from '/@/hooks/core/useTimeoutRef';
 
+type TimeoutFnResult = [Fn<void>, Fn<void>, Ref<boolean>];
+
 export function useTimeout(handle: Fn<any>, wait: number): TimeoutFnResult {
   if (!isFunction(handle)) {
     throw new Error('handle is not Function!');

+ 2 - 3
src/hooks/core/useTimeoutRef.ts

@@ -1,7 +1,6 @@
-import type { TimeoutResult } from './types';
-
-import { ref } from 'vue';
+import { Ref, ref } from 'vue';
 import { tryOnUnmounted } from '/@/utils/helper/vueHelper';
+export type TimeoutResult = [Ref<boolean>, Fn<void>, Fn<void>];
 export function useTimeoutRef(wait: number): TimeoutResult {
   const readyRef = ref(false);
 

+ 0 - 1
src/hooks/event/types.ts

@@ -1 +0,0 @@
-export type Fn<T> = () => T;

+ 0 - 1
src/hooks/event/useEventHub.ts

@@ -1,5 +1,4 @@
 import { tryOnUnmounted } from '/@/utils/helper/vueHelper';
-import {} from 'vue';
 import EventHub from '/@/utils/eventHub';
 const eventHub = new EventHub();
 export function useEventHub(): EventHub {

+ 48 - 0
src/hooks/event/useIntersectionObserver.ts

@@ -0,0 +1,48 @@
+import { Ref, watchEffect, ref } from 'vue';
+
+interface IntersectionObserverProps {
+  target: Ref<Element | null | undefined>;
+  root?: Ref<Element | null | undefined>;
+  onIntersect: IntersectionObserverCallback;
+  rootMargin?: string;
+  threshold?: number;
+}
+
+export function useIntersectionObserver({
+  target,
+  root,
+  onIntersect,
+  rootMargin = '0px',
+  threshold = 0.1,
+}: IntersectionObserverProps) {
+  let cleanup = () => {};
+  const observer: Ref<Nullable<IntersectionObserver>> = ref(null);
+  const stopEffect = watchEffect(() => {
+    cleanup();
+
+    observer.value = new IntersectionObserver(onIntersect, {
+      root: root ? root.value : null,
+      rootMargin,
+      threshold,
+    });
+
+    const current = target.value;
+
+    current && observer.value.observe(current);
+
+    cleanup = () => {
+      if (observer.value) {
+        observer.value.disconnect();
+        target.value && observer.value.unobserve(target.value);
+      }
+    };
+  });
+
+  return {
+    observer,
+    stop: () => {
+      cleanup();
+      stopEffect();
+    },
+  };
+}

+ 35 - 0
src/hooks/event/useNow.ts

@@ -0,0 +1,35 @@
+import { ref } from 'vue';
+import { tryOnUnmounted } from '/@/utils/helper/vueHelper';
+
+function getTimestamp() {
+  return +Date.now();
+}
+
+export function useNow() {
+  const now = ref(getTimestamp());
+  let started = false;
+
+  const update = () => {
+    requestAnimationFrame(() => {
+      now.value = getTimestamp();
+      if (started) update();
+    });
+  };
+
+  const start = () => {
+    if (!started) {
+      started = true;
+      update();
+    }
+  };
+
+  const stop = () => {
+    started = false;
+  };
+
+  start();
+
+  tryOnUnmounted(stop);
+
+  return now;
+}

+ 0 - 1
src/hooks/event/useRaf.ts

@@ -49,7 +49,6 @@ if (isServer) {
     };
   }
 }
-
 export function useRaf() {
   // if (getCurrentInstance()) {
   //   onUnmounted(() => {

+ 5 - 1
src/hooks/web/useMessage.tsx

@@ -1,4 +1,3 @@
-import type { ModalOptionsEx, ModalOptionsPartial } from '/@/hooks/core/types';
 import type { ModalFunc, ModalFuncProps } from 'ant-design-vue/lib/modal/Modal';
 
 import { Modal, message as Message, notification } from 'ant-design-vue';
@@ -6,6 +5,11 @@ import { InfoCircleFilled, CheckCircleFilled, CloseCircleFilled } from '@ant-des
 
 import { useSetting } from '/@/hooks/core/useSetting';
 
+export interface ModalOptionsEx extends Omit<ModalFuncProps, 'iconType'> {
+  iconType: 'warning' | 'success' | 'error' | 'info';
+}
+export type ModalOptionsPartial = Partial<ModalOptionsEx> & Pick<ModalOptionsEx, 'content'>;
+
 interface ConfirmOptions {
   info: ModalFunc;
   success: ModalFunc;