vue.js的核心思想是数据驱动 而数据驱动的实现在本质上就是在数据变化后自动修改视图’让用户只需要关注数据的操作想要实现数据驱动,就需要对数据进行劫持。那么’在VUejs3x中是如何实现数据劫持的呢?它和VUejs2.x相比又做了哪些优化?什么是依赖收集、派发通知’它们又各自做了哪些事情呢?
先回顾下vue2 响应式实现方式
使用0bject.defi∩eP「ope「tyAPI把数据实现为‖问应式的有—定的缺陷:不能侦听对象属性的添加和删除。另外,在VUejs2的数据初始化阶段中’对于嵌套较深的对象递归执行 Object.defineProperty会带来_定的性能负担
reactive API实现
/**
* 创建响应式对象的核心函数
* @param target 要转换为响应式的目标对象
* @param isReadonly 是否创建只读响应式对象
* @param baseHandlers 基本类型处理程序(用于对象和数组)
* @param collectionHandlers 集合类型处理程序(用于Map、Set等)
* @param proxyMap 存储原始对象到代理对象的映射,用于缓存
*/
function createReactiveObject(
target: Target, // 目标对象:要转换为响应式的原始值
isReadonly: boolean, // 是否创建只读响应式对象的标记
baseHandlers: ProxyHandler<any>, // 基本类型的Proxy处理器(对象、数组等)
collectionHandlers: ProxyHandler<any>, // 集合类型的Proxy处理器(Map、Set等)
proxyMap: WeakMap<Target, any>, // 用于缓存已创建的代理对象的WeakMap
) {
// 1. 检查目标是否为对象类型,如果不是则直接返回原对象
if (!isObject(target)) {
// 在开发环境下,对于非对象类型发出警告
if (__DEV__) {
warn(
`value cannot be made ${isReadonly ? 'readonly' : 'reactive'}: ${String(
target,
)}`,
)
}
// 非对象类型无法转换为响应式,直接返回原目标
return target
}
// 2. 检查目标是否已经是一个代理对象
// 特殊情况:如果当前是要创建只读对象,而目标已经是响应式对象,则继续处理
if (
target[ReactiveFlags.RAW] && // 检查目标是否有RAW标记(表示是代理对象)
!(isReadonly && target[ReactiveFlags.IS_REACTIVE]) // 排除"对响应式对象创建只读代理"的情况
) {
// 目标已经是代理对象且不满足特殊情况,直接返回原代理对象
return target
}
// 3. 检查目标类型是否可以被观察(只有特定类型的值可以被转换为响应式)
const targetType = getTargetType(target) // 获取目标类型(普通对象/集合/无效类型)
if (targetType === TargetType.INVALID) { // 如果是无效类型(如函数、Symbol等)
return target // 直接返回原对象
}
// 4. 检查是否已经为该目标创建过相应的代理对象(缓存检查)
const existingProxy = proxyMap.get(target)
if (existingProxy) {
// 如果缓存中已存在对应的代理,则直接返回缓存的代理对象(避免重复创建)
return existingProxy
}
// 5. 创建新的代理对象
// 根据目标类型选择合适的处理器(集合类型使用collectionHandlers,其他使用baseHandlers)
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
)
// 6. 将新创建的代理对象存入缓存,以便后续复用
proxyMap.set(target, proxy)
// 7. 返回创建的响应式代理对象
return proxy
}
通过ProxyAPI劫持target对象,把它变成响应式的。我们j把new Proxy创建的proxy 实例称作响应式对象,这里Proxy对应的处理器对象会根据getTargetType获取到的目标数据类 型的不同而不同:如果是集合类型的数据,使用collectionHandlers;如果是普通对象和数组类 型的数据,则使用baseHandlers。
接下来,我们继续分析Proxy处理器对象mutableHandlers的实现
重写 set get deleteProperty ownKeys
依赖收集
get函数主要做了四件事情。首先,对特殊的key做了代理,比如遇到key是_v_raw,则直接返 回原始对象target。这就是我们在CreateReactiveObject函数中关判断响应式对象是否存在_v_raw 属性,并在其存在时返回该对象对应的原始对象的原因。
在分析这个函数的实现之前,想一下要收集的依赖是什么。我们的目的是实现响应式对象, 也就是当数据变化的时候自动做一些事情,比如执行某些函数。因此,我们收集的依赖就是数据 变化后执行的副作用函数。
派发通知
派发通知发生在数据更新阶段。因为用ProxyAPI劫持了数据对;象,所以当这个响应式对象 的属性值更新的时候,就会执行set函数。我们来看一下set函数的实现,它是执行createSetter 函数的返回值:
set函数主要做两件事情:首先通过Reflect.set求值;然后通过trigger函数派发通知, 并依据key是否存在于target上来确定通知类型,即新增还是修致。
Vue.js 3.x利用ProxyAPI实现了对数据访问和修改的劫持,弥补了Object.defineProperty API的一些不足。
响应式的核心实现就是通过数据劫持,在访问数据的时候执行依赖收集,在修改数据的时候 派发通知。收集的依赖是副作用函数,数据改变后就会触发副作用函数的自动执行。
己亥年 把数据变成响应式的,是为了在数据变化后自动执行一些逻辑。在组件的渲染中,就是让组 件访问的数据一旦被修改,就自动触发组件的重新渲染,实现数据驱动。最后,我们通过图9-3 来看一下响应式API的实现和组件更新之间的整体关系。
是不是很眼熟?没错,它和前面Vue.js2.x的响应式原理图很接近。其实Vue.js 3.x在响应式 的实现思路上和Vue.js2.x很相似,主要差别是改用ProxyAPI实现数据劫持,以及收集的依赖由 watcher实例变成了副作用渲染函数。