深入Vue3/React Hooks

111 阅读6分钟

前言

  • 常网IT源码上线啦!
  • 本篇录入技术选型专栏,希望能祝君拿下Offer一臂之力,各位看官感兴趣可移步🚶。
  • 有人说面试造火箭,进去拧螺丝;其实个人觉得问的问题是项目中涉及的点 || 热门的技术栈都是很好的面试体验,不要是旁门左道冷门的知识,实际上并不会用到的。
  • 接下来想分享一些自己在项目中遇到的技术选型以及问题场景。

鸿鸟只思羽翼齐,点翅飞腾千万里
“只有羽翼齐备才能飞翔千万里
只有准备充分、全面发展
才能取得成功”

18d356928b491450efb5fc92f4ce8fe.jpg

破碎的,也能成为艺术品。

一、前言

vue3出了Hooks的概念。

Vue3的Hooks(更准确称为Composition API)是逻辑复用机制的革命性创新。它允许开发者将组件逻辑拆分为可复用的函数单元,彻底改变了传统的Options API组织方式。

  • 逻辑关注点分离:将相关代码组织在一起(而非按data/methods分块)

  • 无this的编程模型:避免上下文绑定问题

  • 类型推导友好:天然支持TypeScript

  • 可测试性:纯函数逻辑更易单元测试

直入正文。

二、高级Hooks模式

异步状态管理

import { ref } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)

  async function fetchData() {
    loading.value = true
    try {
      const response = await fetch(url)
      data.value = await response.json()
    } catch (err) {
      error.value = err
    } finally {
      loading.value = false
    }
  }

  return { data, error, loading, fetchData }
}

resize

import { onMounted, onUpdated, onUnmounted } from 'vue'

export function useWindowResize() {
  const width = ref(window.innerWidth)
  
  function updateWidth() {
    width.value = window.innerWidth
  }

  onMounted(() => window.addEventListener('resize', updateWidth))
  onUnmounted(() => window.removeEventListener('resize', updateWidth))
  
  return { width }
}

状态共享Hook

import { reactive, readonly } from 'vue'

// 全局状态管理
const globalState = reactive({
  theme: 'light',
  locale: 'en-US'
})

export function useGlobalState() {
  function setTheme(theme) {
    globalState.theme = theme
  }

  return {
    state: readonly(globalState), // 只读访问
    setTheme
  }
}

Hooks开发的时候,我一般会遵循几个原则:

  1. 单一职责原则:每个Hook只解决一个问题

  2. 命名约定useXxx格式(如useMousePosition

  3. 依赖注入:通过参数接收外部依赖

  4. 返回值标准化:返回ref/reactive对象

拖拽Hook

import { ref, onMounted, onUnmounted } from 'vue'

export function useDrag(elementRef) {
  const x = ref(0)
  const y = ref(0)
  const isDragging = ref(false)

  let startX = 0
  let startY = 0

  function onMouseDown(e) {
    isDragging.value = true
    startX = e.clientX - x.value
    startY = e.clientY - y.value
    
    document.addEventListener('mousemove', onMouseMove)
    document.addEventListener('mouseup', onMouseUp)
  }

  function onMouseMove(e) {
    x.value = e.clientX - startX
    y.value = e.clientY - startY
  }

  function onMouseUp() {
    isDragging.value = false
    document.removeEventListener('mousemove', onMouseMove)
    document.removeEventListener('mouseup', onMouseUp)
  }

  onMounted(() => {
    elementRef.value.addEventListener('mousedown', onMouseDown)
  })

  onUnmounted(() => {
    elementRef.value.removeEventListener('mousedown', onMouseDown)
  })

  return { x, y, isDragging }
}

使用

<template>
  <div ref="draggable" :style="{ left: `${x}px`, top: `${y}px` }">
    拖拽我 ({{ isDragging ? '拖动中' : '静止' }})
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useDrag } from './useDrag'

const draggable = ref(null)
const { x, y, isDragging } = useDrag(draggable)
</script>

性能优化

// 1. 惰性计算:使用computed
const expensiveValue = computed(() => 
  heavyCalculation(state.data)
)

// 2. 事件节流
import { throttle } from 'lodash-es'

export function useScrollPosition() {
  const scrollY = ref(0)
  
  const updateScroll = throttle(() => {
    scrollY.value = window.scrollY
  }, 100)

  onMounted(() => window.addEventListener('scroll', updateScroll))
  onUnmounted(() => window.removeEventListener('scroll', updateScroll))
  
  return { scrollY }
}

// 3. 依赖收集优化
watch([userId, params], () => fetchData(), { deep: false })

测试用例

// 使用Vitest测试Hook
import { test, expect } from 'vitest'
import { ref } from 'vue'
import { useCounter } from './useCounter'

test('useCounter hook', () => {
  // 创建测试上下文
  const { count, increment } = useCounter(5)
  
  expect(count.value).toBe(5)
  
  increment()
  expect(count.value).toBe(6)
  
  increment(4)
  expect(count.value).toBe(10)
})

三、设计模式分类

模式描述示例
状态Hook封装响应式状态useState, useToggle
副作用Hook处理生命周期和外部交互useFetch, useEvent
上下文Hook访问组件上下文useRouter, useStore
工具Hook提供通用功能useClipboard, useDebounce
组合Hook组合多个基础HookuseUserDashboard

小而专:每个Hook解决单一问题

参数设计:使用配置对象提高扩展性

function useFetch(url, { immediate = true, timeout = 5000 } = {})

返回值规范:返回响应式对象+方法集合

文档注释:使用TSDoc规范注释

/**
 * 获取鼠标位置
 * @returns { x: Ref<number>, y: Ref<number> }
 */

错误处理:提供错误状态和重试机制

四、React Hooks

自定义Hooks命名:以use开头(如useAuth

类组件 vs 函数组件+Hooks

// 类组件 vs 函数组件+Hooks
class Counter extends React.Component {
  state = { count: 0 }
  
  increment = () => {
    this.setState({ count: this.state.count + 1 })
  }
  
  render() {
    return (
      <button onClick={this.increment}>
        Count: {this.state.count}
      </button>
    )
  }
}

// 使用Hooks的函数组件
function Counter() {
  const [count, setCount] = useState(0)
  
  const increment = () => setCount(count + 1)
  
  return (
    <button onClick={increment}>
      Count: {count}
    </button>
  )
}

useState实现原理

// 简化的useState实现
let state = []
let setters = []
let stateIndex = 0

function useState(initialValue) {
  const currentIndex = stateIndex
  state[currentIndex] = state[currentIndex] || initialValue
  
  function setState(newValue) {
    state[currentIndex] = newValue
    render() // 触发重新渲染
  }
  
  setters.push(setState)
  stateIndex++
  
  return [state[currentIndex], setState]
}

React内部使用链表结构管理Hooks

// React内部使用链表结构管理Hooks
type Hook = {
  memoizedState: any,      // 当前状态
  next: Hook | null,       // 下一个Hook
};

function updateWorkInProgressHook() {
  // 基于调用顺序遍历Hook链表
  const hook = nextWorkInProgressHook;
  nextWorkInProgressHook = hook.next;
  return hook;
}

useEffect:副作用管理

有点像vue的onMounted + watch结合体

function Timer() {
  const [seconds, setSeconds] = useState(0)
  
  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(s => s + 1)
    }, 1000)
    
    // 清理函数
    return () => clearInterval(interval)
  }, []) // 空依赖数组表示只运行一次

  return <div>Seconds: {seconds}</div>
}

useReducer:复杂状态管理

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'decrement':
      return { count: state.count - 1 }
    default:
      throw new Error()
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 })
  
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
    </>
  )
}

useCallback & useMemo:性能优化

useMemo有点像计算属性了。

function Parent() {
  const [count, setCount] = useState(0)
  
  // 使用useCallback避免函数重建
  const increment = useCallback(() => {
    setCount(c => c + 1)
  }, [])
  
  // 使用useMemo缓存计算结果
  const doubleCount = useMemo(() => {
    return count * 2
  }, [count])
  
  return (
    <div>
      <Child onClick={increment} />
      <p>Count: {count}, Double: {doubleCount}</p>
    </div>
  )
}

useCallback解决什么问题?

问题场景

function Parent() {
  const [count, setCount] = useState(0);
  
  // 每次渲染创建新函数
  const handleClick = () => {
    console.log('Click handled');
  };
  
  return <Child onClick={handleClick} />;
}

// Child组件
const Child = React.memo(({ onClick }) => {
  // 即使props没变,因onClick引用不同仍会重渲染
  return <button onClick={onClick}>Click me</button>;
});

正确使用姿势

function Parent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');
  
  // 只有count变化时重建函数
  const handleClick = useCallback(() => {
    console.log(`Count: ${count}`);
  }, [count]); // ✅ 正确声明依赖
  
  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <Child onClick={handleClick} />
    </>
  );
}

何时使用useCallback

  • 函数作为useEffect依赖时

  • 函数作为子组件prop时(配合React.memo)

  • 函数被其他Hook依赖时

何时使用useMemo

  • 计算成本高昂的操作(>1ms)

  • 引用类型(对象/数组)作为依赖或prop时

  • 稳定引用需要传递给子组件时

闭包陷阱

function Counter() {
  const [count, setCount] = useState(0)
  
  useEffect(() => {
    const interval = setInterval(() => {
      // 闭包陷阱:始终使用初始count值
      setCount(count + 1)
    }, 1000)
    
    return () => clearInterval(interval)
  }, []) // 缺少count依赖

  // 正确方案1:使用函数式更新
  useEffect(() => {
    const interval = setInterval(() => {
      setCount(c => c + 1) // 使用最新值
    }, 1000)
    
    return () => clearInterval(interval)
  }, [])
  
  // 正确方案2:使用ref保存值
  const countRef = useRef(count)
  countRef.current = count
  
  useEffect(() => {
    const interval = setInterval(() => {
      setCount(countRef.current + 1)
    }, 1000)
    
    return () => clearInterval(interval)
  }, [])

  return <div>{count}</div>
}

至此撒花~

后记

我发现react的写法也很不错。

我们在实际项目中或多或少遇到一些奇奇怪怪的问题。

自己也会对一些写法的思考,为什么不行🤔,又为什么行了?

最后,祝君能拿下满意的offer。

我是Dignity_呱,来交个朋友呀,有朋自远方来,不亦乐乎呀!深夜末班车

👍 如果对您有帮助,您的点赞是我前进的润滑剂。

以往推荐

为什么一定要有微任务,直接一个宏任务不行吗

聊聊小程序的双线程架构

Vue性能优化:从加载提速到运行时优化

vue2和Vue3和React的diff算法展开说说:从原理到优化策略

前端哪有什么设计模式

小小导出,我大前端足矣!

前端仔,快把dist部署到Nginx上

多图详解,一次性啃懂原型链(上万字)

Vue-Cli3搭建组件库

Vue实现动态路由(和面试官吹项目亮点)

VuePress搭建项目组件文档

原文链接

juejin.cn/post/753797…