共计 19662 个字符,预计需要花费 50 分钟才能阅读完成。
前言
<< 舒适揭示 >>
本文内容偏干,倡议边喝水边食用,如有不适请及时点赞!
【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 } }
set
捕捉器除去额定的边界解决,其实外围还是 更新属性值,并通过
trigger(...)
触发依赖更新function createSetter(shallow = false) { return function set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean { // 保留旧的数据 let oldValue = (target as any)[key] // 若原数据值属于 只读 且 ref 类型,并且新数据值不属于 ref 类型,则意味着批改失败 if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {return false} if (!shallow && !isReadonly(value)) {if (!isShallow(value)) {value = toRaw(value) oldValue = toRaw(oldValue) } if (!isArray(target) && isRef(oldValue) && !isRef(value)) { oldValue.value = value return true } } else {// in shallow mode, objects are set as-is regardless of reactive or not} // 是否存在对应的 key const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key) // 设置对应值 const result = Reflect.set(target, key, value, receiver) // 若指标对象是原始原型链上的内容(非自定义增加),则不触发依赖更新 if (target === toRaw(receiver)) {if (!hadKey) { // 指标对象不存在对应的 key,则为新增操作 trigger(target, TriggerOpTypes.ADD, key, value) } else if (hasChanged(value, oldValue)) { // 指标对象存在对应的值,则为批改操作 trigger(target, TriggerOpTypes.SET, key, value, oldValue) } } // 返回批改后果 return result } }
- 若以后属性值属于 只读的,则通过
deleteProperty
& has
& ownKeys
捕捉器
这三个捕捉器内容十分简洁,其中 has
和 ownKeys
实质也属于 读取操作,因而须要通过 track()
进行依赖收集,而 deleteProperty
相当于批改操作,因而须要 trigger()
触发更新
function deleteProperty(target: object, key: string | symbol): boolean {const hadKey = hasOwn(target, key)
const oldValue = (target as any)[key]
const result = Reflect.deleteProperty(target, key)
// 指标对象上存在对应的 key,并且能胜利删除,才会触发依赖更新
if (result && hadKey) {trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
function has(target: object, key: string | symbol): boolean {const result = Reflect.has(target, key)
if (!isSymbol(key) || !builtInSymbols.has(key)) {track(target, TrackOpTypes.HAS, key)
}
return result
}
function ownKeys(target: object): (string | symbol)[] {track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
return Reflect.ownKeys(target)
}
数组类型捕捉器 —— arrayInstrumentations
数组类型 和 对象类型 的大部分操作是能够共用的,比方 obj.name
和 arr[index]
等,但数组类型的操作还是会比对象类型更丰盛一些,而这些就须要非凡解决。
源码地位:
packages\reactivity\src\collectionHandlers.ts
解决数组索引 index
和 length
数组的 index
和 length
是会相互影响的,比方存在数组 const arr = [1]
:
arr[1] = 2
的操作会隐式批改length
的属性值arr.length = 0
的操作会导致原索引位的值产生变更
为了可能正当触发和 length
相干副作用函数的执行,在 set()
捕捉器中会判断以后操作的类型:
- 当
Number(key) < target.length
证实是批改操作,对应TriggerOpTypes.SET
类型,即以后操作不会扭转length
的值,不须要 触发和length
相干副作用函数的执行 - 当
Number(key) >= target.length
证实是新增操作,TriggerOpTypes.ADD
类型,即以后操作会扭转length
的值,须要 触发和length
相干副作用函数的执行
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
省略其余代码
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {if (!hadKey) {trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
解决数组的查找办法
数组的查找办法包含 includes
、indexOf
、lastIndexOf
,这些办法通常状况下是可能按预期进行工作,但还是须要对某些非凡状况进行解决:
-
当查找的指标数据是响应式数据自身时,失去的就不是预期后果
const obj = {} const proxy = reactive([obj]) console.log(proxy.includs(proxy[0])) // false
- 【产生起因 】首先这里波及到了两次读取操作, 第一次 是
proxy[0]
此时会触发get
捕捉器并为obj
生成对应代理对象并返回, 第二次 是proxy.includs()
的调用,它会遍历数组的每个元素,即会触发get
捕捉器,并又生成一个新的代理对象并返回,而这两次生成的代理对象不是同一个,因而返回false
- 【解决方案】源码中会在
get
中设置一个名为proxyMap
的WeakMap
汇合用于存储每个响应式对象,在触发get
时优先返回proxyMap
存在的响应式对象,这样不论触发多少次get
都能返回雷同的响应式数据
- 【产生起因 】首先这里波及到了两次读取操作, 第一次 是
-
当在响应式对象中查找原始数据时,失去的就不是预期后果
const obj = {} const proxy = reactive([obj]) console.log(proxy.includs(obj)) // false
- 【产生起因 】
proxy.includes()
会触发get
捕捉器并为obj
生成对应代理对象并返回,而includes
办法的参数传递的是 原始数据 ,相当于此时是 响应式对象 和 原始数据对象 进行比拟,因而对应的后果肯定是为false
-
【解决方案 】外围就是将它们的数据类型对立,即对立都应用 原始值数据比照 或 响应式数据比照,因为
includes()
的办法自身并不反对对传入参数或外部响应式数据的解决,因而须要自定义以上对应的数组查找办法- 在 重写 / 加强 的
includes
、indexOf
、lastIndexOf
等办法中,会将以后办法外部拜访到的响应式数据转换为原始数据,而后调用数组对应的原始办法进行查找,若查找后果为true
则间接返回后果 - 若以上操作没有查找到,则通过将以后办法传入的参数转换为原始数据,在调用数组的原始办法,此时间接将对应的后果向外进行返回
- 在 重写 / 加强 的
- 【产生起因 】
源码地位:packages\reactivity\src\baseHandlers.ts
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
// 内部调用上述办法,默认其内的 this 指向的是代理数组对象,// 但实际上是须要通过原始数组中进行遍历查找
const arr = toRaw(this) as any
for (let i = 0, l = this.length; i < l; i++) {track(arr, TrackOpTypes.GET, i + '')
}
// we run the method using the original args first (which may be reactive)
const res = arr[key](...args)
if (res === -1 || res === false) {
// if that didn't work, run it again using raw values.
return arr[key](...args.map(toRaw))
} else {return res}
}
})
解决数组影响 length
的办法
隐式批改数组长度的原型办法包含 push
、pop
、shift
、unshift
、splice
等,在调用这些办法的同时会间接的读取数组的 length
属性,又因为这些办法具备批改数组长度的能力,即相当于 length
的设置操作,若不进行非凡解决,会导致与 length
属性相干的副作用函数被反复执行,即 栈溢出,比方:
const proxy = reactive([])
// 第一个副作用函数
effect(() => {proxy.push(1) // 读取 + 设置 操作
})
// 第二个副作用函数
effect(() => {proxy.push(2) // 读取 + 设置 操作(此时进行 trigger 时,会触发包含第一个副作用函数的内容,而后循环导致栈溢出)})
在源码中还是通过 重写 / 加强 上述对应数组办法的模式实现自定义的逻辑解决:
- 在调用真正的数组原型办法前,会通过设置
pauseTracking()
办法来禁止track
依赖收集 - 在调用数组原生办法后,在通过
resetTracking()
办法复原track
进行依赖收集 - 实际上以上的两个办法就是通过管制
shouldTrack
变量为true
或false
,使得在track
函数执行时是否要执行原来的依赖收集逻辑
源码地位:packages\reactivity\src\baseHandlers.ts
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {instrumentations[key] = function (this: unknown[], ...args: unknown[]) {pauseTracking()
const res = (toRaw(this) as any)[key].apply(this, args)
resetTracking()
return res
}
})
汇合类型的捕捉器 — mutableCollectionHandlers
汇合类型 包含 Map
、WeakMap
、Set
、WeakSet
等,而对 汇合类型 的 代理模式 和 对象类型 须要有所不同,因为 汇合类型 和 对象类型 的操作方法是不同的,比方:
Map
类型 的原型 属性 和 办法 如下, 详情可见:
- size
- clear()
- delete(key)
- has(key)
- get(key)
- set(key)
- keys()
- values()
- entries()
- forEach(cb)
Set
类型 的原型 属性 和 办法 如下, 详情可见:
- size
- add(value)
- clear()
- delete(value)
- has(value)
- keys()
- values()
- entries()
-
forEach(cb)
源码地位:
packages\reactivity\src\collectionHandlers.ts
解决 代理对象
无法访问 汇合类型
对应的 属性
和 办法
代理汇合类型的第一个问题,就是代理对象没法获取到汇合类型的属性和办法,比方:
从报错信息能够看出 size
属性是一个拜访器属性,所以它被作为办法调用了,而次要谬误起因就是在这个拜访器中的 this
指向的是 代理对象 ,在源码中就是通过为这些特定的 属性 和 办法 定义对应的 key 的 mutableInstrumentations 对象,并且在其对应的 属性 和 办法 中将 this
指向为 原对象.
function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {const target = (this as any)[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
if (key !== rawKey) {!isReadonly && track(rawTarget, TrackOpTypes.HAS, key)
}
!isReadonly && track(rawTarget, TrackOpTypes.HAS, rawKey)
return key === rawKey
? target.has(key)
: target.has(key) || target.has(rawKey)
}
function size(target: IterableCollections, isReadonly = false) {target = (target as any)[ReactiveFlags.RAW]
!isReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.get(target, 'size', target)
}
省略其余代码
解决汇合类型的响应式
汇合建设响应式外围还是 track
和 trigger
,转而思考的问题就变成,什么时候须要 track
、什么时候须要 trigger
:
track
机会:get()、get size()、has()、forEach()
trigger
机会:add()、set()、delete()、clear()
这里波及一些优化的内容,比方:
- 在
add()
中通过has()
判断以后增加的元素是否曾经存在于Set
汇合中时,若已存在就不须要进行trigger()
操作,因为Set
汇合自身的一个个性就是 去重 - 在
delete()
中通过has()
判断以后删除的元素或属性是否存在,若不存在就不须要进行trigger()
操作,因为此时的删除操作是 有效的
function createInstrumentations() {
const mutableInstrumentations: Record<string, Function> = {get(this: MapTypes, key: unknown) {// track
return get(this, key)
},
get size() {// track
return size(this as unknown as IterableCollections)
},
has,// track
add,// trigger
set,// trigger
delete: deleteEntry,// trigger
clear,// trigger
forEach: createForEach(false, false) // track
}
省略其余代码
}
防止净化原始数据
通过重写汇合类型的办法并手动指定其中的 this
指向为 原始对象 的形式,解决 代理对象 无法访问 汇合类型 对应的 属性 和 办法 的问题,但这样的实现形式也带来了另一个问题: 原始数据被净化
。
简略来说,咱们只心愿 代理对象(响应式对象
) 才具备 依赖收集 (track
) 和 依赖更新 (trigger
) 的能力,而通过 原始数据 进行的操作不应该具备响应式的能力。
如果只是单纯的把所有操作间接作用到 原始对象 上就不能保障这个后果,比方:
// 原数数据 originalData1
const originalData1 = new Map({});
// 代理对象 proxyData1
const proxyData1 = reactive(originalData1);
// 另一个代理对象 proxyData2
const proxyData2 = reactive(new Map({}));
// 将 proxyData2 做为 proxyData1 一个键值
//【留神】此时的 set() 通过重写,其外部 this 曾经指向 原始对象(originalData1),等价于 原始对象 originalData1 上存储了一个 响应式对象 proxyData2
proxyData1.set("proxyData2", proxyData2);
// 若不做额定解决,如下基于 原始数据的操作 就会触发 track 和 trigger
originalData1.get("proxyData2").set("name", "zs");
在源码中的解决方案也是很简略,间接通过 value = toRaw(value)
获取以后设置值对应的 原始数据 ,这样旧能够防止 响应式数据对原始数据的净化。
解决 forEach
回调参数
首先 Map.prototype.forEach(callbackFn [, thisArg])
其中 callbackFn
回调函数会接管三个参数:
- 以后的 值
value
- 以后的 键
key
- 正在被遍历的
Map
对象(原始对象)
遍历操作 等价于 读取操作 ,在解决 一般对象 的 get()
捕捉器中有一个解决,如果以后拜访的属性值是 对象类型 那么就会向外返回其对应的 代理对象 ,目标是实现 惰性响应 和 深层响应 ,这个解决也同样实用于 汇合类型。
因而,在源码中通过 callback.call(thisArg, wrap(value), wrap(key), observed)
的形式将 Map
类型的 键 和 值 进行响应式解决,以及进行 track
操作,因为 Map
类型关注的就是 键 和 值。
function createForEach(isReadonly: boolean, isShallow: boolean) {
return function forEach(
this: IterableCollections,
callback: Function,
thisArg?: unknown
) {
const observed = this as any
const target = observed[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
!isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
return target.forEach((value: unknown, key: unknown) => {
// important: make sure the callback is
// 1. invoked with the reactive map as `this` and 3rd arg
// 2. the value received should be a corresponding reactive/readonly.
return callback.call(thisArg, wrap(value), wrap(key), observed)
})
}
解决迭代器
汇合类型的迭代器办法:
entries()
keys()
values()
Map
和 Set
都实现了 可迭代协定 (即 Symbol.iterator
办法,而 迭代器协定 是指 一个对象实现了 next
办法),因而它们还能够通过 for...of
的形式进行遍历。
依据对 forEach
的解决,不难晓得波及遍历的办法,究竟还是得将其对应的遍历的 键、值 进行响应式包裹的解决,以及进行 track
操作,而本来的的迭代器办法没方法实现,因而须要外部自定义迭代器协定。
const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
iteratorMethods.forEach(method => {mutableInstrumentations[method as string] = createIterableMethod(
method,
false,
false
)
省略其余代码
})
这一部分的源码波及的内容比拟多,以上只是简略的总结一下,更具体的内容可查看对应的源码内容。
ref 函数 — 原始值的响应式
原始值指的是 Boolean、Number、BigInt、String、Symbol、undefined、null
等类型的值,咱们晓得用 Object.defineProperty
必定是不反对,因为它拦挡的就是对象属性的操作,都说 Proxy
比 Object.defineProperty
强,那么它能不能间接反对呢?
间接反对是必定不能的,别忘了 Proxy
代理的指标也还是对象类型呀,它的强是在本人的所属畛域,跨畛域也是遭不住的。
因而在 Vue3
的 ref
函数中对原始值的解决形式是通过为 原始值类型 提供一个通过 new RefImpl(rawValue, shallow)
实例化失去的 包裹对象 ,说白了还是将原始值类型变成对象类型,但 ref
函数的参数并 不限度数据类型:
- 原始值类型 ,
ref
函数中会为原始值类型数据创立RefImpl
实例对象(必须通过.value
的形式拜访数据),并且实现自定义的get、set
用于别离进行 依赖收集 和 依赖更新 ,留神的是这里并不会通过Proxy
为原始值类型创立代理对象,精确的说在RefImpl
外部自定义实现的get、set
就实现了对原始值类型的拦挡操作,因为原始值类型不须要向对象类型设置那么多的捕捉器
-
对象类型 ,
ref
函数中除了为 对象类型 数据创立RefImpl
实例对象之外,还会通过reactive
函数将其转换为响应式数据,其实次要还是为了反对相似如下的操作const refProxy = ref({name: 'zs'}) refProxy.value.name = 'ls'
- 依赖容器 dep,在
ref
类型中依赖存储的地位就是每个ref
实例对象上的dep
属性,它实质就是一个Set
实例,触发get
时往dep
中增加副作用函数(依赖),触发set
时从dep
中顺次取出副作用函数执行
源码地位:packages\reactivity\src\ref.ts
export function ref(value?: unknown) {return createRef(value, false)
}
function createRef(rawValue: unknown, shallow: boolean) {if (isRef(rawValue)) {return rawValue}
return new RefImpl(rawValue, shallow)
}
class RefImpl<T> {
private _value: T
private _rawValue: T
public dep?: Dep = undefined
public readonly __v_isRef = true
constructor(value: T, public readonly __v_isShallow: boolean) {this._rawValue = __v_isShallow ? value : toRaw(value)
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
// 将依赖收集到 dep 中,实际上就是一个 Set 类型
trackRefValue(this)
return this._value
}
set value(newVal) {
// 获取原始数据
newVal = this.__v_isShallow ? newVal : toRaw(newVal)
// 通过 Object.is(value, oldValue) 判断新旧值是否统一,若不统一才须要进行更新
if (hasChanged(newVal, this._rawValue)) {
// 保留原始值
this._rawValue = newVal
// 更新为新的 value 值
this._value = this.__v_isShallow ? newVal : toReactive(newVal)
// 依赖更新,从 dep 中取出对应的 effect 函数顺次遍历执行
triggerRefValue(this, newVal)
}
}
}
// 若以后 value 是 对象类型,才会通过 reactive 转换为响应式数据
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value
Vue3 如何进行依赖收集?
在 Vue2
中依赖的收集形式是通过 Dep
和 Watcher
的 观察者模式 来实现的,是不是还能想起首次理解 Dep
和 Watcher
之间的这种 剪一直理还乱
的关系时的情绪 ……
对于 设计模式 局部感兴趣可查看 常见 JavaScript 设计模式 — 原来这么简略 一文,外面次要围绕着
Vue
中对应的设计模式来进行介绍,置信会有肯定的帮忙
依赖收集 其实说的就是 track
函数须要解决的内容:
-
申明
targetMap
作为一个容器,用于保留和以后响应式对象相干的依赖内容,自身是一个WeakMap
类型- 抉择
WeakMap
类型作为容器,是因为WeakMap
对 键 (对象类型)的援用是 弱类型 的,一旦内部没有对该 键(对象类型)放弃援用时,WeakMap
就会主动将其删除,即 可能保障该对象可能失常被垃圾回收 - 而
Map
类型对 键 的援用则是 强援用,即使内部没有对该对象放弃援用,但至多还存在Map
自身对该对象的援用关系,因而会导致该对象不能及时的被垃圾回收
- 抉择
- 将对应的 响应式数据对象 作为
targetMap
的 键,存储和以后响应式数据对象相干的依赖关系depsMap
(属于Map
实例),即depsMap
存储的就是和以后响应式对象的每一个key
对应的具体依赖 - 将
deps
(属于Set
实例)作为depsMap
每个key
对应的依赖汇合,因为每个响应式数据可能在多个副作用函数中被应用,并且Set
类型用于主动去重的能力
可视化构造如下:
源码地位:packages\reactivity\src\effect.ts
const targetMap = new WeakMap<any, KeyToDepMap>()
export function track(target: object, type: TrackOpTypes, key: unknown) {
// 以后应该进行依赖收集 且 有对应的副作用函数时,才会进行依赖收集
if (shouldTrack && activeEffect) {
// 从容器中取出【对应响应式数据对象】的依赖关系
let depsMap = targetMap.get(target)
if (!depsMap) {
// 若不存在,则进行初始化
targetMap.set(target, (depsMap = new Map()))
}
// 获取和对【应响应式数据对象 key】相匹配的依赖
let dep = depsMap.get(key)
if (!dep) {
// 若不存在,则进行初始化 dep 为 Set 实例
depsMap.set(key, (dep = createDep()))
}
const eventInfo = __DEV__
? {effect: activeEffect, target, type, key}
: undefined
// 往 dep 汇合中增加 effect 依赖
trackEffects(dep, eventInfo)
}
}
export const createDep = (effects?: ReactiveEffect[]): Dep => {const dep = new Set<ReactiveEffect>(effects) as Dep
dep.w = 0
dep.n = 0
return dep
}
最初
以上就是针对 Vue3
中对不同数据类型的解决的内容,无论是 Vue2
还是 Vue3
响应式的外围都是 数据劫持 / 代理、依赖收集、依赖更新,只不过因为实现数据劫持形式的差别从而导致具体实现的差别,在 Vue3
中值得注意的是:
- 一般对象类型 能够间接配合
Proxy
提供的捕捉器实现响应式 - 数组类型 也能够间接复用大部分和 一般对象类型 的捕捉器,但其对应的查找办法和隐式批改
length
的办法依然须要被 重写 / 加强 - 为了反对 汇合类型 的响应式,也对其对应的办法进行了 重写 / 加强
-
原始值数据类型 次要通过
ref
函数来进行响应式解决,不过内容不会对 原始值类型 应用reactive(或 Proxy)
函数来解决,而是在外部自定义get value(){}
和set value(){}
的形式实现响应式,毕竟原始值类型的操作无非就是 读取 或 设置 ,外围还是将 原始值类型 转变为了 一般对象类型ref
函数可实现原始值类型转换为 响应式数据,但ref
接管的值类型并没只限定为原始值类型,若接管到的是援用类型,还是会将其通过reactive
函数的形式转换为响应式数据
肝了近 1W
字的内容,也是目前写得字数最多的文章,人有点麻了 … 哈哈,心愿对大家有所帮忙!!!