Vue3 Composition API响应式原理与自定义Hook封装实战

Vue3响应式系统Proxy代理机制详解

Vue3的响应式系统基于ES6 Proxy实现,替代了Vue2中基于Object.defineProperty的方案。Proxy能劫持对象属性的增删改操作,包括数组索引修改和length变更,解决了Vue2无法检测属性新增和数组直接索引赋值的问题。响应式核心流程分为三步:reactive函数将目标对象包装为Proxy代理,代理的get/set陷阱拦截读写操作,effect函数建立依赖收集与触发更新的映射关系。

Vue3响应式系统的核心对象关系:reactive创建代理对象后,首次读取属性时触发track进行依赖收集,将当前正在执行的effect函数记录到该属性的依赖集合中;修改属性时触发trigger,遍历依赖集合依次执行关联的effect函数实现更新。这与Vue2的Dep/Watcher模式在概念上相似,但Proxy的拦截范围更广,性能损耗更低。

reactive与ref响应式API实现机制

reactive处理对象类型,内部创建Proxy代理,handler中的get拦截器递归处理嵌套对象实现深层响应。ref处理基本类型,内部通过对象包装(value属性)配合get/set访问器实现。以下是简化版响应式实现的核心逻辑:

// 简化的响应式系统核心逻辑
const targetMap = new WeakMap()
let activeEffect = null

function effect(fn) {
  activeEffect = fn
  fn()
  activeEffect = null
}

function track(target, key) {
  if (!activeEffect) return
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
  let dep = depsMap.get(key)
  if (!dep) {
    dep = new Set()
    depsMap.set(key, dep)
  }
  dep.add(activeEffect)
}

function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach(effect => effect())
  }
}

function reactive(target) {
  const handler = {
    get(obj, key) {
      track(obj, key)
      const result = Reflect.get(obj, key)
      if (typeof result === 'object' && result !== null) {
        return reactive(result)
      }
      return result
    },
    set(obj, key, value) {
      const oldValue = obj[key]
      const result = Reflect.set(obj, key, value)
      if (oldValue !== value) {
        trigger(obj, key)
      }
      return result
    },
    deleteProperty(obj, key) {
      const result = Reflect.deleteProperty(obj, key)
      trigger(obj, key)
      return result
    }
  }
  return new Proxy(target, handler)
}

function ref(value) {
  const wrapper = { _value: value }
  Object.defineProperty(wrapper, 'value', {
    get() {
      track(wrapper, 'value')
      return wrapper._value
    },
    set(newVal) {
      if (newVal !== wrapper._value) {
        wrapper._value = newVal
        trigger(wrapper, 'value')
      }
    }
  })
  return wrapper
}

自定义Hook封装模式与复用实践

Composition API的核心价值在于逻辑复用。通过自定义Hook(组合式函数)可以将组件内逻辑抽取为独立函数,多个组件共享同一套逻辑。自定义Hook命名以use开头,内部使用响应式API和生命周期钩子,返回响应式数据和方法。以下是几个常见业务场景的Hook封装示例:

// useMousePosition.ts - 鼠标位置追踪
import { ref, onMounted, onUnmounted } from 'vue'

export function useMousePosition() {
  const x = ref(0)
  const y = ref(0)

  function update(event: MouseEvent) {
    x.value = event.pageX
    y.value = event.pageY
  }

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return { x, y }
}

// useFetch.ts - 数据请求封装
import { ref, isRef, watch, unref, computed } from 'vue'

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

  async function doFetch() {
    data.value = null
    error.value = null
    loading.value = true

    try {
      const targetUrl = unref(url)
      const resp = await fetch(targetUrl, options)
      if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
      data.value = await resp.json()
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }

  if (isRef(url)) {
    watch(url, doFetch, { immediate: true })
  } else {
    doFetch()
  }

  return { data, error, loading, retry: doFetch }
}

// useLocalStorage.ts - localStorage响应式同步
import { ref, watch } from 'vue'

export function useLocalStorage(key, defaultValue) {
  const stored = localStorage.getItem(key)
  const data = ref(stored ? JSON.parse(stored) : defaultValue)

  watch(data, (newVal) => {
    localStorage.setItem(key, JSON.stringify(newVal))
  }, { deep: true })

  return data
}

// useDebounce.ts - 防抖Hook
import { ref, onUnmounted } from 'vue'

export function useDebounce(fn, delay = 300) {
  const timer = ref(null)

  function debounced(...args) {
    if (timer.value) clearTimeout(timer.value)
    timer.value = setTimeout(() => fn(...args), delay)
  }

  onUnmounted(() => {
    if (timer.value) clearTimeout(timer.value)
  })

  return debounced
}

// usePagination.ts - 分页逻辑封装
import { ref, computed, isRef } from 'vue'

export function usePagination(totalItems, perPage = 10) {
  const currentPage = ref(1)
  const total = isRef(totalItems) ? totalItems : ref(totalItems)
  
  const totalPages = computed(() =>
    Math.ceil(total.value / perPage)
  )
  
  const startIndex = computed(() =>
    (currentPage.value - 1) * perPage
  )
  
  const endIndex = computed(() =>
    Math.min(startIndex.value + perPage, total.value)
  )

  function goTo(page) {
    if (page >= 1 && page <= totalPages.value) {
      currentPage.value = page
    }
  }

  function next() { goTo(currentPage.value + 1) }
  function prev() { goTo(currentPage.value - 1) }

  return {
    currentPage, totalPages, startIndex, endIndex,
    goTo, next, prev
  }
}

组合式函数在组件中的使用与注意事项

自定义Hook在组件中的使用方式直观,在setup函数或<script setup>中解构调用即可:

<script setup lang="ts">
import { useMousePosition } from './composables/useMousePosition'
import { useFetch } from './composables/useFetch'
import { usePagination } from './composables/usePagination'
import { useDebounce } from './composables/useDebounce'

// 鼠标位置追踪
const { x, y } = useMousePosition()

// 数据请求
const apiUrl = ref('https://api.example.com/users')
const { data: users, loading, error, retry } = useFetch(apiUrl)

// 分页
const { currentPage, totalPages, goTo, next, prev } = usePagination(
  computed(() => users.value?.length || 0), 10
)

// 搜索防抖
const searchKeyword = ref('')
const handleSearch = useDebounce((keyword: string) => {
  apiUrl.value = `https://api.example.com/users?q=${keyword}`
}, 500)
</script>

使用自定义Hook有两个关键约束。第一,组合式函数必须在setup同步执行期间调用,不能在异步回调、setTimeout或事件监听回调中调用,因为Vue依赖调用栈上下文注册生命周期钩子。第二,解构返回的reactive对象会丢失响应性,需要使用toRefs转换。ref解构后仍保持响应性,因为解构的是ref对象本身而非其value。

// reactive解构丢失响应性的问题
const state = reactive({ count: 0, name: 'test' })
const { count } = state  // count变为普通值,非响应式

// 修复方案:使用toRefs
import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0, name: 'test' })
const { count, name } = toRefs(state)  // count和name是ref,保持响应式

大型项目中建议将组合式函数按功能域组织目录结构,如composables/目录下按useMouse.ts、useFetch.ts分文件管理,通过barrel文件统一导出。这样既便于按需引入,也方便在多项目间复用或抽取为独立npm包。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3compositionapi-xiang-ying-shi-yuan-li-yu-zi-ding-yi/

(0)
小编小编
上一篇 12小时前
下一篇 12小时前

相关推荐