在 Vue 中,我们曾经依赖 mixin 来实现逻辑复用,它简洁、方便,但在大型项目中也暴露出种种问题:变量来源不明、命名冲突、逻辑耦合……
本文将带你从 Vue 的角度出发,理解自定义 Hook 的设计理念,并通过实战演示其强大之处,帮助你完成从 mixin 到 Hook 的思维跃迁。
🧭 Vue 开发者痛点:从 Mixins 到 Composables
😣 混入之痛 —— mixins:
// Vue Mixins:全局状态污染警告!
const loggerMixin = {
methods: { log(msg) { console.log(msg) } }
}
export default { mixins: [loggerMixin] }
- 问题:隐式依赖、命名冲突、来源不明。
✅ Vue3 Composables:
// Vue 3 Composables:清晰的逻辑复用
export function useCounter() {
const count = ref(0)
const increment = () => count.value++
return { count, increment }
}
- 优点:明确依赖关系、按需引用。
React 的自定义 Hook,则可以视为更高阶的 Composables
:更灵活、更易组合、更契合函数式编程理念。
🔁 从 mixin 到 Hook:两种逻辑复用方式的对比
举个栗子:监听窗口宽度变化
Vue 的 mixin 示例
export default {
data() {
return {
width: window.innerWidth,
};
},
mounted() {
window.addEventListener('resize', this.updateWidth);
},
beforeDestroy() {
window.removeEventListener('resize', this.updateWidth);
},
methods: {
updateWidth() {
this.width = window.innerWidth;
},
},
};
使用时只需要 mixins: [resizeMixin]
,非常方便。但也有问题:
- 数据和行为耦合,难以测试
- 命名冲突风险大
- 外部看不到内部逻辑,调试困难
React 的自定义 Hook 实现
import { useEffect, useState } from 'react';
export function useWindowSize() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return width;
}
使用方式:
const width = useWindowSize();
优势明显:
- 数据和逻辑内聚,职责单一
- 命名可控,避免冲突
- 高度可组合与复用
🧠 自定义 Hook 是什么?
自定义 Hook 就是一个以
use
开头的函数,它内部可以使用其他 Hook,并封装逻辑实现复用。
📌 特点
- 必须以
use
开头,才能被 React 正确识别 - 内部可使用其他 Hook(如
useState
、useEffect
) - 本身不具备 UI 渲染功能,仅负责组织逻辑
- 支持嵌套、组合,便于测试与重构
🧩 写好自定义 Hook 的 4 个关键点
- 命名以
**use**
开头,才能被 React 正确识别 - 保持纯函数,不要直接修改 DOM 或状态以外的东西
- 合理使用
**useEffect**
管理副作用,避免闭包陷阱 - 提取为独立模块,方便测试与维护
🌟 常用自定义 Hook
1. useDebounce
: 输入防抖
场景:搜索输入场景,用户输入时不立即触发请求,而是停顿一段时间后再执行。
import { useState, useEffect } from "react";
function useDebounce(value, delay = 500) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
使用方式:
const [input, setInput] = useState("");
const debouncedInput = useDebounce(input, 300);
2. usePrevious
: 获取前一个状态值
场景:比较 props 或 state 的前后变化。
import { useRef, useEffect } from "react";
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
使用方式:
const previousCount = usePrevious(count);
3. useClickOutside
: 点击外部关闭弹窗
场景:实现点击弹窗外关闭菜单、模态框等。
import { useEffect } from "react";
function useClickOutside(ref, handler) {
useEffect(() => {
function listener(e) {
if (!ref.current || ref.current.contains(e.target)) return;
handler(e);
}
document.addEventListener("mousedown", listener);
return () => {
document.removeEventListener("mousedown", listener);
};
}, [ref, handler]);
}
使用方式:
const modalRef = useRef();
useClickOutside(modalRef, () => setShow(false));
4. useToggle
: 布尔值切换
场景:常用于控制模态框、侧边栏的显示与隐藏。
import { useState, useCallback } from "react";
function useToggle(defaultValue = false) {
const [state, setState] = useState(defaultValue);
const toggle = useCallback(() => setState(v => !v), []);
return [state, toggle];
}
使用方式:
const [isOpen, toggleOpen] = useToggle();
5. useLocalStorage
: 本地持久化
场景:将状态持久化到 localStorage。
import { useState } from "react";
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (err) {
return initialValue;
}
});
const setValue = (value) => {
const valToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valToStore);
localStorage.setItem(key, JSON.stringify(valToStore));
};
return [storedValue, setValue];
}
使用方式:
const [theme, setTheme] = useLocalStorage('theme', 'light');
✅ 总结
自定义 Hook 是 Vue 开发者理解 React 的一座桥梁,它优雅地解决了 mixin 存在的隐式逻辑问题,让逻辑复用变得更加清晰、纯粹、可维护。
从 mixin 到 Hook,不只是代码结构的变化,更是一次范式的转变。
希望这篇文章能帮你快速掌握 自定义 Hook,如果你觉得有帮助,别忘了点个赞👍或关注我后续的 重学 React 系列!