前言
此篇讨论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