Vue3中的数据双向绑定原理主要得益于Proxy对象的使用,它提供了一种更清晰的方式来拦截和自定义对象的操作。在Vue3中,Proxy被用来替换Vue2中的Object.defineProperty方法,使得数据绑定更加高效和强大。Proxy对象允许开发者定义基本操作的自定义行为,如属性查找、赋值、枚举、函数调用等。它类似于一个拦截器,可以在访问对象属性时进行拦截,并可以自定义这些行为。
reactive
API:Vue 3 中的 reactive
函数用于创建响应式对象。它接受一个普通对象作为参数,并返回该对象的响应式代理。在内部,reactive
使用了 ES6 的 Proxy
对象来监听对象的访问和修改,从而实现数据的响应式更新。
import { reactive } from 'vue';
const state = reactive({
count: 0
});
-
Proxy
对象:reactive
函数内部使用了Proxy
对象来实现数据的拦截和监听。当访问响应式对象的属性时,Proxy
会触发 getter 函数,从而建立依赖关系;当修改响应式对象的属性时,Proxy
会触发 setter 函数,从而通知相关依赖进行更新。 -
依赖追踪:当访问响应式对象的属性时,Vue 3 会通过
reactive
和Proxy
内部的机制进行依赖追踪。Vue 会记录当前组件对响应式对象属性的访问,并建立响应式依赖关系。 -
派发更新:当响应式对象的属性发生变化时,
Proxy
会触发 setter 函数,并通知相关依赖进行更新。Vue 3 通过派发更新的方式,触发组件的重新渲染,从而实现了数据的双向绑定。
源码解析:
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,
mutableCollectionHandlers,
reactiveMap,
)
}
reactive
内部调用了createReactiveObject
函数,并传入mutableHandlers
作为Proxy的handler。
createReactiveObject()
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>,
) {
if (!isObject(target)) {
if (__DEV__) {
warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target already has corresponding Proxy
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only specific value types can be observed.
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
)
proxyMap.set(target, proxy)
return proxy
}
getTargetType函数
function getTargetType(value: Target) {
return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
? TargetType.INVALID
: targetTypeMap(toRawType(value))
}
Object.isExtensible()
静态方法判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)。
toRawType
函数的作用是获取给定值的原始类型。这个函数通常用于内部实现,用于确定值的类型,以便在响应式系统中做出相应的处理。
TargetType:
enum TargetType {
INVALID = 0,
COMMON = 1,
COLLECTION = 2,
}
Common
:普通对象,可以被转换为响应式对象。Collection
:集合对象,例如Map
、Set
和WeakMap
等,需要特殊处理以确保其内部的每个属性也能够被响应式地跟踪。Invalid
:无效的对象,不应该被转换为响应式对象。
export enum ReactiveFlags {
SKIP = '__v_skip',
IS_REACTIVE = '__v_isReactive',
IS_READONLY = '__v_isReadonly',
IS_SHALLOW = '__v_isShallow',
RAW = '__v_raw',
}
-
SKIP
: 用于跳过对当前属性的依赖追踪。在某些情况下,例如遍历对象属性时,可能需要跳过对特定属性的依赖追踪,以避免不必要的性能开销。 -
IS_REACTIVE
: 表示当前对象是否已经是一个响应式对象。如果该标志位被设置,表示当前对象已经被转换为了响应式对象。 -
IS_READONLY
: 表示当前对象是否是只读的。当一个响应式对象被标记为只读时,任何尝试修改其属性的操作都会被阻止,以确保对象的不可变性。 -
IS_SHALLOW
: 表示当前对象是否是浅层响应式的。在 Vue 3 中,可以选择将对象转换为浅层响应式对象,即只对对象的顶层属性进行响应式转换,而不会递归地将其内部所有属性都转换为响应式。 -
RAW
: 表示当前对象是否是一个原始对象。原始对象是指普通的非响应式对象,它们没有被转换为响应式对象。
targetTypeMap函数:
function targetTypeMap(rawType: string) {
switch (rawType) {
case 'Object':
case 'Array':
return TargetType.COMMON
case 'Map':
case 'Set':
case 'WeakMap':
case 'WeakSet':
return TargetType.COLLECTION
default:
return TargetType.INVALID
}
}
toRawType
函数的作用是获取给定值的原始类型。这个函数通常用于内部实现,用于确定值的类型,以便在响应式系统中做出相应的处理。
export const objectToString = Object.prototype.toString
export const toTypeString = (value: unknown): string =>
objectToString.call(value)
export const toRawType = (value: unknown): string => {
// extract "RawType" from strings like "[object RawType]"
return toTypeString(value).slice(8, -1)
}
通过一系列的判断确定使用哪一个handler
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
baseHandlers
用于处理普通的对象,例如普通的 JavaScript 对象(Plain Object)和数组(Array)等。collectionHandlers
则用于处理集合对象,包括Map
、Set
、WeakMap
和WeakSet
等。
接下来我们看一下mutableHandlers方法做了什么事情
export const mutableHandlers: ProxyHandler<object> =
/*#__PURE__*/ new MutableReactiveHandler()
MutableReactiveHandler类
这个类里面主要就是trigger方法 用于数据更新时触发相应的方法
class MutableReactiveHandler extends BaseReactiveHandler {
constructor(isShallow = false) {
super(false, isShallow)
}
set(
target: object,
key: string | symbol,
value: unknown,
receiver: object,
): boolean {
let oldValue = (target as any)[key]
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
}
}
具体来说,hadKey逻辑如下:
- 首先,使用
isArray
函数检查target
是否为数组,同时使用isIntegerKey
函数检查key
是否为整数类型的键。 - 如果
target
是数组且key
是整数类型的键,并且key
的值小于target
的长度(即target.length
),则返回true
。 - 否则,使用
hasOwn
函数检查target
是否具有自己的属性key
,如果是,则返回true
,否则返回false
。
判断时set属性还是add属性。
MutableReactiveHandler继承与BaseReactiveHandler
这个类里面主要就是track方法 用于收集依赖
class BaseReactiveHandler implements ProxyHandler<Target> {
constructor(
protected readonly _isReadonly = false,
protected readonly _isShallow = false,
) {}
get(target: Target, key: string | symbol, receiver: object) {
const isReadonly = this._isReadonly,
isShallow = this._isShallow
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (key === ReactiveFlags.IS_SHALLOW) {
return isShallow
} else if (key === ReactiveFlags.RAW) {
if (
receiver ===
(isReadonly
? isShallow
? shallowReadonlyMap
: readonlyMap
: isShallow
? shallowReactiveMap
: reactiveMap
).get(target) ||
// receiver is not the reactive proxy, but has the same prototype
// this means the reciever is a user proxy of the reactive proxy
Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver)
) {
return target
}
// early return undefined
return
}
const targetIsArray = isArray(target)
if (!isReadonly) {
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
if (key === 'hasOwnProperty') {
return hasOwnProperty
}
}
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 (isShallow) {
return res
}
if (isRef(res)) {
// ref unwrapping - skip unwrap for Array + integer key.
return targetIsArray && isIntegerKey(key) ? res : res.value
}
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
我们主要看trigger方法:
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>,
) {
if (key !== void 0) {
deps.push(depsMap.get(key))
}
/**首先,通过条件判断 key !== void 0 来确保键 key 不是未定义或未初始化的值。这是为了避免将未定义的键传递给 depsMap.get(key),从而避免出现错误。
如果键 key 是有效的,则调用 depsMap.get(key) 方法从 depsMap 中获取与该键相关联的依赖。
将获取到的依赖添加到 deps 数组中。deps 数组通常用于存储与某个特定属性或键相关联的所有依赖,以便在属性发生变化时可以通知到这些依赖进行更新。**/
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// new index added to array -> length changes
deps.push(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
deps.push(depsMap.get(ITERATE_KEY))
}
break
}
pauseScheduling()
for (const dep of deps) {
if (dep) {
triggerEffects(
dep,
DirtyLevels.Dirty,
__DEV__
? {
target,
type,
key,
newValue,
oldValue,
oldTarget,
}
: void 0,
)
}
}
resetScheduling()
}
-
在
ADD
操作类型下:- 如果目标对象
target
不是数组,则将与迭代键(ITERATE_KEY
)相关联的依赖添加到deps
数组中。 - 如果目标对象
target
是 Map,则除了添加与迭代键相关联的依赖外,还会添加与 Map 迭代键(MAP_KEY_ITERATE_KEY
)相关联的依赖。 - 如果目标对象
target
是数组,并且添加的键key
是整数类型的键,则将与数组长度('length')相关联的依赖添加到deps
数组中,因为数组长度发生了变化。
- 如果目标对象
-
在
DELETE
操作类型下:- 类似于
ADD
操作类型,如果目标对象target
不是数组,则将与迭代键(ITERATE_KEY
)相关联的依赖添加到deps
数组中。 - 如果目标对象
target
是 Map,则除了添加与迭代键相关联的依赖外,还会添加与 Map 迭代键(MAP_KEY_ITERATE_KEY
)相关联的依赖。
- 类似于
-
在
SET
操作类型下:- 如果目标对象
target
是 Map,则将与迭代键(ITERATE_KEY
)相关联的依赖添加到deps
数组中。
- 如果目标对象
for (const dep of deps) {}这段代码主要用于循环遍历依赖数组 deps
,并对每个依赖执行触发效果。具体来说,它的作用如下:
- 使用
for...of
循环遍历deps
数组中的每个依赖dep
。 - 对于每个非空的依赖
dep
,调用triggerEffects
函数触发效果。 triggerEffects
函数用于触发指定依赖的效果,将其标记为脏(dirty),以便稍后重新计算或更新。它接受三个参数:依赖对象dep
、脏状态的级别DirtyLevels.Dirty
,以及可选的其他参数对象(用于调试时传递额外的信息)。- 在开发环境下,会传递包含目标对象
target
、操作类型type
、键key
、新值newValue
、旧值oldValue
和旧目标对象oldTarget
等信息的参数对象,以便进行调试和跟踪。
pauseScheduling
: 这个方法用于暂停 Vue 3 内部的调度器,阻止组件的更新。一旦调用了 pauseScheduling
,Vue 将停止调度任务的执行,直到你调用 resetScheduling
方法来恢复调度器的工作。
resetScheduling
: 这个方法用于重新启动 Vue 3 内部的调度器,恢复组件更新的调度。当调用 resetScheduling
后,Vue 3 将继续按照正常的调度机制来更新组件。
这两个方法有兴趣的可以自行查看一下源码。
track方法:收集依赖 有兴趣可以自己研究一下。
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()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep(() => depsMap!.delete(key))))
}
trackEffect(
activeEffect,
dep,
__DEV__
? {
target,
type,
key,
}
: void 0,
)
}
}