用Vue 相关模拟React Hooks 【setup版】

197 阅读4分钟

前言

此篇讨论Vue2.7+,React16.8+

内部使用的Vue去模拟React的hooks,只是在调用时一致,但在执行时机还是有本质区别。React的hooks调用时机相当于Vue的render函数执行时机,Vue模拟的hooks还是在setup中执行,故还是会有些差别,如useState,Vue模拟版本的state还是需要保留响应式。

React这一系列hooks本质还是每次渲染重新执行会导致相关函数重新执行,引用数据类型堆地址变化/重新定义导致即使子组件memo还是会重新执行一系列问题的优化

故此使用Vue模拟,不少情况可能到像是开倒车,但对理解/对比两个框架有一些帮助

React部分常用的Hooks

useState

React useState类型

/**
 * Returns a stateful value, and a function to update it.
 *
 * @version 16.8.0
 * @see https://react.dev/reference/react/useState
 */
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];

Vue模拟useState

import { ref, type UnwrapRef } from 'vue';
export const useState = <T = unknown>(
  initValue: T | (() => T),
): [Ref<UnwrapRef<T>>, (value: T) => void] => {
  const state = ref(
    typeof initValue === "function" ? (initValue as () => T)() : initValue,
  );

  const setState = (value: T) => {
    state.value = value as UnwrapRef<T>;
  };

  return [state, setState];
};

如前言所言 以上代码可以应用于实际生产,如果纯粹是为了对比完全模拟React useState 则可以改成如下:

// 返回类型
[T, (value: T) => void]

// 返回值
return [state.value, setState]

保留了state响应式,但需要state.value去访问,

首先要明确,React函数式组件的调用时机可以认为是Vue的render函数的调用,但本次

自我评价 Vue模拟版本的实用性:0

如果增加 beforeChangeSync, afterChangeSync 倒实用些 即增加第二个参数为配置项 类型如下:

interface useStateOptions{
    beforeChangeSync?: () => void;
    afterChangeSync?: () => void;
}

setState内部实现更改为如下

const setState = (value: T) => {
    beforeChangeSync?.(value);
    
    state.value = value as UnwrapRef<T>;
    
    afterChangeSync?.(value);
}

自我评价 这个Vue模拟版本的变种实用性:3

useEffect

React useEffect类型

/**
 * Accepts a function that contains imperative, possibly effectful code.
 *
 * @param effect Imperative function that can return a cleanup function
 * @param deps If present, effect will only activate if the values in the list change.
 *
 * @version 16.8.0
 * @see https://react.dev/reference/react/useEffect
 */
function useEffect(effect: EffectCallback, deps?: DependencyList): void;

Vue 模拟useEffect

import { 
  watch,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
} from 'vue';
export const useEffect = (
  callback: () => undefined | (() => void),
  deps?: any[],
) => {
  let cleanup: undefined | (() => void);

  if (Array.isArray(deps)) {
    // 只在依赖变化后渲染完成后执行 执行前清除 卸载前清除
    if (deps.length) {
      onBeforeUnmount(() => {
        cleanup && cleanup();
      });

      watch(
        deps,
        () => {
          cleanup && cleanup();
          cleanup = callback();
        },
        {
          immediate: true,
          flush: "post",
        },
      );
    } else {
      // 只在首次渲染完成后执行 卸载前清除
      onMounted(() => {
        cleanup = callback();
      });
      onBeforeUnmount(() => {
        cleanup && cleanup();
      });
    }
  } else {
    // 每次渲染完成后执行 执行前清除 卸载前清除
    onMounted(() => {
      cleanup = callback();
    });
    onBeforeUpdate(() => {
      cleanup && cleanup();
    });
    onUpdated(() => {
      cleanup = callback();
    });
    onBeforeUnmount(() => {
      cleanup && cleanup();
    });
  }
};

算是特定情况的语法组合

自我评价 Vue模拟版本的实用性:3

useCallback

React useCallBack类型

/**
 * `useCallback` will return a memoized version of the callback that only changes if one of the `inputs`
 * has changed.
 *
 * @version 16.8.0
 * @see https://react.dev/reference/react/useCallback
 */
// A specific function type would not trigger implicit any.
// See https://github.com/DefinitelyTyped/DefinitelyTyped/issues/52873#issuecomment-845806435 for a comparison between `Function` and more specific types.
// tslint:disable-next-line ban-types
function useCallback<T extends Function>(callback: T, deps: DependencyList): T;

Vue 模拟useCallBack

import { watch } from 'vue';
export const useCallback = (callback: (...args: any[]) => any, deps: any[]) => {
  let handler: (...args: any[]) => any;
  if (deps.length) {
    watch(
      deps,
      () => {

        handler = (...args: any[]) => callback(...args);
      },
      {
        immediate: true,
      },
    );
  } else {
    handler = callback
  }
  return (...args: any[]) => {
    handler(...args);
  };
};

这种hooks是React为了使callback的堆地址不频繁变化的优化,这个Hooks对Vue来说完全没有意义

完全是吃饱了的行为,基本没啥意义

自我评价 这个Vue模拟版本的实用性:-1

不过我们可以加点糖,做其他的事,当参数不变化的时候我们缓存结果,即每次都返回相同值

import { watch } from 'vue';
import _memoize from 'lodash/memoize';
export const useMemoCallback = (callback: (...args: any[]) => any, deps: any[]) => {
  let handler: (...args: any[]) => any;
  watch(
    deps,
    () => {
      handler = _memoize(callback);
    },
    {
      immediate: true,
    },
  );
  return (...args: any[]) => {
    handler(...args);
  };
};

此处除了使用Vue的api 还使用了lodash的缓存函数

自我评价 这个Vue模拟版本的变种实用性:1

useMemo

React useMemo类型

/**
 * `useMemo` will only recompute the memoized value when one of the `deps` has changed.
 *
 * @version 16.8.0
 * @see https://react.dev/reference/react/useMemo
 */
// allow undefined, but don't make it optional as that is very likely a mistake
function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T;

Vue 模拟useMemo

import { ref, type UnwrapRef, watch, onBeforeMount, onBeforeUpdate } from 'vue';

export const useMemo = <T>(callback: () => T, deps?: any[]) => {
  const state = ref<T>(undefined as T);
  if (Array.isArray(deps) && deps.length) {
    watch(
      deps,
      () => {
        state.value = callback() as UnwrapRef<T>;
      },
      {
        immediate: true,
      },
    );
  } else {
    onBeforeMount(() => {
      state.value = callback() as UnwrapRef<T>;
    });
    onBeforeUpdate(() => {
      state.value = callback() as UnwrapRef<T>;
    });
  }

  return state;
};

与computed唯一的差别就是一个自动收集依赖,一个指定依赖

自我评价 这个Vue模拟版本的变种实用性:0

useRef

React useRef类型

/**
 * `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument
 * (`initialValue`). The returned object will persist for the full lifetime of the component.
 *
 * Note that `useRef()` is useful for more than the `ref` attribute. It’s handy for keeping any mutable
 * value around similar to how you’d use instance fields in classes.
 *
 * @version 16.8.0
 * @see https://react.dev/reference/react/useRef
 */
function useRef<T>(initialValue: T): MutableRefObject<T>;

Vue 模拟useRef

export const useRef = <T = any>(initValue: T): { current: T } => {
  const raw = ref(initValue) as Ref<UnwrapRef<T>>;

  const state = reactive({
    current: initValue,
  });

  watch(
    raw,
    (r) => {
      state.current = r as UnwrapRef<T>;
    },
    { immediate: true },
  );

  return state as { current: T };
};

自我评价 这个Vue模拟版本的实用性:0