前言
<< 温馨提醒 >>
本文内容偏干,建议边喝水边食用,如有不适请及时点赞
【A】:能不能说说 Vue3 响应式都处理了哪些数据类型?都怎么处理的呀?
【B】:能,只能说一点点...
【A】:...
只要问到 Vue
相关的内容,似乎总绕不过 响应式原理 的话题,随之而来的回答必然是围绕着 Object.defineProperty
和 Proxy
来展开(即 Vue2
和 Vue3
),但若继续追问某些具体实现是不是就仓促结束回答了()。你跑我追,你不跑我还追
本文就不再过多介绍 Vue2
中响应式的处理,感兴趣可以参考 从 vue 源码看问题 —— 如何理解 Vue
响应式?,但是会有简单提及,下面就来看看 Vue3
中是如何处理 原始值、Object、Array、Set、Map 等数据类型的响应式。
从 Object.defineProperty
到 Proxy
一切的一切还得从 Object.defineProperty
开始讲起,那是一个不一样的 API
... ()bgm 响起,自行体会
Object.defineProperty
Object.defineProperty(obj, prop, descriptor)
方法会直接在一个对象上定义一个 新属性,或修改一个 对象 的 现有属性,并返回此对象,其参数具体为:
obj
:要定义属性的对象prop
:要定义或修改的 属性名称 或Symbol
descriptor
:要定义或修改的 属性描述符
从以上的描述就可以看出一些限制,比如:
- 目标是 对象属性,不是 整个对象
- 一次只能 定义或修改一个属性
- 当然有对应的一次处理多个属性的方法
Object.defineProperties()
,但在vue
中并不适用,因为vue
不能提前知道用户传入的对象都有什么属性,因此还是得经过类似Object.keys() + for
循环的方式获取所有的key -> value
,而这其实是没有必要使用Object.defineProperties()
在 Vue2 中的缺陷
Object.defineProperty()
实际是通过 定义 或 修改对象属性
的描述符来实现 数据劫持,其对应的缺点也是没法被忽略的:
- 只能拦截对象属性的
get
和set
操作,比如无法拦截delete
、in
、方法调用
等操作 - 动态添加新属性(响应式丢失)
- 保证后续使用的属性要在初始化声明
data
时进行定义 - 使用
this.$set()
设置新属性
- 通过
delete
删除属性(响应式丢失)
- 使用
this.$delete()
删除属性
- 使用数组索引 替换/新增 元素(响应式丢失)
- 使用
this.$set()
设置新元素
- 使用数组
push、pop、shift、unshift、splice、sort、reverse
等 原生方法 改变原数组时(响应式丢失)
- 使用 重写/增强 后的
push、pop、shift、unshift、splice、sort、reverse
方法
- 一次只能对一个属性实现 数据劫持,需要遍历对所有属性进行劫持
- 数据结构复杂时(属性值为 引用类型数据),需要通过 递归 进行处理
【扩展】Object.defineProperty
和 Array
?
它们有啥关系,其实没有啥关系,只是大家习惯性的会回答 Object.defineProperty
不能拦截 Array
的操作,这句话说得对但也不对。
使用 Object.defineProperty 拦截 Array
Object.defineProperty
可用于实现对象属性的 get
和 set
拦截,而数组其实也是对象,那自然是可以实现对应的拦截操作,如下:
Vue2 为什么不使用 Object.defineProperty 拦截 Array?
尤大在曾在 GitHub
的 Issue
中做过如下回复:
说实话性能问题到底指的是什么呢?
下面是总结了一些目前看到过的回答:
- 数组 和 普通对象 在使用场景下有区别,在项目中使用数组的目的大多是为了 遍历,即比较少会使用
array[index] = xxx
的形式,更多的是使用数组的Api
的方式 - 数组长度是多变的,不可能像普通对象一样先在
data
选项中提前声明好所有元素,比如通过array[index] = xxx
方式赋值时,一旦index
的值超过了现有的最大索引值,那么当前的添加的新元素也不会具有响应式 - 数组存储的元素比较多,不可能为每个数组元素都设置
getter/setter
- 无法拦截数组原生方法如
push、pop、shift、unshift
等的调用,最终仍需 重写/增强 原生方法
Proxy & Reflect
由于在 Vue2
中使用 Object.defineProperty
带来的缺陷,导致在 Vue2
中不得不提供了一些额外的方法(如:Vue.set、Vue.delete()
)解决问题,而在 Vue3
中使用了 Proxy
的方式来实现 数据劫持,而上述的问题在 Proxy
中都可以得到解决。
Proxy
Proxy
主要用于创建一个 对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等),本质上是通过拦截对象 内部方法 的执行实现代理,而对象本身根据规范定义的不同又会区分为 常规对象 和 异质对象(这不是重点,可自行了解)。
new Proxy(target, handler)
是针对整个对象进行的代理,不是某个属性- 代理对象属性拥有 读取、修改、删除、新增、是否存在属性 等操作相应的捕捉器,更多可见
get()
属性 读取 操作的捕捉器set()
属性 设置 操作的捕捉器deleteProperty()
是delete
操作符的捕捉器ownKeys()
是Object.getOwnPropertyNames
方法和Object.getOwnPropertySymbols
方法的捕捉器has()
是in
操作符的捕捉器
Reflect
Reflect
是一个内置的对象,它提供拦截 JavaScript
操作的方法,这些方法与 Proxy handlers
提供的的方法是一一对应的,且 Reflect
不是一个函数对象,即不能进行实例化,其所有属性和方法都是静态的。
Reflect.get(target, propertyKey[, receiver])
获取对象身上某个属性的值,类似于target[name]
Reflect.set(target, propertyKey, value[, receiver])
将值分配给属性的函数。返回一个Boolean
,如果更新成功,则返回true
Reflect.deleteProperty(target, propertyKey)
作为函数的delete
操作符,相当于执行delete target[name]
Reflect.ownKeys(target)
返回一个包含所有自身属性(不包含继承属性)的数组。(类似于Object.keys()
, 但不会受enumerable
影响)Reflect.has(target, propertyKey)
判断一个对象是否存在某个属性,和in
运算符 的功能完全相同
Proxy 为什么需要 Reflect 呢?
在 Proxy
的 get(target, key, receiver)、set(target, key, newVal, receiver)
的捕获器中都能接到前面所列举的参数:
target
指的是 原始数据对象key
指的是当前操作的 属性名newVal
指的是当前操作接收到的 最新值receiver
指向的是当前操作 正确的上下文
怎么理解 Proxy handler
中 receiver
指向的是当前操作正确上的下文呢?
- 正常情况下,
receiver
指向的是 当前的代理对象 - 特殊情况下,
receiver
指向的是 引发当前操作的对象
- 通过
Object.setPrototypeOf()
方法将代理对象proxy
设置为普通对象obj
的原型 - 通过
obj.name
访问其不存在的name
属性,由于原型链的存在,最终会访问到proxy.name
上,即触发get
捕获器
在 Reflect
的方法中通常只需要传递 target、key、newVal
等,但为了能够处理上述提到的特殊情况,一般也需要传递 receiver
参数,因为 Reflect 方法中传递的 receiver 参数代表执行原始操作时的 this
指向,比如:Reflect.get(target, key , receiver)
、Reflect.set(target, key, newVal, receiver)
。
总结:Reflect
是为了在执行对应的拦截操作的方法时能 传递正确的 this
上下文。
Vue3 如何使用 Proxy 实现数据劫持?
Vue3
中提供了 reactive()
和 ref()
两个方法用来将 目标数据 变成 响应式数据,而通过 Proxy
来实现 数据劫持(或代理) 的具体实现就在其中,下面一起来看看吧!
reactive 函数
从源码来看,其核心其实就是 createReactiveObject(...)
函数,那么继续往下查看对应的内容
源码位置:packages\reactivity\src\reactive.ts
export function reactive(target: object) { // if trying to observe a readonly proxy, return the readonly version. // 若目标对象是响应式的只读数据,则直接返回 if (isReadonly(target)) { return target } // 否则将目标数据尝试变成响应式数据 return createReactiveObject( target, false, mutableHandlers, // 对象类型的 handlers mutableCollectionHandlers, // 集合类型的 handlers reactiveMap ) } 复制代码
createReactiveObject() 函数
源码的体现也是非常简单,无非就是做一些前置判断处理:
- 若目标数据是 原始值类型,直接向返回 原数据
- 若目标数据的
__v_raw
属性为true
,且是【非响应式数据】或 不是通过调用readonly()
方法,则直接返回 原数据 - 若目标数据已存在相应的
proxy
代理对象,则直接返回 对应的代理对象 - 若目标数据不存在对应的 白名单数据类型 中,则直接返回原数据,支持响应式的数据类型如下:
- 可扩展的对象,即是否可以在它上面添加新的属性
- __v_skip 属性不存在或值为 false 的对象
- 数据类型为
Object、Array、Map、Set、WeakMap、WeakSet
的对象 - 其他数据都统一被认为是 无效的响应式数据对象
- 通过
Proxy
创建代理对象,根据目标数据类型选择不同的Proxy handlers
看来具体的实现又在不同数据类型的 捕获器 中,即下面源码的 collectionHandlers
和 baseHandlers
,而它们则对应的是在上述 reactive()
函数中为 createReactiveObject()
函数传递的 mutableCollectionHandlers
和 mutableHandlers
参数。
源码位置:packages\reactivity\src\reactive.ts
function createReactiveObject( target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any>, proxyMap: WeakMap<Target, any> ) { // 非对象类型直接返回 if (!isObject(target)) { if (__DEV__) { console.warn(`value cannot be made reactive: ${String(target)}`) } return target } // 目标数据的 __v_raw 属性若为 true,且是【非响应式数据】或 不是通过调用 readonly() 方法,则直接返回 if ( target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE]) ) { return target } // 目标对象已存在相应的 proxy 代理对象,则直接返回 const existingProxy = proxyMap.get(target) if (existingProxy) { return existingProxy } // 只有在白名单中的值类型才可以被代理监测,否则直接返回 const targetType = getTargetType(target) if (targetType === TargetType.INVALID) { return target } // 创建代理对象 const proxy = new Proxy( target, // 若目标对象是集合类型(Set、Map)则使用集合类型对应的捕获器,否则使用基础捕获器 targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers ) // 将对应的代理对象存储在 proxyMap 中 proxyMap.set(target, proxy) return proxy } 复制代码
捕获器 Handlers
对象类型的捕获器 — mutableHandlers
这里的对象类型指的是 数组 和 普通对象
源码位置:packages\reactivity\src\baseHandlers.ts
export const mutableHandlers: ProxyHandler<object> = { get, set, deleteProperty, has, ownKeys } 复制代码
以上这些捕获器其实就是我们在上述 Proxy
部分列举出来的捕获器,显然可以拦截对普通对象的如下操作:
- 读取,如
obj.name
- 设置,如
obj.name = 'zs'
- 删除属性,如
delete obj.name
- 判断是否存在对应属性,如
name in obj
- 获取对象自身的属性值,如
obj.getOwnPropertyNames()
和obj.getOwnPropertySymbols()
get
捕获器
具体信息在下面的注释中,这里只列举核心内容:
- 若当前数据对象是 数组,则 重写/增强 数组对应的方法
- 数组元素的 查找方法:
includes、indexOf、lastIndexOf
- 修改原数组 的方法:
push、pop、unshift、shift、splice
- 若当前数据对象是 普通对象,且非 只读 的则通过
track(target, TrackOpTypes.GET, key)
进行 依赖收集
- 若当前数据对象是 浅层响应 的,则直接返回其对应属性值
- 若当前数据对象是 ref 类型的,则会进行 自动脱 ref
- 若当前数据对象的属性值是 对象类型
- 若当前属性值属于 只读的,则通过
readonly(res)
向外返回其结果 - 否则会将当前属性值以
reactive(res)
向外返回 proxy 代理对象
- 否则直接向外返回对应的 属性值
function createGetter(isReadonly = false, shallow = false) { return function get(target: Target, key: string | symbol, receiver: object) { // 当直接通过指定 key 访问 vue 内置自定义的对象属性时,返回其对应的值 if (key === ReactiveFlags.IS_REACTIVE) { return !isReadonly } else if (key === ReactiveFlags.IS_READONLY) { return isReadonly } else if (key === ReactiveFlags.IS_SHALLOW) { return shallow } else if ( key === ReactiveFlags.RAW && receiver === (isReadonly ? shallow ? shallowReadonlyMap : readonlyMap : shallow ? shallowReactiveMap : reactiveMap ).get(target) ) { return target } // 判断是否为数组类型 const targetIsArray = isArray(target) // 数组对象 if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) { // 重写/增强数组的方法: // - 查找方法:includes、indexOf、lastIndexOf // - 修改原数组的方法:push、pop、unshift、shift、splice return Reflect.get(arrayInstrumentations, key, receiver) } // 获取对应属性值 const res = Reflect.get(target, key, receiver) if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) { return res } // 依赖收集 if (!isReadonly) { track(target, TrackOpTypes.GET, key) } // 浅层响应 if (shallow) { return res } // 若是 ref 类型响应式数据,会进行【自动脱 ref】,但不支持【数组】+【索引】的访问方式 if (isRef(res)) { const shouldUnwrap = !targetIsArray || !isIntegerKey(key) return shouldUnwrap ? res.value : res } // 属性值是对象类型: // - 是只读属性,则通过 readonly() 返回结果, // - 且是非只读属性,则递归调用 reactive 向外返回 proxy 代理对象 if (isObject(res)) { return isReadonly ? readonly(res) : reactive(res) } return res } }