从手里剑看设计模式 — 工厂与策略
忍具的制造与使用,完美诠释了工厂模式和策略模式的设计思想。让我们用忍者的视角重新理解这两种经典设计模式。
React 16.8 引入 Hooks 时,社区的反应两极分化——有人觉得「终于不用写 this 了」,有人觉得「这依赖数组是什么鬼」。但要真正理解 Hooks,你必须先接受一个认知:React 的函数组件不再只是"渲染函数",它变成了有状态的执行单元。
每次 re-render,你的组件函数都会重新执行一遍。Hooks 的魔法就在于:React 能在这一次次执行中记住你之前的状态。靠的是调用顺序——这也是「不能在条件语句里用 Hook」的根本原因。
function Counter() {
const [count, setCount] = useState(0)
// ❌ 连续调用三次 setCount(count + 1),count 还是 1
function wrongTriple() {
setCount(count + 1)
setCount(count + 1)
setCount(count + 1)
}
// ✅ 用函数式更新,每次拿到最新值
function correctTriple() {
setCount(c => c + 1)
setCount(c => c + 1)
setCount(c => c + 1)
}
return <button onClick={correctTriple}>+3</button>
}
💡 原理:
setCount(count + 1)读取的是当前渲染帧的count值(闭包),三次调用读到的都是同一个值。函数式更新c => c + 1则每次都基于最新值计算。当新状态依赖旧状态时,一定要用函数式更新。
// ❌ 每次 re-render 都执行一次复杂计算,尽管只有初次渲染用得到
const [data, setData] = useState(expensiveComputation())
// ✅ useState 支持传入初始化函数,只会执行一次
const [data, setData] = useState(() => expensiveComputation())
这是 React Hooks 最大的坑,没有之一。
function Timer() {
const [count, setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
// 🐛 Bug! count 永远是 0
console.log(count)
setCount(count + 1)
}, 1000)
return () => clearInterval(id)
}, []) // 空依赖 = 只在 mount 时执行
}
为什么 count 永远是 0? 因为 useEffect 的回调在 mount 时创建,它捕获了当时的 count = 0。之后每次 re-render 产生的新闭包里 count 是不同的,但定时器一直用的是最初那个闭包。
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1) // 函数式更新,不读 count
}, 1000)
return () => clearInterval(id)
}, []) // 空依赖也 OK!
function Timer() {
const [count, setCount] = useState(0)
const countRef = useRef(count)
countRef.current = count // 始终保持最新值
useEffect(() => {
const id = setInterval(() => {
console.log(countRef.current) // ✅ 总是最新值
}, 1000)
return () => clearInterval(id)
}, [])
}
React 世界经历了一条清晰的复用演化路线:Mixin(混乱)→ HOC(嵌套地狱)→ Render Props(啰嗦)→ Custom Hooks(简洁)。
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay)
return () => clearTimeout(timer)
}, [value, delay])
return debouncedValue
}
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch {
return initialValue
}
})
const setValue = (value: T | ((val: T) => T)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value
setStoredValue(valueToStore)
window.localStorage.setItem(key, JSON.stringify(valueToStore))
}
return [storedValue, setValue] as const
}
function useWindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 })
useEffect(() => {
function handleResize() {
setSize({ width: window.innerWidth, height: window.innerHeight })
}
window.addEventListener('resize', handleResize)
handleResize()
return () => window.removeEventListener('resize', handleResize)
}, [])
return size
}
看到模式了吗?自定义 Hook = 把有状态的逻辑提取出来,在多个组件间共享。它共享的是逻辑,不是状态本身——每个调用 useDebounce 的组件都有自己独立的 timer 和 debouncedValue。
一个判断标准:如果你的下一个状态依赖前一个状态,而且逻辑比较复杂,就该上 useReducer。
type Action =
| { type: 'ADD_TODO'; text: string }
| { type: 'TOGGLE'; id: number }
| { type: 'DELETE'; id: number }
function todoReducer(state: Todo[], action: Action): Todo[] {
switch (action.type) {
case 'ADD_TODO':
return [...state, { id: Date.now(), text: action.text, done: false }]
case 'TOGGLE':
return state.map(t => t.id === action.id ? { ...t, done: !t.done } : t)
case 'DELETE':
return state.filter(t => t.id !== action.id)
default:
return state
}
}
function TodoList() {
const [todos, dispatch] = useReducer(todoReducer, [])
return (
<div>
<button onClick={() => dispatch({ type: 'ADD_TODO', text: '新任务' })}>
添加
</button>
{todos.map(todo => (
<div key={todo.id}>
<span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => dispatch({ type: 'TOGGLE', id: todo.id })}>
{todo.done ? '撤销' : '完成'}
</button>
</div>
))}
</div>
)
}
有什么好处?所有状态变更逻辑集中在 todoReducer 里,组件只管触发 dispatch。测试时你不需要渲染组件,直接测 reducer 函数的输入输出即可。
| React Hooks | Vue Composables | |
|---|---|---|
| 执行时机 | 每次 re-render 都跑一遍 | 只在 setup 时跑一次 |
| 依赖追踪 | 手动声明依赖数组 | 自动追踪(Proxy) |
| 闭包问题 | 常见,需小心处理 | 不存在(ref 是引用) |
| 心智负担 | 较高(规则、闭包、memo) | 较低 |
| 复用模式 | 自定义 Hook | 可组合函数 |
🎯 React Hooks 的「重渲染即执行」模型决定了你必须时刻注意闭包和依赖数组。Vue 的响应式系统(Proxy + ref)天然避免了这些问题——ref 是引用,无论什么时候读
.value拿到的都是最新值。两种方案没有绝对的优劣,但写 React 时确实需要更警觉。
use 开头 — 让 React 和 ESLint 能识别react-hooks/exhaustive-deps 规则帮你检查掌握了这些模式,你已经能驾驭 90% 的 React 日常开发了。记住:Hooks 让你用更少的代码做更多的事,但前提是你理解它的运行机制,而不是死记硬背规则。
忍具的制造与使用,完美诠释了工厂模式和策略模式的设计思想。让我们用忍者的视角重新理解这两种经典设计模式。
告别 XML Layout,拥抱 Kotlin 声明式 UI —— Compose 的核心概念、状态管理、布局系统和导航一网打尽。
掌握 SwiftUI 的 View 协议、@State/@Binding 状态体系、布局三件套和 MVVM 架构——适合有 iOS 基础想转型的开发者。