注册
web

突发奇想(吃饱了撑的) vue3能实现react中的hooks吗?

前言


在前端领域,Vue 和 React 是两个备受欢迎的 JavaScript 框架,它们都为开发者提供了灵活而强大的工具。React 引入了 Hooks 的概念,使得函数组件可以拥有状态和其他 React 特性,而不再需要使用类组件。那么,对于那些突发奇想、吃饱了撑的时候,我们是否可以在 Vue 3 中实现类似 React Hooks 的功能呢?


首先,我们需要了解 React Hooks 的核心思想。React Hooks 允许函数组件拥有状态,副作用和其他 React 特性,而无需使用类组件。这是通过使用 useStateuseEffect 等特殊的函数来实现的。


Vue 3 也引入了 Composition API,它在一定程度上类似于 React Hooks。Composition API 允许我们在函数组件中组织和重用逻辑。虽然它不是完全相同的实现,但能够达到类似的效果。


useState


React 中的 useState:


useState 是 React 中的一个 Hook,用于在函数组件中引入状态。通过 useState,我们可以在函数组件中保存和更新状态,而不必使用类组件。


基本语法如下:


import React, { useState } from 'react';

function ExampleComponent() {
// 使用 useState 定义状态变量 count 和更新函数 setCount,并初始化为 0
const [count, setCount] = useState(0);

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
加一
</button>
</div>

);
}

下面是一个使用 vue3实现类似于 useState 的例子:


import { ref, UnwrapRef } from "vue";

type UpdateFunction<T> = (nextState: UnwrapRef<T>) => UnwrapRef<T>;
function isUpdateFc<T>(
nextState: UnwrapRef<T> | UpdateFunction<T>
): nextState is UpdateFunction<T> {
return typeof nextState === "function";
}

export default function useState<T>(initialState: T) {
const state = ref<T>(initialState);
const useState = (nextState: UnwrapRef<T> | UpdateFunction<T>) => {
// 检测传入的是不是函数,如果是函数就把state传给函数,把函数执行返回值赋给重新state
if (isUpdateFc(nextState)) {
state.value = nextState(state.value);
} else {
state.value = nextState;
}
};
return [state, useState] as const;
}

<script setup lang="ts">
import { useState } from "./hooks";
const [count, setCount] = useState(0);

const handerCount = () => {
setCount(n => n + 1)
}
</script>
<template>
<div>
<button>{{ count }}</button>
<button @click="() => handerCount()">+</button>
</div>
</template>


useEffect


React 中的 useEffect:


useEffect 是 React 中的一个重要 Hook,用于处理副作用操作,比如数据获取、订阅、手动操作 DOM 等。它在函数组件渲染完成后执行,可以用于管理组件的生命周期。


基本语法如下:


import React, { useState, useEffect } from 'react';

function ExampleComponent() {
const [data, setData] = useState(null);

useEffect(() => {
// 在组件渲染完成后执行的副作用操作
fetchData(); // 例如,发起数据请求
}, []); // 第二个参数是依赖数组,为空数组表示只在组件挂载和卸载时执行

return (
<div>
{/* 组件渲染的内容 */}
</div>

);
}

在 Vue 3 中使用 onMounted, onUpdated, onUnmounted,watch 实现类似功能:


在 Vue 3 中,可以使用一系列的生命周期钩子和 watch 函数来实现与 useEffect 类似的效果。



  1. onMounted: 在组件挂载后执行。
  2. onUpdated: 在组件更新后执行。
  3. onUnmounted: 在组件卸载前执行。
  4. watch: 监听特定数据的变化。

下面是一个使用 vue3实现类似于 useEffect 的例子:


import { ref, onMounted, watch, onUnmounted, onUpdated } from "vue";

type EffectCleanup = void | (() => void);
export default function useEffect(
setup: () => EffectCleanup,
dependencies?: readonly unknown[]
): void {
const cleanupRef = ref<EffectCleanup | null>(null);
const runEffect = () => {
// 判断下一次执行副作用前还有没有清理函数没有执行
if (cleanupRef.value) {
cleanupRef.value();
}
// 执行副作用,并赋值清理函数
cleanupRef.value = setup();
};
// 组件挂载的时候执行一次副作用
onMounted(runEffect);
// 判断有没有传依赖项,有的话就watch监听
if (dependencies && dependencies.length > 0) {
watch(dependencies, runEffect);
} else if(dependencies === undefined) {
// 没有传依赖项就组件每次渲染都要执行副作用
onUpdated(runEffect)
}
// 组件销毁的使用如果有清理函数就执行清理函数
onUnmounted(() => {
if (cleanupRef.value) {
cleanupRef.value();
}
});
}

<script setup lang="ts">
import { useEffect } from "./hooks";
useEffect(() => {
console.log('执行了副作用');
return () => {
console.log('清理副作用的操作');
}
}, [count])
</script>

useReducer


React 中的 useReducer:


useReducer 是 React 中的另一个 Hook,用于处理具有复杂状态逻辑的组件。它接受一个包含当前状态和触发状态更新的函数的 reducer,以及初始状态。通过 useReducer,我们可以更好地管理和处理复杂的状态变更逻辑。


基本语法如下:


import React, { useReducer } from 'react';

// 定义 reducer 函数
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
};

function ExampleComponent() {
// 使用 useReducer,传入 reducer 函数和初始状态
const [state, dispatch] = useReducer(reducer, { count: 0 });

return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>加一</button>
<button onClick={() => dispatch({ type: 'decrement' })}>减一</button>
</div>

);
}

通过刚刚实现的 useState 来实现类似 useReducer 的功能:


import { UnwrapRef } from "vue";
import useState from "./useState";

type ReducerType<T, A> = (state: T, action: A) => any;
export default function useReducer<T, A>(
reducer: ReducerType<UnwrapRef<T>, A>,
initialArg: T,
init?: (value: T) => T
) {
// 根据传没传init函数来初始化state
const [state, setState] = useState(init ? init(initialArg) : initialArg);
const dispatch = (action: A) => {
// 通过reducer函数的返回结果来修改state的值
setState((state) => reducer(state, action));
};
return [state, dispatch] as const;
}

<script setup lang="ts">
import { useReducer } from "./hooks";
const reducer = (
state: {
count: number
},
action: {
type: "increment" | "decrement";
}
) => {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
return state;
}
};
const identity = <T>(value: T) => {
return value;
};
const [state, dispatch] = useReducer(reducer, { count: 10 }, identity);
</script>
<template>
<div>
<div>
<p>Count: {{ state.count }}</p>
<button @click="() => dispatch({ type: 'increment' })">
加一
</button>
<button @click="() => dispatch({ type: 'decrement' })">
减一
</button>
</div>
</div>
</template>


useCallback


React 中的 useCallback:


useCallback 是 React 中的一个 Hook,用于返回一个 memoized 版本的回调函数,避免在每次渲染时都创建新的回调函数。这在防止不必要的渲染和优化性能方面非常有用。


基本语法如下:


import React, { useState, useCallback } from 'react';

function ExampleComponent() {
const [count, setCount] = useState(0);

// 使用 useCallback 返回 memoized 版本的回调函数
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]); // 依赖数组中的值发生变化时,重新创建回调函数

return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>加一</button>
</div>

);
}

下面是一个使用 useState 和 vue3 中的 watch 模拟实现类似 useCallback 的例子:


import { watch } from "vue";
import useState from "./useState";

type FnType<T> = (...args: T[]) => any;
export default function useCallback<T, D>(fn: FnType<T>, dependencies: D[]) {
const [callback, setCallback] = useState(fn);
// 如果依赖项有变更就把fn重新赋值没有就直接返回callback
watch(
dependencies,
() => {
setCallback((cb: FnType<T>) => cb = fn);
},
{ immediate: false }
);
return callback;
}

<script setup lang="ts">
import { useState, useCallback } from "./hooks";
const [count, setCount] = useState(0);

const handerCount = useCallback(() => {
setCount(n => n + 1)
}, [])
</script>
<template>
<div>
<button>{{ count }}</button>
<button @click="() => handerCount()">+</button>
</div>
</template>


useMemo


React 中的 useMemo:


useMemo 是 React 中的一个 Hook,用于记忆(memoize)计算结果,避免在每次渲染时都重新计算。它对于在渲染期间执行昂贵的计算并确保只在依赖项更改时重新计算结果非常有用。


基本语法如下:


import React, { useState, useMemo } from 'react';

function ExampleComponent() {
const [count, setCount] = useState(0);

// 使用 useMemo 记忆计算结果
const expensiveCalculation = useMemo(() => {
console.log('计算了一次...');
return count * 2;
}, [count]); // 依赖数组中的值发生变化时,重新计算结果

return (
<div>
<p>Count1: {count}</p>
<p>Count2: {expensiveCalculation}</p>
<button onClick={() => setCount(count + 1)}>加一</button>
</div>

);
}

下面是一个使用 useStateuseEffect 和 vue3 的 computed 模拟实现类似 useMemo 的例子:


import { UnwrapRef, computed } from "vue";
import useEffect from "./useEffect";
import useState from "./useState";

export default function useMemo<T, R>(
calculateValue: () => R,
dependencies: T[]
) {
const [cache, setCache] = useState<R | null>(null);
// 判断依赖项有没有变更,没有就直接返回缓存,有的话就重新计算
useEffect(() => {
setCache((cache) => {
return (cache = computed(calculateValue) as UnwrapRef<R>);
});
}, dependencies);
return cache as UnwrapRef<R>;
}

<script setup lang="ts">
import { useState, useMemo } from "./hooks";
const [numbers, setNumbers] = useState([1, 2]);
const squareSum = useMemo(() => {
console.log("计算平方和...");
return numbers.value.reduce((sum, num) => sum + num * num, 0);
}, [numbers]);
function handelNumbers() {
setNumbers(numbers => numbers.map(number => number * 2))
}
</script>
<template>
<div>
<div>平方: {{ squareSum }}</div>
<div>平方: {{ squareSum }}</div>
<button @click="handelNumbers">更改numbers</button>
</div>
</template>


useRef


React 中的 useRef:


useRef 是 React 中的一个 Hook,主要用于在函数组件中创建一个可变的对象,该对象的 current 属性被初始化为传入的参数。通常用于获取或存储组件中的引用(reference),并且不会触发组件重新渲染。


基本语法如下:


import React, { useRef, useEffect } from 'react';

function ExampleComponent() {
const myRef = useRef(null);

useEffect(() => {
// 使用 myRef.current 访问引用的 DOM 元素
console.log(myRef.current);
}, []);

return <div ref={myRef}>获取DOM</div>;
}

下面是一个使用 Vue 3 的 ref 模拟实现 useRef 的例子:


import { ref, Ref } from "vue";

function isHTMLElement(obj: unknown): obj is HTMLElement {
return obj instanceof HTMLElement;
}

function useRef<T extends HTMLElement>(initialValue: T | null): Ref<T | null>;
function useRef<T extends unknown>(
initialValue: T extends HTMLElement ? never : T
): { current: T };

function useRef(
initialValue: unknown
): Ref<HTMLElement | null> | { current: unknown } {
// 判断传入的是不是一个HTML节点
// 这里可能有点问题就是,或者传入null也会被判定为HTML节点,我没想到怎么解决这个问题
if (isHTMLElement(initialValue) || initialValue === null) {
return ref(initialValue);
} else {
// 不是就返回一个普通对象
return {
current: initialValue,
};
}
}

export default useRef;

<script setup lang="ts">
import { useRef, useEffect } from "./hooks";
const myInputRef = useRef(null);
const counterRef = useRef(0);

useEffect(() => {
myInputRef.value!.focus();
}, []);

const incrementCounter = () => {
counterRef.current += 1;
console.log('Counter:', counterRef.current);
};
</script>
<template>
<div>
<input ref="myInputRef" type="text" />
<p>Counter: {{ counterRef.current }}</p>
<button @click="incrementCounter">加一</button>
</div>
</template>


补充


对于react中的createContext,useContext和vue3中的provide,inject很像。


React 中的 createContext 和 useContext:



  1. createContext: 用于创建一个上下文对象,它包含一个 Provider 组件和一个 Consumer 组件。createContext 接受一个默认值,这个默认值在组件树中找不到对应的 Provider 时被使用。

const MyContext = React.createContext(defaultValue);


  1. useContext: 用于在函数组件中订阅上下文的变化,获取当前 Provider 提供的值。

const contextValue = useContext(MyContext);

Vue3 中的 provide 和 inject:



  1. provide: 用于在父组件中提供数据,被提供的数据可以被子组件通过 inject 访问到。provide 接受一个对象,对象的属性即为提供的数据。

<script setup lang="ts">
import { provide } from "vue";
const defaultValue = ref('defaultValue');
provide('defaultValue', defaultValue);
</script>


  1. inject: 用于在子组件中注入父组件提供的数据。可以是一个数组,也可以是一个对象,对象的属性为子组件中的变量名,值为从父组件中注入的数据。

<script setup lang="ts">
import { inject } from 'vue';
const defaultValue = inject('defaultValue');
</script>

相似之处:



  • 目的相同: 无论是 React 中的上下文和钩子,还是 Vue 3 中的 provide 和 inject,它们都旨在实现组件之间的状态共享,提供一种在组件树中传递数据的方式。
  • 使用方式: 在使用上,它们都在父组件中提供数据,并在子组件中获取数据。
  • 避免了 props 层层传递: 这些机制都避免了将数据通过 props 层层传递的麻烦,特别在深层嵌套的组件树中,可以更方便地进行状态管理。

总体而言,虽然具体的实现和语法有所不同,但这些机制在概念上非常相似,都是为了解决在组件树中共享数据的问题。


总结


本文源于作者的一时灵感,尝试探讨在 Vue 3 中是否能实现类似 React Hooks 的功能。虽然这个想法是出于好奇和娱乐,但在实际的开发中或许并没有太多实际用途。


通过对比 React Hooks 和 Vue 3 Composition API,我们发现两者在语法和实现上存在一些差异,但本质上都为开发者提供了在函数组件中组织和重用逻辑的方式。这种灵活性是前端技术不断演进的体现,而每一种方式都有其适用的场景。


在实际项目中,选择使用 React Hooks 还是 Vue3 Composition API 取决于团队和个人的偏好,以及项目的具体需求。技术的发展是不断前行的过程,而我们在其中的探索和实践都是宝贵的经验。


愿读者在技术的海洋中,既能保持对新鲜事物的好奇心,又能在实际项目中选择合适的工具,取得更好的开发效果。无论是整活还是严肃的技术探讨,都让我们在编码的世界里保持一份热爱和乐趣。


作者:辛克莱
来源:juejin.cn/post/7328229830134972425

0 个评论

要回复文章请先登录注册