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/