Vue3 Composition API响应式系统原理与自定义Hook封装

Vue3响应式系统Proxy拦截机制

Vue3的响应式系统基于ES6 Proxy实现,相比Vue2的Object.defineProperty方案,Proxy能直接拦截对象属性的增删操作,无需递归遍历,支持数组索引修改和Map/Set集合类型的响应式追踪。reactive函数用Proxy包装目标对象,在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) {
  return new Proxy(target, {
    get(obj, key) {
      track(obj, key)
      return Reflect.get(obj, key)
    },
    set(obj, key, value) {
      Reflect.set(obj, key, value)
      trigger(obj, key)
      return true
    }
  })
}

track函数在属性被读取时调用,将当前的activeEffect(即正在执行的副作用函数)记录到该属性的依赖集合中。trigger函数在属性被修改时调用,遍历依赖集合依次执行所有副作用函数,实现数据变化到视图更新的自动派发。WeakMap的使用确保目标对象被垃圾回收时,关联的依赖关系也能被自动清理。

ref与reactive的使用场景区分

reactive只能代理对象类型,对原始值(string、number、boolean)无法使用。ref通过将原始值包装为{value: x}对象来解决这个限制。ref的内部实现使用get/set访问器拦截value属性:

function ref(value) {
  const wrapper = {
    get value() {
      track(wrapper, 'value')
      return value
    },
    set value(newVal) {
      value = newVal
      trigger(wrapper, 'value')
    }
  }
  return wrapper
}

// 使用示例
const count = ref(0)
const state = reactive({ name: 'vue', list: [] })

effect(() => {
  console.log(`count: ${count.value}, name: ${state.name}`)
})

count.value++      // 输出: count: 1, name: vue
state.name = 'vue3' // 输出: count: 1, name: vue3

模板编译时,ref类型的变量会被自动解包,直接使用变量名而无需.value后缀。但在JavaScript逻辑中访问必须通过.value。一个实用原则:基本类型用ref,对象和数组用reactive。如果需要在reactive对象中嵌套使用ref,Vue3会自动解包第一层ref,深层嵌套仍需手动处理。

自定义Hook函数封装与复用

Composition API的核心优势在于逻辑可提取复用。将相关状态和操作封装为自定义Hook函数,是Vue3项目组织的推荐模式。以下是一个带有防抖和请求取消功能的异步数据加载Hook:

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

export function useFetchData(url, options = {}) {
  const {
    immediate = true,
    initialData = null,
    debounce = 0,
    transform = (data) => data
  } = options

  const data = ref(initialData)
  const loading = ref(false)
  const error = ref(null)
  let abortController = null
  let debounceTimer = null

  async function execute(params = {}) {
    if (abortController) {
      abortController.abort()
    }
    abortController = new AbortController()

    loading.value = true
    error.value = null

    try {
      const queryString = new URLSearchParams(params).toString()
      const fullUrl = queryString ? `${url}?${queryString}` : url

      const response = await fetch(fullUrl, {
        signal: abortController.signal,
        headers: { 'Content-Type': 'application/json' }
      })

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
      }

      const rawData = await response.json()
      data.value = transform(rawData)
    } catch (err) {
      if (err.name !== 'AbortError') {
        error.value = err.message
      }
    } finally {
      loading.value = false
    }
  }

  function debouncedExecute(params = {}) {
    if (debounceTimer) clearTimeout(debounceTimer)
    debounceTimer = setTimeout(() => execute(params), debounce)
  }

  function refresh() {
    return execute()
  }

  onUnmounted(() => {
    if (abortController) abortController.abort()
    if (debounceTimer) clearTimeout(debounceTimer)
  })

  if (immediate) {
    execute()
  }

  return {
    data,
    loading,
    error,
    execute: debounce > 0 ? debouncedExecute : execute,
    refresh
  }
}

该Hook在组件中使用时,所有异步加载逻辑只需一行声明:

import { useFetchData } from '@/hooks/useFetchData'

export default {
  setup() {
    const { data, loading, error, refresh } = useFetchData(
      '/api/users',
      {
        immediate: true,
        debounce: 300,
        transform: (raw) => raw.list
      }
    )

    return { data, loading, error, refresh }
  }
}

computed与watch的依赖追踪实现

computed函数创建惰性响应式计算属性,只有在被访问时才计算,且依赖未变化时返回缓存值。其实现原理是在effect基础上增加缓存标识和脏值检测:

function computed(getter) {
  let cachedValue
  let dirty = true

  const runner = effect(() => {
    // 依赖变化时标记为脏值
    dirty = true
    cachedValue = getter()
  })

  // runner首次执行后,依赖已建立
  // 但需要让runner在依赖变化时不立即执行
  // 实际实现中通过scheduler选项控制

  return {
    get value() {
      if (dirty) {
        dirty = false
        runner()
      }
      return cachedValue
    }
  }
}

watch函数监听响应式数据变化并执行副作用,支持监听单个ref、reactive对象的属性函数返回值、或多个数据源的数组。与直接使用effect不同,watch允许访问变化前后的值(oldValue和newValue),并通过flush选项控制回调执行时机:默认在组件更新前同步执行,设置flush:’post’推迟到DOM更新后执行,适合需要操作更新后DOM的场景。

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

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

相关推荐

Vue3 Composition API响应式系统原理与自定义Hook设计模式

Vue3 Composition API通过setup函数和一组响应式API,将组件逻辑按功能而非选项类型组织,解决了Options API在复杂组件中逻辑分散的问题。响应式系统底层基于Proxy代理实现依赖追踪与触发更新,reactive、ref、computed、watch四个API构成核心数据流。自定义Hook通过组合这些API实现逻辑复用,是Composition API设计哲学的最终落地方案。

reactive与ref响应式原理与Proxy追踪机制

Vue3响应式系统用ES6 Proxy替代了Vue2的Object.defineProperty。Proxy能拦截对象的所有属性操作(包括新增/删除),解决了Vue2无法检测属性新增的问题。reactive()函数对传入对象创建Proxy代理,在get拦截器中收集依赖、set拦截器中触发更新。

依赖收集的核心数据结构是targetMap(WeakMap),结构为 target -> key -> dep(Set)。当effect函数读取响应式数据时,get拦截器将当前activeEffect注册到对应dep集合。数据变更时,set拦截器遍历dep集合依次执行effect,完成视图更新。

import { reactive, effect } from 'vue'

const state = reactive({
  count: 0,
  list: [1, 2, 3]
})

// effect自动追踪state.count依赖
effect(() => {
  console.log('count changed:', state.count)
})

state.count++  // 控制台输出: count changed: 1

// reactive的局限:仅支持对象类型,基本类型需用ref
const num = reactive(0)  // 警告:reactive()需要传入对象

ref为基本类型值提供响应式能力。内部通过对象包装({ value: T }),对value属性做getter/setter拦截。ref的.value访问在模板中自动解包,但在JS逻辑中必须通过.value读写。reactive对象内部嵌套的ref会自动解包,直接访问值无需.value。

import { ref, reactive, isRef, unref } from 'vue'

const count = ref(0)
const name = ref('Vue')

// JS中需通过.value访问
count.value++
console.log(count.value)  // 1

// reactive对象内的ref自动解包
const state = reactive({ count, name })
console.log(state.count)    // 1,无需.value
console.log(state.name)     // 'Vue'

// 判断是否为ref
console.log(isRef(count))   // true
console.log(isRef(state))   // false

// 安全取值(ref返回.value,非ref原样返回)
console.log(unref(count))   // 1
console.log(unref(42))      // 42

computed计算属性与watch依赖追踪机制

computed创建惰性响应式值——只在被读取时才计算,且缓存计算结果直到依赖变更。computed内部维护dirty标记,依赖变更时标记为dirty但不立即重算,下次读取时才重新计算。

import { ref, computed, watch, watchEffect } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

// 可写computed
const fullName = computed({
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  set(newValue) {
    [firstName.value, lastName.value] = newValue.split(' ')
  }
})

console.log(fullName.value)  // 'John Doe'
fullName.value = 'Jane Smith'
console.log(firstName.value)  // 'Jane'

// watch显式声明监听源,获取新旧值
watch(fullName, (newVal, oldVal) => {
  console.log(`${oldVal} -> ${newVal}`)
}, { immediate: true })

// watchEffect自动收集依赖,无需声明监听源
watchEffect(() => {
  console.log('当前全名:', fullName.value)
})

watch和watchEffect的核心区别:watch需要显式指定监听源,能拿到新旧值对比,默认惰性执行(除非immediate: true);watchEffect自动追踪回调内使用的响应式数据,立即执行一次建立依赖。监听多个源时watch接收数组:watch([refA, refB], ([a, b], [oldA, oldB]) => {})。

深度监听通过deep: true开启,但会给对象每个嵌套属性都建立依赖,性能开销较大。对于深层嵌套对象且只需整体变更检测的场景,用watchEffect配合手动比较更高效。

自定义Hook设计模式与逻辑复用策略

自定义Hook本质上是以”use”开头的函数,内部组合响应式API封装可复用逻辑,返回响应式数据和方法。良好的Hook遵循单一职责原则,输入通过参数接收配置,输出通过返回值暴露API。

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

// 鼠标位置追踪Hook
function useMousePosition() {
  const x = ref(0)
  const y = ref(0)

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

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

  return { x, y }
}

// 使用
const { x, y } = useMousePosition()
// 模板中直接使用 x, y
import { ref, computed, watch, readonly, type Ref } from 'vue'

// 分页Hook:封装页码状态与计算逻辑
function usePagination(totalItems: Ref, pageSize: number = 10) {
  const currentPage = ref(1)
  
  const totalPages = computed(() => 
    Math.ceil(totalItems.value / pageSize)
  )
  
  const hasNext = computed(() => 
    currentPage.value < totalPages.value
  )
  
  const hasPrev = computed(() => 
    currentPage.value > 1
  )
  
  function nextPage() {
    if (hasNext.value) currentPage.value++
  }
  
  function prevPage() {
    if (hasPrev.value) currentPage.value--
  }
  
  function goToPage(page: number) {
    const clamped = Math.max(1, Math.min(page, totalPages.value))
    currentPage.value = clamped
  }
  
  // totalItems变化时重置到第一页
  watch(totalItems, () => {
    currentPage.value = 1
  })
  
  return {
    currentPage: readonly(currentPage),
    totalPages,
    hasNext,
    hasPrev,
    nextPage,
    prevPage,
    goToPage
  }
}

// 组件中使用
const totalCount = ref(150)
const { 
  currentPage, totalPages, 
  nextPage, prevPage, goToPage 
} = usePagination(totalCount, 20)

响应式性能优化与内存泄漏防治

大型列表渲染推荐使用虚拟滚动(vue-virtual-scroller),避免大量DOM节点创建。响应式数据并非越多越好——不参与视图更新的数据用shallowRef/shallowRef或markRaw避免不必要的代理追踪。

import { shallowRef, triggerRef, markRaw } from 'vue'

// shallowRef:仅.value变更触发更新,深层修改不追踪
const largeList = shallowRef<{ id: number }[]>([])
largeList.value.push({ id: 1 })  // 不触发更新
triggerRef(largeList)              // 手动触发更新

// markRaw:标记对象永不转为响应式
const chartInstance = markRaw(echarts.init(dom))
// 第三方实例对象不需要响应式追踪

内存泄漏排查重点:事件监听器是否在onUnmounted清除、setInterval/setTimeout是否清除、WebSocket连接是否关闭。effectScope能批量管理一组effect的生命周期,适合封装复杂Hook时统一清理。

import { effectScope, watchEffect } from 'vue'

function useDashboard() {
  const scope = effectScope()
  
  scope.run(() => {
    watchEffect(() => { /* 数据轮询逻辑 */ })
    watchEffect(() => { /* 定时刷新逻辑 */ })
  })
  
  onUnmounted(() => {
    scope.stop()  // 一次性清除scope内所有effect
  })
}

effectScope确保Hook内部所有watchEffect和watch在组件卸载时统一清理,避免遗漏。对于内部创建多个监听器的复杂Hook,用effectScope包裹比逐个手动清理更安全。

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

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

相关推荐