Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useEventListener 重写 #3405

Closed
luocong2016 opened this issue Dec 12, 2023 · 4 comments
Closed

useEventListener 重写 #3405

luocong2016 opened this issue Dec 12, 2023 · 4 comments

Comments

@luocong2016
Copy link

luocong2016 commented Dec 12, 2023

Subject of the feature

useEventListerner 感觉写的有点绕,改了下感觉比较清晰

Describe your issue here.

Problem

If the feature requests relates to a problem, please describe the problem you are trying to solve here.

Expected behaviour

// onMountedOrActivated.ts
import { nextTick, onMounted, onActivated } from 'vue';

export function onMountedOrActivated(hook: () => any) {
  let mounted: boolean;

  onMounted(() => {
    hook();
    nextTick(() => {
      mounted = true;
    });
  });

  onActivated(() => {
    if (mounted) {
      hook();
    }
  });
}

// useEventListener.ts
import type { Ref } from 'vue';
import { watch, unref, onUnmounted, onDeactivated } from 'vue';
import { onMountedOrActivated } from './onMountedOrActivated';
import { useThrottleFn, useDebounceFn } from '@vueuse/core';

export type RemoveEventFn = () => void;

type TargetRef = EventTarget | Ref<EventTarget | undefined>;

export interface UseEventBaseParams {
  el?: TargetRef;
  options?: boolean | AddEventListenerOptions;
  autoRemove?: boolean;
  isDebounce?: boolean;
  wait?: number;
}

export function useEventListener<K extends keyof DocumentEventMap>(
  opts: UseEventBaseParams & { name: K; listener: (event: DocumentEventMap[K]) => void },
): { removeEvent: RemoveEventFn };
export function useEventListener(
  opts: UseEventBaseParams & { name: string; listener: EventListener },
): { removeEvent: RemoveEventFn } {
  // 这里感觉不能用 el,target 比较合适,window、document 这个就不是 DOM 
  const { el: target = window, name, listener, options, isDebounce = true, wait = 80 } = opts;

  let cleaned = false;
  let attached = false;
  let realHandler: EventListener;

  const add = (target?: TargetRef) => {
    if (cleaned) return;
    const element = unref(target);
    if (element && !attached) {
      const handler = isDebounce ? useDebounceFn(listener, wait) : useThrottleFn(listener, wait);
      realHandler = wait ? handler : listener;
      element.addEventListener(name, realHandler, options);
      attached = true;
    }
  };

  const remove = (target?: TargetRef) => {
    if (cleaned) return;
    const element = unref(target);
    if (element && attached) {
      element.removeEventListener(name, realHandler, options);
      attached = false;
    }
  };

  const stopWatch = watch(
    () => opts,
    (val, oldVal) => {
      remove(oldVal.el);
      add(val.el);
    },
    { deep: true },
  );

  const removeEvent = () => {
    stopWatch?.();
    remove(target);
    cleaned = true;
  };

  onUnmounted(() => remove(target));
  onDeactivated(() => remove(target));
  onMountedOrActivated(() => add(target));

  // 这里可以直接返回个方法,如果要停止。就可以直接调用
  return { removeEvent };
}

Alternatives

What are the alternative solutions? Please describe what else you have considered?

@wangjue666
Copy link
Collaborator

可以提个 PR

@luocong2016
Copy link
Author

luocong2016 commented Dec 13, 2023

我重写了useEventListener, 发现主要是 useECharts 需要重构。感觉这个有要重构下。不理解为什么里面有那么多useTimeoutFn

@luocong2016
Copy link
Author

luocong2016 commented Dec 13, 2023

import type { MaybeRef } from 'vue';
import { watch, unref, onUnmounted, onDeactivated } from 'vue';
import { onMountedOrActivated } from './onMountedOrActivated';
import { useThrottleFn, useDebounceFn, useTimeoutFn, type PromisifyFn } from '@vueuse/core';

type TargetRef = MaybeRef<EventTarget | Element | undefined | null>;

export interface UseEventBaseParams {
  target?: TargetRef;
  options?: boolean | AddEventListenerOptions;
  autoRemove?: boolean;
  isDebounce?: boolean;
  wait?: number;
}

export type UseEventResult = {
  removeEvent: () => void;
  TimeoutFn: (wait: number) => void;
};

export function useEventListener<K extends keyof DocumentEventMap>(
  opts: UseEventBaseParams & { name: K; listener: (event: DocumentEventMap[K]) => void },
): UseEventResult;
export function useEventListener(
  opts: UseEventBaseParams & { name: string; listener: EventListener },
): UseEventResult {
  const { target = window, name, listener, options, isDebounce = true, wait = 80 } = opts;
  let cleaned = false;
  let attached = false;
  let realHandler: EventListener;
  let handler: PromisifyFn<EventListener>;

  const add = (target?: TargetRef) => {
    if (cleaned) return;
    const element = unref(target);
    if (element && !attached) {
      handler = isDebounce ? useDebounceFn(listener, wait) : useThrottleFn(listener, wait);
      realHandler = wait ? handler : listener;
      element.addEventListener(name, realHandler, options);
      attached = true;
    }
  };

  const remove = (target?: TargetRef) => {
    if (cleaned) return;
    const element = unref(target);
    if (element && attached) {
      element.removeEventListener(name, realHandler, options);
      attached = false;
    }
  };

  const stopWatch = watch(
    () => opts,
    (val, oldVal) => {
      remove(oldVal.target);
      add(val.target);
    },
    { deep: true },
  );

  onMountedOrActivated(() => add(target));
  onUnmounted(() => remove(target));
  onDeactivated(() => remove(target));

  const removeEvent = () => {
    stopWatch?.();
    remove(target);
    cleaned = true;
  };

  const TimeoutFn = (wait: number) => realHandler && useTimeoutFn(realHandler, wait);

  return {
    removeEvent,
    TimeoutFn,
  };
}

import type { EChartsOption } from 'echarts';
import type { Ref, MaybeRef } from 'vue';
import { useTimeoutFn } from '@vben/hooks';
import { tryOnUnmounted } from '@vueuse/core';
import { unref, nextTick, watch, computed, ref } from 'vue';
import { useEventListener } from '@/hooks/event/useEventListener';
import { useBreakpoint } from '@/hooks/event/useBreakpoint';
import echarts from '@/utils/lib/echarts';
import { useRootSetting } from '@/hooks/setting/useRootSetting';
import { useMenuSetting } from '@/hooks/setting/useMenuSetting';

export function useECharts(
  target: MaybeRef<HTMLDivElement>,
  theme: 'light' | 'dark' | 'default' = 'default',
) {
  const { getDarkMode: getSysDarkMode } = useRootSetting();
  const { getCollapsed } = useMenuSetting();

  const getDarkMode = computed(() => {
    return theme === 'default' ? getSysDarkMode.value : theme;
  });

  let chartInstance: echarts.ECharts | null = null;

  const cacheOptions: Ref<EChartsOption> = ref({});
  const { removeEvent, TimeoutFn } = useEventListener({
    target: window,
    name: 'resize',
    listener: resize,
    isDebounce: true,
    wait: 200,
  });

  const getOptions = computed(() => {
    if (getDarkMode.value !== 'dark') {
      return cacheOptions.value;
    }
    return {
      backgroundColor: 'transparent',
      ...cacheOptions.value,
    };
  });

  function initCharts(t = theme) {
    const el = unref(target);
    if (!el || !unref(el)) {
      return;
    }
    chartInstance = echarts.init(el, t);
    const { widthRef, screenEnum } = useBreakpoint();
    if (unref(widthRef) <= screenEnum.MD || el.offsetHeight === 0) {
      TimeoutFn(30);
    }
  }

  function setOptions(options: EChartsOption, clear = true) {
    cacheOptions.value = options;
    return new Promise((resolve) => {
      if (unref(target)?.offsetHeight === 0) {
        useTimeoutFn(() => {
          setOptions(unref(getOptions));
          resolve(null);
        }, 30);
      }
      nextTick(() => {
        useTimeoutFn(() => {
          if (!chartInstance) {
            initCharts(getDarkMode.value as 'default');

            if (!chartInstance) return;
          }
          clear && chartInstance?.clear();

          chartInstance?.setOption(unref(getOptions));
          resolve(null);
        }, 30);
      });
    });
  }

  function resize() {
    chartInstance?.resize({
      animation: {
        duration: 300,
        easing: 'quadraticIn',
      },
    });
  }

  watch(
    () => getDarkMode.value,
    (theme) => {
      if (chartInstance) {
        chartInstance.dispose();
        initCharts(theme as 'default');
        setOptions(cacheOptions.value);
      }
    },
  );

  watch(getCollapsed, (_) => {
    TimeoutFn(300);
  });

  tryOnUnmounted(() => {
    if (!chartInstance) return;
    removeEvent();
    chartInstance.dispose();
    chartInstance = null;
  });

  function getInstance(): echarts.ECharts | null {
    if (!chartInstance) {
      initCharts(getDarkMode.value as 'default');
    }
    return chartInstance;
  }

  return {
    setOptions,
    resize,
    echarts,
    getInstance,
  };
}

@anncwb
Copy link
Collaborator

anncwb commented Apr 9, 2024

This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days

@anncwb anncwb added the Stale label Apr 9, 2024
@anncwb anncwb closed this as completed Apr 17, 2024
@github-actions github-actions bot locked and limited conversation to collaborators Aug 7, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants