提交 87fcd0d2 编写于 作者: V vben

perf: optimize lazy loading components

上级 35d2bfc5
...@@ -12,12 +12,21 @@ ...@@ -12,12 +12,21 @@
<script lang="ts"> <script lang="ts">
import type { PropType } from 'vue'; 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 { Skeleton } from 'ant-design-vue';
import { useRaf } from '/@/hooks/event/useRaf'; import { useRaf } from '/@/hooks/event/useRaf';
import { useTimeout } from '/@/hooks/core/useTimeout'; import { useTimeout } from '/@/hooks/core/useTimeout';
import { useIntersectionObserver } from '/@/hooks/event/useIntersectionObserver';
interface State { interface State {
isInit: boolean; isInit: boolean;
loading: boolean; loading: boolean;
...@@ -30,7 +39,7 @@ ...@@ -30,7 +39,7 @@
// 等待时间,如果指定了时间,不论可见与否,在指定时间之后自动加载 // 等待时间,如果指定了时间,不论可见与否,在指定时间之后自动加载
timeout: { timeout: {
type: Number as PropType<number>, type: Number as PropType<number>,
default: 8000, default: 0,
// default: 8000, // default: 8000,
}, },
// 组件所在的视口,如果组件是在页面容器内滚动,视口就是该容器 // 组件所在的视口,如果组件是在页面容器内滚动,视口就是该容器
...@@ -40,6 +49,7 @@ ...@@ -40,6 +49,7 @@
>, >,
default: () => null, default: () => null,
}, },
// 预加载阈值, css单位 // 预加载阈值, css单位
threshold: { threshold: {
type: String as PropType<string>, type: String as PropType<string>,
...@@ -51,6 +61,7 @@ ...@@ -51,6 +61,7 @@
type: String as PropType<'vertical' | 'horizontal'>, type: String as PropType<'vertical' | 'horizontal'>,
default: 'vertical', default: 'vertical',
}, },
// 包裹组件的外层容器的标签名 // 包裹组件的外层容器的标签名
tag: { tag: {
type: String as PropType<string>, type: String as PropType<string>,
...@@ -62,20 +73,14 @@ ...@@ -62,20 +73,14 @@
default: 80, default: 80,
}, },
// // 是否在不可见的时候销毁
// autoDestory: {
// type: Boolean as PropType<boolean>,
// default: false,
// },
// transition name // transition name
transitionName: { transitionName: {
type: String as PropType<string>, type: String as PropType<string>,
default: 'lazy-container', default: 'lazy-container',
}, },
}, },
emits: ['before-init', 'init'], emits: ['init'],
setup(props, { emit, slots }) { setup(props, { emit }) {
const elRef = ref<any>(null); const elRef = ref<any>(null);
const state = reactive<State>({ const state = reactive<State>({
isInit: false, isInit: false,
...@@ -83,17 +88,10 @@ ...@@ -83,17 +88,10 @@
intersectionObserverInstance: null, intersectionObserverInstance: null,
}); });
immediateInit();
onMounted(() => { onMounted(() => {
immediateInit();
initIntersectionObserver(); 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 // If there is a set delay time, it will be executed immediately
function immediateInit() { function immediateInit() {
...@@ -105,9 +103,6 @@ ...@@ -105,9 +103,6 @@
} }
function init() { 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; state.loading = true;
requestAnimationFrameFn(() => { requestAnimationFrameFn(() => {
...@@ -120,9 +115,7 @@ ...@@ -120,9 +115,7 @@
// Prevent waiting too long without executing the callback // Prevent waiting too long without executing the callback
// Set the maximum waiting time // Set the maximum waiting time
useTimeout(() => { useTimeout(() => {
if (state.isInit) { if (state.isInit) return;
return;
}
callback(); callback();
}, props.maxWaitingTime || 80); }, props.maxWaitingTime || 80);
...@@ -132,12 +125,10 @@ ...@@ -132,12 +125,10 @@
} }
function initIntersectionObserver() { function initIntersectionObserver() {
const { timeout, direction, threshold, viewport } = props; const { timeout, direction, threshold } = props;
if (timeout) { if (timeout) return;
return;
}
// According to the scrolling direction to construct the viewport margin, used to load in advance // According to the scrolling direction to construct the viewport margin, used to load in advance
let rootMargin; let rootMargin: string = '0px';
switch (direction) { switch (direction) {
case 'vertical': case 'vertical':
rootMargin = `${threshold} 0px`; rootMargin = `${threshold} 0px`;
...@@ -146,35 +137,26 @@ ...@@ -146,35 +137,26 @@
rootMargin = `0px ${threshold}`; rootMargin = `0px ${threshold}`;
break; break;
} }
try { try {
// Observe the intersection of the viewport and the component container const { stop, observer } = useIntersectionObserver({
state.intersectionObserverInstance = new window.IntersectionObserver( rootMargin,
intersectionHandler, target: toRef(elRef.value, '$el'),
{ onIntersect: (entries: any[]) => {
rootMargin, const isIntersecting = entries[0].isIntersecting || entries[0].intersectionRatio;
root: viewport, if (isIntersecting) {
threshold: [0, Number.MIN_VALUE, 0.01], init();
} if (observer) {
); stop();
}
const el = unref(elRef); }
},
state.intersectionObserverInstance.observe(el.$el); root: toRef(props, 'viewport'),
});
} catch (e) { } catch (e) {
init(); 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 { return {
elRef, elRef,
...toRefs(state), ...toRefs(state),
......
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'>;
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 };
}
import type { export interface DebounceAndThrottleOptions {
DebounceAndThrottleOptions, // 立即执行
DebounceAndThrottleProcedureResult, immediate?: boolean;
DebounceAndThrottleProcedure,
} from './types'; // 是否为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 { import {
// throttle, // throttle,
useThrottle, useThrottle,
......
import type { export interface DebounceAndThrottleOptions {
DebounceAndThrottleOptions, // 立即执行
DebounceAndThrottleProcedureResult, immediate?: boolean;
DebounceAndThrottleProcedure,
} from './types'; // 是否为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'; import { isFunction } from '/@/utils/is';
export function throttle<T extends unknown[]>( export function throttle<T extends unknown[]>(
......
import type { TimeoutFnResult, Fn } from './types';
import { isFunction } from '/@/utils/is'; import { isFunction } from '/@/utils/is';
import { watch } from 'vue'; import { Ref, watch } from 'vue';
import { useTimeoutRef } from '/@/hooks/core/useTimeoutRef'; import { useTimeoutRef } from '/@/hooks/core/useTimeoutRef';
type TimeoutFnResult = [Fn<void>, Fn<void>, Ref<boolean>];
export function useTimeout(handle: Fn<any>, wait: number): TimeoutFnResult { export function useTimeout(handle: Fn<any>, wait: number): TimeoutFnResult {
if (!isFunction(handle)) { if (!isFunction(handle)) {
throw new Error('handle is not Function!'); throw new Error('handle is not Function!');
......
import type { TimeoutResult } from './types'; import { Ref, ref } from 'vue';
import { ref } from 'vue';
import { tryOnUnmounted } from '/@/utils/helper/vueHelper'; import { tryOnUnmounted } from '/@/utils/helper/vueHelper';
export type TimeoutResult = [Ref<boolean>, Fn<void>, Fn<void>];
export function useTimeoutRef(wait: number): TimeoutResult { export function useTimeoutRef(wait: number): TimeoutResult {
const readyRef = ref(false); const readyRef = ref(false);
......
export type Fn<T> = () => T;
import { tryOnUnmounted } from '/@/utils/helper/vueHelper'; import { tryOnUnmounted } from '/@/utils/helper/vueHelper';
import {} from 'vue';
import EventHub from '/@/utils/eventHub'; import EventHub from '/@/utils/eventHub';
const eventHub = new EventHub(); const eventHub = new EventHub();
export function useEventHub(): EventHub { export function useEventHub(): EventHub {
......
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();
},
};
}
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;
}
...@@ -49,7 +49,6 @@ if (isServer) { ...@@ -49,7 +49,6 @@ if (isServer) {
}; };
} }
} }
export function useRaf() { export function useRaf() {
// if (getCurrentInstance()) { // if (getCurrentInstance()) {
// onUnmounted(() => { // onUnmounted(() => {
......
import type { ModalOptionsEx, ModalOptionsPartial } from '/@/hooks/core/types';
import type { ModalFunc, ModalFuncProps } from 'ant-design-vue/lib/modal/Modal'; import type { ModalFunc, ModalFuncProps } from 'ant-design-vue/lib/modal/Modal';
import { Modal, message as Message, notification } from 'ant-design-vue'; import { Modal, message as Message, notification } from 'ant-design-vue';
...@@ -6,6 +5,11 @@ import { InfoCircleFilled, CheckCircleFilled, CloseCircleFilled } from '@ant-des ...@@ -6,6 +5,11 @@ import { InfoCircleFilled, CheckCircleFilled, CloseCircleFilled } from '@ant-des
import { useSetting } from '/@/hooks/core/useSetting'; 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 { interface ConfirmOptions {
info: ModalFunc; info: ModalFunc;
success: ModalFunc; success: ModalFunc;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册