useRequest 改造成Vue3版本

663 阅读2分钟

ahooks中有许多优秀的hook实践 , 但是它是面向react的用户, 由于作者目前任职的公司是用vue3, 所以就想把useRequest改造成vue3版本来用, 毕竟不想造轮子, 那么它有什么功能呢, 基本涵盖了前端的大多数场景了(useRequest官方文档)

截屏2023-03-12 21.03.03.png

以下是改造过程

首先, react因为流程设计的问题啊, 需要手动监听依赖变化. vue3在这方面呢, 有它的优势性. 所以替换的关键只是在依赖监听, 触发更新这步, 为了迁移效率, 我们遵循个最小变动的原则. 就是不改变原有流程.

以下是改造后的目录结构, 对外只暴露useRequest函数

因为目前只想迁移useRequest, 所以把这块用到的其他hooks都放在它的目录下 截屏2023-03-12 20.40.55.png

useRequestImplement.ts

// 旧
import useCreation from '../../useCreation';
import useLatest from '../../useLatest';
import useMemoizedFn from '../../useMemoizedFn';
import useMount from '../../useMount';
import useUnmount from '../../useUnmount';
import useUpdate from '../../useUpdate';

// 新
import useCreation from '../hooks/useCreation';
import useMemoizedFn from '../hooks/useMemoizedFn';
import {
  onMounted,
  onBeforeUnmount,
  computed,
} from 'vue';

// 旧
  const serviceRef = useLatest(service);

  const update = useUpdate();

// 新
  const serviceRef = service;
  const update = () => {};
// useMount 用 onMounted 替换
// useUnmount 用 onBeforeUnmount 替换

// 旧 关键
  return {
    loading: fetchInstance.state.loading,
    data: fetchInstance.state.data,
    error: fetchInstance.state.error,
    params: fetchInstance.state.params || [],
    cancel: useMemoizedFn(fetchInstance.cancel.bind(fetchInstance)),
    refresh: useMemoizedFn(fetchInstance.refresh.bind(fetchInstance)),
    refreshAsync: useMemoizedFn(fetchInstance.refreshAsync.bind(fetchInstance)),
    run: useMemoizedFn(fetchInstance.run.bind(fetchInstance)),
    runAsync: useMemoizedFn(fetchInstance.runAsync.bind(fetchInstance)),
    mutate: useMemoizedFn(fetchInstance.mutate.bind(fetchInstance)),
  } as Result<TData, TParams>;
  
  // 新
const exportRef = computed(() => ({
    loading: fetchInstance.state.loading,
    data: fetchInstance.state.data,
    error: fetchInstance.state.error,
    params: fetchInstance.state.params || [],
    cancel: useMemoizedFn(fetchInstance.cancel.bind(fetchInstance)),
    refresh: useMemoizedFn(fetchInstance.refresh.bind(fetchInstance)),
    refreshAsync: useMemoizedFn(fetchInstance.refreshAsync.bind(fetchInstance)),
    run: useMemoizedFn(fetchInstance.run.bind(fetchInstance)),
    runAsync: useMemoizedFn(fetchInstance.runAsync.bind(fetchInstance)),
    mutate: useMemoizedFn(fetchInstance.mutate.bind(fetchInstance)),
  }));
  return exportRef;

最关键的一步, 就是把对外暴露的对象变为计算属性. 因为react的更新是整个组件重新渲染的. vue3不是这个机制.

useCreation

// 旧
import type { DependencyList } from 'react';
import { useRef } from 'react';
import depsAreSame from '../utils/depsAreSame';

export default function useCreation<T>(factory: () => T, deps: DependencyList) {
  const { current } = useRef({
    deps,
    obj: undefined as undefined | T,
    initialized: false,
  });
  if (current.initialized === false || !depsAreSame(current.deps, deps)) {
    current.deps = deps;
    current.obj = factory();
    current.initialized = true;
  }
  return current.obj as T;
}

// 新
import { ref } from 'vue';
import depsAreSame from '../../utils/depsAreSame';

export default function useCreation<T>(factory: () => T, deps: []) {
  const { value } = ref({
    deps,
    obj: undefined as undefined | T,
    initialized: false,
  });
  if (value.initialized === false || !depsAreSame(value.deps, deps)) {
    value.deps = deps;
    value.obj = factory();
    value.initialized = true;
  }
  return value.obj as T;
}

useMemoizedFn

// 旧
import { useMemo, useRef } from 'react';
import { isFunction } from '../utils';
import isDev from '../utils/isDev';

type noop = (this: any, ...args: any[]) => any;

type PickFunction<T extends noop> = (
  this: ThisParameterType<T>,
  ...args: Parameters<T>
) => ReturnType<T>;

function useMemoizedFn<T extends noop>(fn: T) {
  if (isDev) {
    if (!isFunction(fn)) {
      console.error(`useMemoizedFn expected parameter is a function, got ${typeof fn}`);
    }
  }

  const fnRef = useRef<T>(fn);

  // why not write `fnRef.current = fn`?
  // https://github.com/alibaba/hooks/issues/728
  fnRef.current = useMemo(() => fn, [fn]);

  const memoizedFn = useRef<PickFunction<T>>();
  if (!memoizedFn.current) {
    memoizedFn.current = function (this, ...args) {
      return fnRef.current.apply(this, args);
    };
  }

  return memoizedFn.current as T;
}

export default useMemoizedFn;

// 新
// import { isFunction } from '../utils';
// import isDev from '../utils/isDev';
import { ref, computed } from 'vue';

type noop = (this: any, ...args: any[]) => any;

type PickFunction<T extends noop> = (
  this: ThisParameterType<T>,
  ...args: Parameters<T>
) => ReturnType<T>;

function useMemoizedFn<T extends noop>(fn: T) {
  // if (isDev) {
  //   if (!isFunction(fn)) {
  //     console.error(`useMemoizedFn expected parameter is a function, got ${typeof fn}`);
  //   }
  // }

  const fnRef = ref<T>(fn);

  // why not write `fnRef.current = fn`?
  // https://github.com/alibaba/hooks/issues/728
  // fnRef.value = computed(() => fn);

  const memoizedFn = ref<PickFunction<T>>();
  if (!memoizedFn.value) {
    memoizedFn.value = function (this, ...args) {
      return fnRef.value.apply(this, args);
    };
  }

  return memoizedFn.value as T;
}

export default useMemoizedFn;

到这阶段, 流程已经通了

剩下的就是把插件一个个接入就行了, 原理同上面的改造, 为了方便大家学习交流,已经把改造后的代码, 做了个demo 项目地址

总结

轮子能不造就不造吧, 吃现成的优秀实践多好. 改造一下, 做做语法迁移就好了