注:本文使用vue版本为3.3.4
我们知道Vue渲染流程是基于effect函数的依赖变动,从而不断触发patch
,保持页面为数据的最新渲染,那么我们这次来了解一下他的依赖是怎么建立起来的。
或者说vue
的响应式是什么。众所周知,vue的响应式数据包括ref
, reactive
, computed
等,我们一个一个来看。
ref、shallowRef
ref
方法和shallowRef
都可以创建一个响应式数据,这个响应式数据的value
就是我们使用的数据,对这个value
进行修改都会触发响应式逻辑。
他们之中不同点是
ref
可以进行深层次转换shallowRef
不能进行深层次的转换,这里需要注意的是,value
本身也算一层。所以,对value
整体的赋值,才会触发shallowRef
的响应式。
我们直接看看他们的源码。
typescript
export function ref(value?: unknown) {
return createRef(value, false)
}
export function shallowRef(value?: unknown) {
return createRef(value, true)
}
可以看到,他们都调用了createRef
,不同的是第二个参数不同,可以推断出第二个参数就是控制shallow
的开关。
那么我们进入createRef
看一下
typescript
function createRef(rawValue: unknown, shallow: boolean) {
// 已经是响应式了,那么原值返回
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
可以看到,createRef
尽可能拦截多层响应式数据的ref
嵌套,如果已经是一个响应式数据,那么将原值返回,否则,进入new RefImpl
逻辑。显而易见,构造出来的对象就是那个包含value
的响应式对象。
typescript
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
) {
// 递归获取原始值,如果是shallowRef就不递归,直接返回传入值
this._rawValue = __v_isShallow ? value : toRaw(value)
// 没有value字段的响应式数据,如果是shallowRef,就不对他使用响应式处理
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
// 依赖收集,没有依赖会创建一个,后面会讲
trackRefValue(this)
return this._value
}
set value(newVal) {
// 是否对新值进行响应式
const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
newVal = useDirectValue ? newVal : toRaw(newVal)
//新旧值比较,不同才会赋值
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
// 触发依赖变更逻辑
triggerRefValue(this, newVal)
}
}
}
这里可以看到,我们平时常说的vue3
的响应式是基于proxy
是不完全正确的,准确说是对象基于proxy
,而非对象类型,是用的基于对象的get
和set
实现响应式,换句话说,是对vue2
的Object.defineProperty
的简化应用。
vue2
的Object.defineProperty
是需要遍历key
值,而vue3
是直接挂载到对象的value
字段下面,直接用set
和get
就实现了劫持。
在set
的时候,会对新值进行比较,如果不相同才会赋值,并且判断新值是否响应式,比如当前ref
本身是shallow
,或者新值是一个shallow
,或者新值是只读属性,那么不进行响应式化。
那么,这就是非对象类型,基于对象实现的响应式逻辑。
那么如果是对象类型呢?
换句话说,toReactive
的实现逻辑呢?
typescript
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value
简单明了,如果是对象类型,调用reactive
,也就是ref
的对象类型,是对reactive
的一层封装。
我们直接看看reactive
的源码。
reactive、shallowReactive
reactive
和shallowReactive
都可以创建响应式对象,区别同ref
一样,带有shallow
的只能创建浅层响应式数据。
typescript
export function reactive(target: object) {
// readonly原路返回
if (isReadonly(target)) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
export function shallowReadonly<T extends object>(target: T): Readonly<T> {
return createReactiveObject(
target,
true,
shallowReadonlyHandlers,
shallowReadonlyCollectionHandlers,
shallowReadonlyMap
)
}
看起来核心函数就是createReactiveObject
。而第二个参数,就是shallow
的开关。
typescript
function createReactiveObject(
target: Target, // 目标对象,需要被代理的对象
isReadonly: boolean, // 是否为只读
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any> //缓存
) {
// 如果目标对象不是一个对象,则无法创建响应式代理
if (!isObject(target)) {
return target
}
// 如果目标对象已经是一个代理对象,并且不是只读代理,直接返回它
// 例外:在一个响应式对象上调用readonly()方法
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// 如果目标对象已经有对应的代理对象,返回该代理对象
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 只有特定类型的值可以被观察
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
}
注释写的很详细,整个核心流程就是首先经过一系列判断,当前对象是否符合要求,包括了入参的类型、是否是响应式的、是否已经被定义过了,以及是否是符合要求的类型这些步骤,最后执行的是 new Proxy()
这样的一个响应式代理 ,在执行的时候,还需要根据类型挂载不同的handler
。
我们目光来到handler
上,调用createReactiveObject
的handler
出现了多个,我们分情况讨论下
reactive | shallowReactive | readonly | shallowReadonly | |
---|---|---|---|---|
baseHandlers | mutableHandlers | shallowReactiveHandlers | readonlyHandlers | shallowReadonlyHandlers |
collectionHandlers | mutableCollectionHandlers | shallowCollectionHandlers | readonlyCollectionHandlers | shallowReadonlyCollectionHandlers |
这里大家会产生疑问,现在不是讨论reactive
和shallowReactive
吗?怎么多出来readonly
、shallowReadonly
,实际上在解析createReactiveObject
的时候,我们注意到判断了isReadonly
,会判断入参是不是响应式数据。
也就是说readonly
和shallowReadonly
其实也是响应式数据,只是他们是只读的,或者只有根层数是只读的。他们都调用了createReactiveObject
,创建了一个代理对象,并挂载不同的代理行为。
baseHandlers
typescript
export const mutableHandlers: ProxyHandler<object> =
/*#__PURE__*/ new MutableReactiveHandler()
export const readonlyHandlers: ProxyHandler<object> =
/*#__PURE__*/ new ReadonlyReactiveHandler()
export const shallowReactiveHandlers = /*#__PURE__*/ new MutableReactiveHandler(
true
)
export const shallowReadonlyHandlers =
/*#__PURE__*/ new ReadonlyReactiveHandler(true)
看起来他们还是分开的,reactive
和shallowReactiveHandlers
是使用同一个类------MutableReactiveHandler
,而readonly
和shallowReadonly
使用同一个类------ReadonlyReactiveHandler
。
但他们都继承了同一个类BaseReactiveHandler
,我们直接看看BaseReactiveHandler
实现了什么功能。
get
typescript
class BaseReactiveHandler implements ProxyHandler<Target> {
constructor(
protected readonly _isReadonly = false, // 是否为只读
protected readonly _shallow = false // 是否为浅层
) {}
get(target: Target, key: string | symbol, receiver: object) {
const isReadonly = this._isReadonly,
shallow = this._shallow;
// 如果 key 是 ReactiveFlags 中的特殊标记,返回相应的值
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 是 RAW,且 receiver 是对应的 Map 中的代理对象,则返回原始目标对象
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target;
}
const targetIsArray = isArray(target);
// 如果不是只读,且 key 是数组内置方法或者 hasOwnProperty 方法,则返回相应的值
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); // 获取目标对象的属性值
// 如果 key 是 Symbol 类型或者是不可追踪的key,则直接返回属性值
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res;
}
// 如果不是只读,追踪目标对象的属性访问操作
if (!isReadonly) {
track(target, TrackOpTypes.GET, key);
}
// 如果是浅层,直接返回属性值
if (shallow) {
return res;
}
// 如果属性值是 ref 对象,且 key 是数组的整数索引,则返回 ref 对象的值,否则返回 ref 对象本身
if (isRef(res)) {
// ref unwrapping - 跳过对数组 + 整数键的拆包。
return targetIsArray && isIntegerKey(key) ? res : res.value;
}
// 如果属性值是对象,则将返回值也转换为代理对象。在这里进行 isObject 检查是为了避免无效值警告。
return isReadonly ? readonly(res) : reactive(res);
}
}
整个逻辑比较清晰,首先对 key
属于 ReactiveFlags
的部分做了特殊处理,从代码上看起来,vue3
并没有将ReactiveFlags
挂载到数据上,而是在get
上面做了劫持,这也是为什么可以在 createReactiveObject
函数中判断响应式对象是否存在 ReactiveFlags.RAW
属性,如果存在就返回这个响应式对象本身。
同时,也因为响应式对象扩展了各种key
, 让代理对象有了不同的表现逻辑。
然后如果target
是数组,数组用到了arrayInstrumentations
,逻辑是返回arrayInstrumentations
中对应的值,arrayInstrumentations
是什么?
arrayInstrumentations
是createArrayInstrumentations
创建来的,是他的返回值,我们看看createArrayInstrumentations
实现了什么逻辑。
typescript
const arrayInstrumentations = createArrayInstrumentations()
function createArrayInstrumentations() {
const instrumentations: Record<string, Function> = {};
// 为对这些方法进行响应化处理
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
const arr = toRaw(this) as any; // 获取原始的数组
for (let i = 0, l = this.length; i < l; i++) {
track(arr, TrackOpTypes.GET, i + ''); // 追踪数组元素的访问操作
}
// 首先使用原始参数(可能是响应式的)运行方法
const res = arr[key](...args);
if (res === -1 || res === false) {
// 如果失败,使用原始值再次运行方法
return arr[key](...args.map(toRaw));
} else {
return res;
}
};
});
// 处理修改数组长度的方法,避免追踪数组长度变化导致的无限循环问题
;(['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; // 返回方法的结果
};
});
return instrumentations; // 返回处理后的数组方法
}
也就是说,arrayInstrumentations
劫持了数组的target
,对上面的方法进行了包装,对于includes
、 indexOf
、lastIndexOf
这些查询方法,数组是可以进行依赖收集的,而针对push
、pop
、shift
、 unshift
、splice
,这些方法会修改数组的length
,但这样会在effect
中导致死循环。
具体来说,是在effect
中,有个逻辑是发现数组长度变化,就自增数组,但是数组的length
也是依赖收集的一环,结果自增数组又会触发这个effect
,导致了数组疯狂自增,可以看这个Issue。
按照之前的逻辑往下走,我们发现了收集依赖函数------track
,在这之前我们已经在ref
的get
中见到trackRefValue
了,现在补上之前留下的坑。
typescript
// 本质是还原ref获取原始值,然后调用trackEffects
export function trackRefValue(ref: RefBase<any>) {
if (shouldTrack && activeEffect) {
ref = toRaw(ref)
// 如果ref上有dep就用,没有就创建一个,dep是一个Set
trackEffects(ref.dep || (ref.dep = createDep()))
}
}
// 是否应该收集依赖
let shouldTrack = true
// 当前正在收集依赖的effect
let activeEffect
const targetMap = new WeakMap()
export function track(target: object, type: TrackOpTypes, key: unknown) {
// 如果需要追踪(即 shouldTrack 为 true)并且存在活跃的effect函数
if (shouldTrack && activeEffect) {
// 获取目标对象的依赖映射
let depsMap = targetMap.get(target)
// 如果不存在依赖映射,创建一个新的 Map 对象
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 获取属性的依赖集合
let dep = depsMap.get(key)
// 如果不存在依赖集合,创建一个新的依赖对象(dep)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
// 将当前effect函数追加到依赖集合中
trackEffects(dep)
}
}
export function trackEffects(
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// 用于判断是否应该追踪该依赖对象
let shouldTrack = false
// 如果效果函数的追踪深度小于等于最大标记位数
if (effectTrackDepth <= maxMarkerBits) {
// 如果该依赖对象尚未被新追踪,将其标记为新追踪
if (!newTracked(dep)) {
dep.n |= trackOpBit // 设置为新追踪
shouldTrack = !wasTracked(dep) // 判断是否应该追踪该依赖对象
}
} else {
// 只有没收集的依赖才能收集
shouldTrack = !dep.has(activeEffect!)
}
// 如果应该追踪该依赖对象
if (shouldTrack) {
// 将当前的effect函数添加到依赖对象中
dep.add(activeEffect!)
// 将依赖对象添加到当前的effect函数的依赖集合中
activeEffect!.deps.push(dep)
}
}
我们在这里补了前面挖的坑,trackRefValue
和track
其实都是触发trackEffects
的包装,这里有一个全局的targetMap
,他的key
就是target
,value
是depsMap
。
depsMap
的key
是target
的key
,而alue
就是dep
的集合,dep
就是副作用函数。这个类似Vue2
中Observer
的dep
。
在trackEffects
中,响应式数据(ref
,effect
,computed
等)的dep
会传入进来,供trackEffects
进行处理,里面会遇到新旧依赖的逻辑,这个之后会讲,在收集依赖的逻辑里面,会讲当前的activeEffect
视同add
加入dep
中,也就是收集到了依赖,明确说明一点是activeEffect
依赖当前正在trackEffects
处理的响应式数据,当这个响应式数据变化的时候,会触发dep
里面的当时推入的effect
的不同逻辑。
同时activeEffect
还有自己的逻辑,他本身还有deps
,同时把dep
推入自己的deps
数组里面。
也就是说,deps
的是当前effect
依赖的响应式数据所被依赖的合集。比如说
typescript
import {ref, computed} from 'vue'
const a = ref(1)
const b = computed(() => a.value + 1)
const c = computed(() => a.value + 2)
// 捕获依赖
b.value
c.value
此时,a
的dep
就是一个Set
里面有两个value
,分别是b
和c
的effect
。
而b
的effect
的deps
,此时数组只有一个元素,是长度为2
的Set
,value
分别的b
的effect
和c
的effect
。
c
同理。
我们回到BaseReactiveHandler
,处理完各种特殊情况的时候,逻辑来到了shallow
判断。
如果shallow
为true
,那么直接返回res
,也就是结果,为false
。
接下来处理了数组展开的情况。
然后关键的地方来了,如果是对象,只读的话递归只读逻辑,非只读返回reactive
处理后的。
我们来复习一下Proxy
,使用Proxy
代理对象的时候,天生只会代理第一层对象,也就是说Proxy
默认shallow
为true
,那么shallow
为false
的功能怎么实现呢?
那就是使用了get
,当get
到非根级对象的时候,会先把对象响应式化,再返回。
这样就实现了对象多级响应式化的功能。而非Vue2
在定义的时候就使用更加耗费性能的遍历递归。
也就是说,BaseReactiveHandler
起到了一个公共get
的作用,在get
使用lazy
的方式,对用到的数据进行响应式化,而set
等则在子类中实现。
set、deleteProperty...
我们看一下readonly
和shallowReadonly
使用的ReadonlyReactiveHandler
。
typescript
class ReadonlyReactiveHandler extends BaseReactiveHandler {
constructor(shallow = false) {
super(true, shallow)
}
set(target: object, key: string | symbol) {
return true
}
deleteProperty(target: object, key: string | symbol) {
return true
}
}
简单粗暴,就是继承了BaseReactiveHandler
,然后把set
和deleteProperty
屏蔽了,起到了readonly
的效果,唯一区别是shallow
的值不同。
isReadonly
默认是true
,在BaseReactiveHandler
的逻辑中,isReadonly
相关判断逻辑是在shallow
判断逻辑之后。
如果shallow
是true
,那么不进行递归响应式化,如果是false
,并且取到的值是对象,那么对取到的值进行递归readonly
化。
而递归的readonly
又把之前的流程走一遍:调用createReactiveObject
,构造代理对象,然后再次挂载readonlyHandlers
。
总结一下,ReadonlyReactiveHandler
通过屏蔽set
和deleteProperty
,实现只读的功能,通过对shallow
开关的控制,来决定是否对非根层级的对象节点实现响应式。
那么mutableHandlers
和shallowReactiveHandlers
使用的MutableReactiveHandler
呢?我们再次看看源码
typescript
class MutableReactiveHandler extends BaseReactiveHandler {
constructor(shallow = false) {
super(false, shallow);
}
set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
let oldValue = (target as any)[key]; // 获取目标对象的旧值
// 如果旧值是只读且是 ref 对象,且新值不是 ref 对象,直接返回false,禁止修改
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
return false;
}
if (!this._shallow) {
// 如果不是浅层代理,且新值和旧值都不是浅层响应式对象,将它们转换为原始值
if (!isShallow(value) && !isReadonly(value)) {
oldValue = toRaw(oldValue);
value = toRaw(value);
}
// 如果目标对象不是数组,旧值是 ref 对象,且新值不是 ref 对象,更新 ref 对象的值并返回true
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value;
return true;
}
} else {
// 在浅层代理模式下,无论是否是响应式对象,都直接将对象设置为新值
}
// 检查 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) {
// 触发trigger
trigger(target, TriggerOpTypes.ADD, key, value); // 触发添加操作
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue); // 触发更新操作
}
}
return result; // 返回操作结果
}
deleteProperty(target: object, key: string | symbol): boolean {
const hadKey = hasOwn(target, key); // 检查 key 是否存在于目标对象中
const oldValue = (target as any)[key]; // 获取旧值
const result = Reflect.deleteProperty(target, key); // 删除目标对象的属性
// 如果删除成功且 key 存在于目标对象中,触发删除操作
if (result && hadKey) {
// 触发trigger
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue);
}
return result; // 返回删除操作结果
}
has(target: object, key: string | symbol): boolean {
const result = Reflect.has(target, key); // 检查 key 是否存在于目标对象中
// 如果 key 不是 Symbol 类型或者不是内置 Symbol,则进行追踪
if (!isSymbol(key) || !builtInSymbols.has(key)) {
track(target, TrackOpTypes.HAS, key);
}
return result; // 返回检查结果
}
ownKeys(target: object): (string | symbol)[] {
// 触发trigger
track(
target,
TrackOpTypes.ITERATE,
isArray(target) ? 'length' : ITERATE_KEY
); // 追踪迭代操作
return Reflect.ownKeys(target);
}
除了set
以外,其他的操作实现原理都很简单,就是使用反射触发原有逻辑,然后触发一下trigger
,trigger
是什么之后会讲。
在set
中,如果旧值readonly
是ref
并且新值不是ref
的话,那么不允许赋值,如果非浅层,会通过toRaw
获取新值和旧值的原始值,如果target
非数组,旧值是ref
但是新值不是,新值就赋值给旧值的value
下,触发前文提到的ref
中set
的更新。
其他情况,通过Reflect.set
设置值,最后触发一下trigger
。
需要注意一下,这里通过判断了target
是否等于receiver
的原始值,如果相等,说明更改的非原型链上的值,才会正常触发trigger
,反之如果更改的原型链的的属性就不触发trigger
,因为如果这里触发就会触两次trigger
了------原型链上的先触发,这里再触发。
有人奇怪会是什么情况,这个情况确实不常见,但的确存在一种边界情况。我们引入一个例子
ini
const obj0 = {a:1}
const obj1 = reactive(obj0)
const obj2 = Object.create(obj1)
const obj3 = reactive(obj2)
obj3
或者obj2上
,并不存在a
属性,a
属性是存在于原型链上的。因此打印a
属性还是可以出来的。
那么当我尝试修改obj3
的a
属性,根据常识,我们并不可以修改原型链,因此我们实际结果是在obj3
或者obj2
上新建了一个a属性,并赋值为我们更改修改的值。
也就是说是set
会触发了两次,Reflect.set
的返回值两次都是true
------即使原型链的并没有被修改。
- 第一次
target
是原型链上的obj0
,receiver
是obj3
- 第二次
target
是obj2
,receiver
是obj3
target
天生跟代理对象,也就是receiver
不相等,但receiver
是由toRaw
包裹的,而toRaw
是递归获取代理对象的ReactiveFlags.RAW
,根据get
属性中的逻辑,ReactiveFlags.RAW
又被转到对象建立响应式时候的target
中。
也就是toRaw(receiver)
后,结果是obj2
。
也就是第二次的时候,会走进if
语句中。
在if
语句中,判断key
之前存不存在,存在的话就走修改trigger
,不存在就触发增加trigger
,判断是否存在使用的hasOwn
,就是Object.prototype.hasOwnProperty.call
的封装。
好了,说了那么多,我们来看看trigger
是什么东西
typescript
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
const depsMap = targetMap.get(target)
if (!depsMap) {
// 没有依赖列表,返回
return
}
let deps: (Dep | undefined)[] = []
if (type === TriggerOpTypes.CLEAR) {
// 触发这个对象所有的副作用
deps = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
// 如果是数组,触发类型是length 收集关于length的依赖以及index大于新值的所有依赖
const newLength = Number(newValue)
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= newLength) {
deps.push(dep)
}
})
} else {
// 触发key对应副作用
if (key !== void 0) {
deps.push(depsMap.get(key))
}
// 分情况触发对应副租用
switch (type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
// 新增且对象情况下,ITERATE_KEY也需要收集触发
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
// 新增且map情况下,MAP_KEY_ITERATE_KEY
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// 新增指定key的情况下,length也被依赖收集
deps.push(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
//删除情况下,非数组触发ITERATE_KEY
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
// 删除情况下 map 为MAP_KEY_ITERATE_KEY
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
// 如果是map,修改的话,触发ITERATE_KEY
if (isMap(target)) {
deps.push(depsMap.get(ITERATE_KEY))
}
break
}
}
// 如果依赖为1
if (deps.length === 1) {
// 第一个存在
if (deps[0]) {
// 直接触发
triggerEffects(deps[0])
}
} else {
const effects: ReactiveEffect[] = []
for (const dep of deps) {
if (dep) {
// 过滤无效值
effects.push(...dep)
}
}
// 使用createDep包装后,createDep实际是Set,再用triggerEffects触发
triggerEffects(createDep(effects))
}
}
trigger
就是依赖收集,因为修改了一个值后,并非只有当前值需要变动,相关数据也需要变动,而依赖那些数据的依赖也需要变动。
比如数组,当我push
数组的时候,数组变化,会触发依赖数组元素的依赖,那么对数组length
的依赖呢?也需要收集触发。
好消息是,push
等方法的确可以触发length
的set
和get
,map
等方法的确可以触发length
的get
,
javascript
const arr = []
const arr1 = new Proxy(arr, {
get(target, key,receiver) {
console.log('get', key)
return Reflect.get(...arguments)
},
set(target, key, value,receiver) {
console.log('set', key)
return Reflect.set(...arguments)
}
})
arr1.map(v => v)
// get map
// get length
// get constructor
arr1.push(0)
// get push
// get length
// set 0
// set length
可以看到,触发map方法会触发get
三次,分别是map
,length
,以及constructor
,正是因为这个访问,才收集到了length
的依赖,而我们push
数据的时候改变了length
,从而触发了对应的effect
。
那么接着发散一下,对象呢?对象也是可迭代数据,但没有length
啊,比如Object.keys
,回忆一下MutableReactiveHandler
之中,是不是有个ownKeys
,是的,他是收集key
的访问的,当使用对象迭代的时候,会触发这个拦截器。
这拦截器里面调用了track
,关于track
这个我们前面讲过了,就有依赖触发,没依赖就设置一个,然后触发。
那么我们接着看,最后的依赖会被triggerEffects
触发
typescript
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
//不是数组变成数组,注意变成数组的时候使用了rest,此时dep可能是Set,使用[...Set]会将Set内容转化成数组
const effects = isArray(dep) ? dep : [...dep]
// 先计算属性
for (const effect of effects) {
if (effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
// 再非计算属性
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
}
我们看到实际上是对dep
进行循环,如果dep不
是数组,那么可能是Set
,所以使用[...Set]
把他转成数组,循环的时候先计算属性,再非计算属性,这个状态提升我们之后再讲。循环的单个dep
进入triggerEffect
函数。
typescript
function triggerEffect(
effect: ReactiveEffect,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
if (effect !== activeEffect || effect.allowRecurse) {
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
这个函数也很简单,就是有scheduler
执行scheduler
,否则执行run
。当然这里这里也判断了effect
是不是正在触发,防止多次同时触发。而effect
是怎么来的,我们之后再说。
collectionHandlers
我们来看看collectionHandlers
,collectionHandlers
也分为四种
typescript
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: /*#__PURE__*/ createInstrumentationGetter(false, false)
}
export const shallowCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: /*#__PURE__*/ createInstrumentationGetter(false, true)
}
export const readonlyCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: /*#__PURE__*/ createInstrumentationGetter(true, false)
}
export const shallowReadonlyCollectionHandlers: ProxyHandler<CollectionTypes> =
{
get: /*#__PURE__*/ createInstrumentationGetter(true, true)
}
但这里就开始奇怪了,虽然他们都是使用同一个函数,怎么只有get
。
难道没有set
吗?
肯定不是,他们的实现逻辑同Vue2
的数组类似,是通过拦截对应的方法,来实现响应式的。同时为了性能提升,并没有Vue2
定义数据的时候,进行数据劫持,而是在访问的时候,也就是触发get
的时候,进行数据劫持,这里的get
并非map
的get
,而是代理的get
。
我们看看createInstrumentationGetter
是怎么实现的。
typescript
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
// isReadonly shallow 两两组合出四个代理target
const instrumentations = shallow
? isReadonly
? shallowReadonlyInstrumentations
: shallowInstrumentations
: isReadonly
? readonlyInstrumentations
: mutableInstrumentations
return (
target: CollectionTypes,
key: string | symbol,
receiver: CollectionTypes
) => {
// 处理ReactiveFlags,不挂载具体数据,通过get实现
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (key === ReactiveFlags.RAW) {
return target
}
return Reflect.get(
// 访问的key是不是在当前属性或者原型链上,是的话劫持,不是的话使用原target
hasOwn(instrumentations, key) && key in target
? instrumentations
: target,
key,
receiver
)
}
}
由于Proxy
限制,拦截map
等数据结构不是很轻松,但依然有办法,当访问map.set
或者map.get
或者map.size
等内置方法的时候,一定会触发代理的get
,这个时候通过isReadonly
和shallow
,组合出合适的target
,来替换之前的target
,因为receiver
存在,所以保证了this
的指向是没问题,也可以通过this
来获取原始数据,进行依赖收集。
instrumentations
是createInstrumentations
方法创建的,我们看看createInstrumentations
的逻辑。
typescript
function createInstrumentations() {
const mutableInstrumentations: Record<string, Function | number> = {
get(this: MapTypes, key: unknown) {
//依赖收集,如果是对象,响应化
return get(this, key)
},
get size() {
return size(this as unknown as IterableCollections)
},
has,
add,
set,
delete: deleteEntry,
clear,
// 依赖收集,如果是对象,响应化
forEach: createForEach(false, false)
}
const shallowInstrumentations: Record<string, Function | number> = {
get(this: MapTypes, key: unknown) {
// 依赖收集,不对获取到的数据响应式化
return get(this, key, false, true)
},
get size() {
return size(this as unknown as IterableCollections)
},
has,
add,
set,
delete: deleteEntry,
clear,
// 依赖收集,不对获取到的数据响应式化
forEach: createForEach(false, true)
}
const readonlyInstrumentations: Record<string, Function | number> = {
// 依赖收集,如果是对象,只读化
get(this: MapTypes, key: unknown) {
// readonly是true,不用get收集依赖
return get(this, key, true)
},
get size() {
// readonly是true,不用size收集依赖
return size(this as unknown as IterableCollections, true)
},
// readonly是true,不用has收集依赖
has(this: MapTypes, key: unknown) {
return has.call(this, key, true)
},
add: createReadonlyMethod(TriggerOpTypes.ADD),
set: createReadonlyMethod(TriggerOpTypes.SET),
delete: createReadonlyMethod(TriggerOpTypes.DELETE),
clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
// 依赖收集,如果是对象,响应化
forEach: createForEach(true, false)
}
const shallowReadonlyInstrumentations: Record<string, Function | number> = {
// 不收集依赖,不对数据响应式化
get(this: MapTypes, key: unknown) {
return get(this, key, true, true)
},
// 不收集依赖
get size() {
return size(this as unknown as IterableCollections, true)
},
// readonly是true,不用has收集依赖
has(this: MapTypes, key: unknown) {
return has.call(this, key, true)
},
add: createReadonlyMethod(TriggerOpTypes.ADD),
set: createReadonlyMethod(TriggerOpTypes.SET),
delete: createReadonlyMethod(TriggerOpTypes.DELETE),
clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
// 不依赖收集,不对获取到的数据响应式化
forEach: createForEach(true, true)
}
const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
iteratorMethods.forEach(method => {
mutableInstrumentations[method as string] = createIterableMethod(
method,
false,
false
)
readonlyInstrumentations[method as string] = createIterableMethod(
method,
true,
false
)
shallowInstrumentations[method as string] = createIterableMethod(
method,
false,
true
)
shallowReadonlyInstrumentations[method as string] = createIterableMethod(
method,
true,
true
)
})
return [
mutableInstrumentations,
readonlyInstrumentations,
shallowInstrumentations,
shallowReadonlyInstrumentations
]
}
从上面可以看到,基本是对get
、has
、add
、set
、delete
、clear
、forEach
的方法劫持,使用反射调用原来的逻辑,但在此之前,根据shallow
和readonly
来定义数据是否响应式化,是否收集依赖。
但接下来,对keys
、values
、entries
、Symbol.iterator
做了特殊处理,他们通过createIterableMethod
方法进行拦截。
typescript
function createIterableMethod(
method: string | symbol,
isReadonly: boolean,
isShallow: boolean
) {
return function (
this: IterableCollections,
...args: unknown[]
): Iterable & Iterator {
const target = (this as any)[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const targetIsMap = isMap(rawTarget)
const isPair =
method === 'entries' || (method === Symbol.iterator && targetIsMap)
const isKeyOnly = method === 'keys' && targetIsMap
const innerIterator = target[method](...args) // 调用目标对象的迭代器方法,并传入参数
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
!isReadonly &&
// 如果非只读,进行依赖收集
track(
rawTarget,
TrackOpTypes.ITERATE,
isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
)
// 返回一个包装后的迭代器,该迭代器返回目标迭代器方法的观察版本
return {
// iterator protocol
next() {
const { value, done } = innerIterator.next()
return done
? { value, done }
: {
value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
done
}
},
// iterable protocol
[Symbol.iterator]() {
return this
}
}
}
}
也就是说,针对keys
、values
、entries
、Symbol.iterator
做的特殊处理,就是如果只读就不收集依赖,如果shallow
就不对新结果响应式化。
Method Map.prototype.set called on incompatible receiver
如果我们模仿上文中对Map
等数据结构代理,大概率会报这个错误。
typescript
const map = new Proxy(new Map([['name', 'lumozx']]), {
get(target, p, receiver) {
console.log('get');
return Reflect.get(target, p, receiver);
},
set(target, p, value, receiver) {
console.log('set');
return Reflect.set(target, p, value, receiver);
},
});
console.log(map.set('age', 20));
这里我用了反射,但为什么还不行呢?答案是Internal slots
引起的。
Map
、WeakMap
等数据结构,会将数据保存在Internal slots
(内部插槽)中,所以只有Map、WeakMap
等这些数据对象在内部可以通过this
访问到数据,但是代理Proxy
是不存在这样的插槽的,当Map
等这些数据对象被Proxy
包装了之后,this
就变成了Proxy
对象,所以this
自然就访问不到数据了。
解决办法是将this
强制更正为原始数据对象,也就是bind target
。
typescript
const map = new Proxy(new Map([['name', 'lumozx']]), {
get(target, p, receiver) {
console.log('get');
return Reflect.get(target, p, receiver).bind(target);
},
set(target, p, value, receiver) {
console.log('set');
return Reflect.set(target, p, value, receiver).bind(target);
},
});
console.log(map.set('age', 20));
这样就可以访问到了。
但问题是Vue3
并没有使用bind
,他们用了什么方法呢?回忆一下,他们确实没有bind target
,但是他们自己造了一个target
。
在这个target
中,vue3
使用了getProto
,也就是Reflect.getPrototypeOf
,获取了原始数据的原型,然后直接在原始数据的原型上调用对应的方法。
需要注意的是,这里获取到的this
实际上是代理对象 ,直接使用,会导致无限循环。反复触发代理对象对应拦截器。
所以Vue3
中,使用toRaw
还原了this
,使用的代理之前的原始对象。然后在原始对象的原型上调用对应的方法。从而实现方法劫持。
effect
我们从第一节就讲effect
,到现在前置知识已经补完,是应该了解一下这个是什么东西,或者是怎么来的。
我们从上文中了解到收集依赖,但是依赖是怎么来的呢?
我们知道,在setupRenderEffect
中,调用了new ReactiveEffect
typescript
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(update),
instance.scope // track it in component's effect scope
))
const update: SchedulerJob = (instance.update = () => effect.run())
update()
当前实例会挂载一个effect
属性,那么我们看看ReactiveEffect
是怎么处理的,怎么当依赖变动的时候,会触发执行componentUpdateFn
。
typescript
// 初始化追踪深度为0
let effectTrackDepth = 0
// 定义追踪操作位为1,这个是二进制位
export left trackOpBit = 1
// 最大嵌套层数
const maxMarkerBits = 30
// 当前活跃的 effect
let activeEffect;
export class ReactiveEffect<T = any> {
active = true; // 是否激活的标志
deps: Dep[] = []; // 依赖数组
parent: ReactiveEffect | undefined = undefined; // 父级effect
computed?: ComputedRefImpl<T>; // 计算属性引用
allowRecurse?: boolean; // 是否允许递归
private deferStop?: boolean; // 是否延迟停止
onStop?: () => void; // 停止时执行的回调函数
constructor(
public fn: () => T, // 响应式函数
public scheduler: EffectScheduler | null = null, // 调度器,可为空
scope?: EffectScope // effect的作用域
) {
recordEffectScope(this, scope); // 记录effect的作用域
}
run() {
if (!this.active) {
return this.fn(); // 如果effect处于非激活状态,直接返回函数的执行结果,不进行依赖相关逻辑
}
let parent: ReactiveEffect | undefined = activeEffect; // 保存当前活动的effect
let lastShouldTrack = shouldTrack; // 保存当前的追踪状态
while (parent) {
if (parent === this) {
return; // 如果当前effect的父级是自身,则直接返回,避免无限递归
}
parent = parent.parent; // 否则,继续向上查找父级响应式效果
}
try {
this.parent = activeEffect; // 将当前活动的effect设置为当前effect父级
activeEffect = this; // 当前活动的effect为当前effect
shouldTrack = true; // 设置追踪状态为true
trackOpBit = 1 << ++effectTrackDepth; // 更新追踪操作位
if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this); // 如果追踪深度不超过最大标记位数,初始化依赖标记
} else {
cleanupEffect(this); // 否则,清理响应式效果
}
return this.fn(); // 执行响应式函数
} finally {
if (effectTrackDepth <= maxMarkerBits) {
finalizeDepMarkers(this); // 如果追踪深度不超过最大标记位数,完成依赖标记
}
trackOpBit = 1 << --effectdeferStopTrackDepth; // 恢复追踪操作位
activeEffect = this.parent; // 恢复活动的 effect 为当前父级,当前父级可能是undefined,所以当所有effect执行完毕后,activeEffect肯定是undefined
shouldTrack = lastShouldTrack; // 恢复追踪状态
this.parent = undefined; // 清空父级,因为在effect中父级是不定的,没必要记载
if (this.deferStop) {
this.stop(); // 如果延迟停止标志为true,执行停止操作
}
}
}
stop() {
// 在运行自身时停止 - 延迟清理
if (activeEffect === this) {
this.deferStop = true; // 如果活动的effect是自身,设置延迟停止标志为true
} else if (this.active) {
cleanupEffect(this); // 否则,清理响应式效果
if (this.onStop) {
this.onStop(); // 如果有停止时的回调函数,执行回调函数
}
this.active = false; // 将effect的激活状态设置为false
}
}
}
看一下,当new ReactiveEffect
的时候,fun
会保存为传入的函数,当使用run
的时候,会查询当前effect
与activeEffect
是否是循环依赖,如果不是,那么当前的父依赖就是现在的activeEffect
,然后将activeEffect
赋值为此effect
。也就是说让当前activeEffect
就是此effect了
。
此时再执行fun
,fun
获取到的响应式数据,都是在activeEffect
为此effect
的前提下进行依赖收集的。一旦触发响应式数据的get
,就会触发track
,从而触发trackEffects
我们回忆一下trackEffects
的关键逻辑。
typescript
if (shouldTrack) {
//收集依赖
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
}
activeEffect
就是上文的effect
。
当执行完trackEffects
后,会接着执行ReactiveEffect.run
中的finally
,在finally
中会执行依赖清理逻辑,如果有多个effect
嵌套,就会逐个一层一层还原activeEffect
,直至undefined
。让activeEffect
的指针有来有回,而不是让activeEffect
固定在最深的依赖,这就是this.parent
的逻辑。
这里需要注意一下清理依赖的逻辑。
依赖清理
假如说有这样一段代码。
typescript
const state = reactive({
count: 0,
isActive: true,
});
effect(() => {
console.log('effect触发')
if (state.isActive) {
console.log(`Count is: ${state.count}`);
}
});
state.count++;
state.isActive = false;
state.count++; // 不会触发effect
显而易见,在第一次加载执行的时候,是没有问题的,然后执行 state.count++
是会触发effect
的,然后state.isActive = false
,依然执行了effect
。
但是,之后执行的state.count++
却不会触发effect
,虽然内部有非响应式数据,但对于响应式数据来说,这时候执行effect
是没有意义的。因此从性能角度来说,需要删除effect
的副作用函数。
不过在了解清理依赖之前,需要知道Vue3
对依赖做了什么样的标记,这样我们才能再必要的时候清理依赖。
typescript
// 当前追踪深度为0
let effectTrackDepth = 0
// 定义追踪操作位为1,这个是二进制位
export left trackOpBit = 1
// 最大嵌套层数
const maxMarkerBits = 30
在上文的逻辑中,因为没有超过最大层数,会在run
中执行initDepMarkers
。
typescript
export const initDepMarkers = ({ deps }: ReactiveEffect) => {
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].w |= trackOpBit
}
}
}
此时因为是初始化,deps
是空数组,所以if
逻辑不会执行。
然后执行this.fun
,从而触发get
收集依赖。然后进入trackEffects
函数。我们回忆下trackEffects
函数,主要看这串逻辑。
typescript
// 如果该依赖对象尚未被新追踪,将其标记为新追踪
if (!newTracked(dep)) {
dep.n |= trackOpBit // 设置为新追踪
shouldTrack = !wasTracked(dep) // 判断是否应该追踪该依赖对象
}
if (shouldTrack) {
dep.add(activeEffect!)
activeEffect!..push(dep)
}
这里面有newTracked
和wasTracked
两个方法
typescript
export const wasTracked = dep => (dep.w & trackOpBit) > 0
export const newTracked = dep => (dep.n & trackOpBit) > 0
实际上是进行位运算。其中的w是已经被收集过的。n是新收集的依赖。
当初始化的时候。state.isActive
是true
,w
和n
都是false
,所以会收集到依赖中。
然后函数结尾,activeEffect.dep
会是下面的数据。
typescript
[
{"w":0,"n": 00000000000000000000000000000010, [effect]},
{"w":0,"n": 00000000000000000000000000000010, [effect]}
]
然后在finally
执行finalizeDepMarkers
逻辑
typescript
export const finalizeDepMarkers = (effect: ReactiveEffect) => {
const { deps } = effect
if (deps.length) {
let ptr = 0
for (let i = 0; i < deps.length; i++) {
const dep = deps[i]
// 如果依赖项之前被追踪,但新的追踪操作不再依赖它
if (wasTracked(dep) && !newTracked(dep)) {
dep.delete(effect)
} else {
// 如果依赖项是有效的,则保留在 deps 数组中,并更新指针位置
deps[ptr++] = dep
}
// 清除标记位,确保下一轮追踪不受上一轮的影响
dep.w &= ~trackOpBit
dep.n &= ~trackOpBit
}
// 更新 deps 数组,截断无效的依赖项
deps.length = ptr
}
}
根据这些逻辑,如果新收集到的依赖里面没有已经收集的依赖,说明那些依赖是无效的,因此会移出,重新更新deps
,也就是依赖列表。此时w和n会被重置为0
。
也就是如下
typescript
[{"w":0, "n":0, [effect]},{"w":0, "n":0, [effect]}]
然后执行state.isActive = false
的时候,再次触发effect
,然后走到initDepMarkers
,因为dep
长度非0
,已经收集了依赖,所以会进入isActive
的依赖。然后执行trackEffects
,此时的 newTracked = false
,然后跟之前一样执行,但由于isAvtive
是false
,所以没有触发count
的依赖收集。此时deps
如下
typescript
[
{
"w": 00000000000000000000000000000010,
"n": 00000000000000000000000000000010,
[effect]
},
{
"w": 00000000000000000000000000000010,
"n": 0,
[effect]
}
]
然后执行finalizeDepMarkers
,触发了delete
删除,然后执行state.coiunt++
的操作,但是因为依赖已经没有count
了不会执行副作用函数。
如果当依赖层级大于30
的时候,会触发cleanupEffect
typescript
function cleanupEffect(effect: ReactiveEffect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
其实就是一个一个删除,然后下次收集的时候重新添加。
computed
那么计算属性是什么逻辑呢?
我们看看计算属性的源码
typescript
export function computed<T>(
getter: ComputedGetter<T>,
debugOptions?: DebuggerOptions
): ComputedRef<T>
export function computed<T>(
options: WritableComputedOptions<T>,
debugOptions?: DebuggerOptions
): WritableComputedRef<T>
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions,
isSSR = false
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
// 如果是函数类型,是只有get
const onlyGetter = isFunction(getterOrOptions)
if (onlyGetter) {
// 将函数赋值getter
getter = getterOrOptions
setter = NOOP
} else {
// 对象类类型,get赋值给getter
getter = getterOrOptions.get
// 对象类类型,set赋值给setter
setter = getterOrOptions.set
}
// 创建 ComputedRefImpl 并返回
const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)
return cRef as any
}
从入参可以看出,计算属性可以接受函数类型,也接受对象类型,这个在文档里已经说过了,不过对于函数类型,他只是赋值给了getter
,最后他们都会传入ComputedRefImpl
,我们已经从文档中得知,new ComputedRefImpl
的返回值就是一个ref
响应式对象。那么我们看看ComputedRefImpl
做了什么。
typescript
export class ComputedRefImpl<T> {
public dep?: Dep = undefined; // 依赖对象
private _value!: T; // 计算属性的值
public readonly effect: ReactiveEffect<T>; // 对应的effect
public readonly __v_isRef = true; // 标记为响应式引用
public readonly [ReactiveFlags.IS_READONLY]: boolean = false; // 是否为只读,如果是函数就是true
public _dirty = true; // 标志计算属性是否脏,是否需要重新计算
public _cacheable: boolean; // 是否可以缓存计算结果
constructor(
getter: ComputedGetter<T>, // 计算属性的getter
private readonly _setter: ComputedSetter<T>, // 计算属性的seter
isReadonly: boolean, // 是否为只读,如果是函数就是true
isSSR: boolean
) {
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true; // 如果不是脏的,设置为脏,并触发引用值的更新
triggerRefValue(this);
}
});
this.effect.computed = this; // 将当前计算属性与响应式效果关联
this.effect.active = this._cacheable = !isSSR; // 计算属性在非SSR环境下默认为激活状态且可缓存
this[ReactiveFlags.IS_READONLY] = isReadonly; // 设置只读标志
}
get value() {
// 计算属性的值被其他代理对象(例如readonly())包裹可能性很高,见 #3376
const self = toRaw(this); // 获取原始的计算属性对象
trackRefValue(self); // 收集依赖
if (self._dirty || !self._cacheable) {
self._dirty = false; // 如果是脏的或者不可缓存,标记为非脏
self._value = self.effect.run()!; // 重新计算计算属性的值
}
return self._value; // 返回计算属性的值,如果上面的if不通过。这个值可能是缓存的
}
set value(newValue: T) {
this._setter(newValue); // 设置计算属性的值,触发设置函数
}
}
整个逻辑还是比较简单的,在constructor
的时候,创建了一个effect
函数,在这里,设置了ReactiveEffect
第二个参数------scheduler
,在这里,我们先回忆一下,effect
是如何触发的?我们知道effect
中有一个run
,触发effect
的函数执行实际上就是执行run
,但是run
是谁触发的呢?
前面已经说的很明白了,在各种响应式上逻辑上,是ref
set
-> triggerRefValue
/ reactive
track
-> triggerEffects
-> triggerEffect
如果是ref
的话,会在set
触发triggerRefValue
,获取到dep
,然后把dep
传入triggerEffects
,而reactive
会在各个地方的set
触发track
,然后根据targetMap
获取dep
, 然后把dep
传入triggerEffects
。
这样triggerEffects
就获取到了dep
,然后遍历dep
,每个都是effect
,分情况先后使用triggerEffect
触发effect
,前面也讲过,effect
有scheduler
就执行scheduler
,有run
就执行run
。
是的,我们可以说effect
是triggerEffect
触发的,但不一定触发run
,因为有scheduler
就触发scheduler
。
这时候有人就问了,定义componentUpdateFn
的effect
的地方,也传了第二个参数,那么按理说依赖变动的时候,应该执行第二个参数,而不是componentUpdateFn
。
很好,看的很仔细,但不够仔细,我们看看第二个参数是什么。
typescript
() => queueJob(update),
是一个调度函数包装的update
,最终会调用的update
函数。
typescript
const update: SchedulerJob = (instance.update = () => effect.run())
// ...
update()
也就是说componentUpdateFn
的effect
,经常调用的是scheduler
,而scheduler
其实是包装过的run
。本质还是run
,而run
最后执行了 componentUpdateFn
。
因此,对componentUpdateFn
的effect
来说,scheduler
约等于run
,但别的effect
不一定。
比如计算属性的这个
typescript
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true;
triggerRefValue(this);
}
});
显而易见,这个effect
会执行scheduler
,但scheduler
不会执行run
,而是如果_dirty
为false
的时候,接着触发这个effect
的依赖,换句话说,跳过了计算属性的run
,或者说跳过了计算属性getter
对应的函数的执行。
那么什么时候触发getter
的执行呢?
我们知道计算属性返回包裹着value
的对象,而根据代码来看value
不是具体的值,而是进行了set
和get
劫持。
我们看看get value
。
typescript
get value() {
// ...
if (self._dirty || !self._cacheable) {
self._dirty = false
self._value = self.effect.run()!
}
return self._value
}
如果_dirty
为true
,或者_cacheable
为false
才会执行里面的值,这里说明下,_cacheable
是非SSR
的情况下才为true
,代表能否缓存值,也就是说非SSR
情况下, !self._cacheable
一直为false
。
那么我们把目光聚焦到前面的self._dirty
,_dirty
默认是true
。
所以第一次get value
必定走这个函数,这里面把_dirty
改为了false
。然后执行了effect
的run
。
然后把结果值保存在_value
上。
无论走不走if
。都会返回缓存的_value
,区别在于_value
是刚缓存的还是很久之前缓存的。
那么有疑问了,既然 !self._cacheable
是false
,而刚刚我们把self._dirty
也设置了false
,那么这个if
就永远也走不通了。永远使用第一次执行run
缓存下来的值了。
我们别忘了scheduler
,如果这个计算属性的依赖有变化,scheduler
会执行,然后把_dirty
设置为true
。然后接着触发依赖这个计算属性的effect
。
如果那些effect
里面依赖这个计算属性的value
,正好触发了get
拦截器,然后再次设置_dirty
为false
,更新缓存值为最新的数据。返回缓存值。
我们捋一下流程我们捋一下流程
get value
触发 ->self._dirty
设置为false
-> 缓存最新的值 -> 返回最新的值- 计算属性依赖更新 -> 触发
scheduler
->._dirty
设置为true
-> 触发依赖计算属性的更新 -> 计算属性value
的get
->第1步
- 如果计算属性的依赖没有更新 获取计算属性
value
的get
-> 返回缓存的计算属性的值
当前的effect
改变后,才会执行dep
,让他使用与effect
有关的最新的值。比如上文的计算属性,通过触发triggerRefValue
,来让自己的dep
获取自己最新的value
值。
有的人就会问了,既然计算属性依靠的也是effect
,那么岂不是在收集依赖的时候,effect
可能存在先于计算属性执行的情况(因为effect
是立刻执行)?写在计算属性下面的effect
,获取上面的计算属性可能获取的是缓存值?这与vue2
的直觉不符------计算属性的计算应该在生命周期非常靠前的。
是这样的,因此在triggerEffects
里面用了两次循环,特殊处理了计算属性,优先循环是计算属性的effect
,优先执行他的scheduler
,让他的_dirty
为false
,然后再执行依赖他的effects
,然后再优先执行里面的计算属性的effect
,当_dirty
为false
的时候,这样获取这个计算属性的value
的时候,就会执行他的run
方法,获取他最新的值。
举个例子
typescript
import { ref, effect, computed } from 'vue'
const num = ref(0)
const add = computed(() => num.value + 1)
effect(() => {
console.log('num',num.value)
console.log('add', add.value)
})
num.value++
// 初始化执行
// num 0
// add 1
// computed改变引起的effect执行
// num 1
// add 2
// num改变引起的effect执行
// num 1
// add 2
此时因为computed
优先执行,因此打印的值add
优先变成了2
,同时因为computed
优先执行,依赖他的effect
也会被优先执行,因此第一次打印出 num 1 add 2
的effect
,实际上是computed
触发的。
Watch
我们一直在讲响应式的基础------effect
,那么看到effect
的行为,我们想到了什么类似的api
吗?答案是watch
。而watch
的实现其实就是校验了一下参数,然后原封不动调用了doWatch
,所以我们直接看看doWatch
的源码。
typescript
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object, // 监听的数据源,可以是单个数据、数组、响应式对象或watch effect函数
cb: WatchCallback | null, // 数据变化时触发的回调函数
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ // 配置项,默认为空对象
): WatchStopHandle { // 返回一个用于停止监听的函数句柄
const instance = getCurrentScope() === currentInstance?.scope ? currentInstance : null // 获取当前组件实例
let getter: () => any // 获取数据的函数
let forceTrigger = false // 是否强制触发回调函数
let isMultiSource = false // 是否监听多个数据源
// 判断数据源的类型,设置相应的getter函数
if (isRef(source)) {
getter = () => source.value
forceTrigger = isShallow(source)
} else if (isReactive(source)) {
getter = () => source
deep = true
} else if (isArray(source)) {
isMultiSource = true
forceTrigger = source.some(s => isReactive(s) || isShallow(s))
getter = () =>
source.map(s => {
if (isRef(s)) {
return s.value
} else if (isReactive(s)) {
return traverse(s)
} else if (isFunction(s)) {
return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
}
})
} else if (isFunction(source)) {
if (cb) {
// 带有回调函数的getter
getter = () =>
callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
} else {
// 没有回调函数的简单effect
getter = () => {
if (instance && instance.isUnmounted) {
return
}
if (cleanup) {
cleanup()
}
return callWithAsyncErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onCleanup]
)
}
}
} else {
getter = NOOP
}
// 深度监听时,递归获取数据
if (cb && deep) {
const baseGetter = getter
getter = () => traverse(baseGetter())
}
let cleanup: () => void // 副作用清理函数
let onCleanup: OnCleanup = (fn: () => void) => {
cleanup = effect.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
}
}
// 在服务器端渲染时,无需设置实际的effect,应该是空操作,除非是eager或sync flush
let ssrCleanup: (() => void)[] | undefined
if (__SSR__ && isInSSRComponentSetup) {
// 在这种情况下,无需调用invalidate回调(+ runner未设置)
onCleanup = NOOP
if (!cb) {
getter()
} else if (immediate) {
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
getter(),
isMultiSource ? [] : undefined,
onCleanup
])
}
if (flush === 'sync') {
const ctx = useSSRContext()!
ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = [])
} else {
return NOOP
}
}
// 初始化oldValue
let oldValue: any = isMultiSource
? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
: INITIAL_WATCHER_VALUE
// 定义响应函数,用于触发回调函数
const job: SchedulerJob = () => {
if (!effect.active) {
return
}
if (cb) {
// watch(source, cb)
const newValue = effect.run()
if (
deep ||
forceTrigger ||
(isMultiSource
? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
: hasChanged(newValue, oldValue)) ||
(__COMPAT__ &&
isArray(newValue) &&
isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
) {
// 在再次运行回调函数之前进行清理
if (cleanup) {
cleanup()
}
// 调用带有异步错误处理的回调函数
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
newValue,
// 在第一次变化时将旧值传递为undefined
oldValue === INITIAL_WATCHER_VALUE
? undefined
: isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
? []
: oldValue,
onCleanup
])
oldValue = newValue
}
} else {
// watchEffect
effect.run()
}
}
// 将该job标记为一个watcher回调函数,以便scheduler知道它可以自触发
job.allowRecurse = !!cb
let scheduler: EffectScheduler
if (flush === 'sync') {
scheduler = job as any // 直接调用scheduler函数
} else if (flush === 'post') {
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// 默认为'pre'
job.pre = true
if (instance) job.id = instance.uid
scheduler = () => queueJob(job)
}
// 创建ReactiveEffect实例
const effect = new ReactiveEffect(getter, scheduler)
// 初始运行
if (cb) {
if (immediate) {
job()
} else {
oldValue = effect.run()
}
} else if (flush === 'post') {
queuePostRenderEffect(
effect.run.bind(effect),
instance && instance.suspense
)
} else {
effect.run()
}
// 返回一个函数,用于停止监听
const unwatch = () => {
effect.stop()
if (instance && instance.scope) {
remove(instance.scope.effects!, effect)
}
}
// 在服务器端渲染时,将unwatch函数加入到ssrCleanup数组中
if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch)
return unwatch
}
首先,会根据传入的数据源,设置不同的监听逻辑。
- 如果是
ref
,那么getter
函数返回的就是数据源的value
,并且使用isShallow
来判断forceTrigger
是否是true
,从而能否强制触发回调函数 - 如果是
reactive
,那么getter
函数返回的就是数据源本身,默认是深度监听 - 如果是非响应式的数组,那么这个数组可能是多个数据源组成,那么就需要监听多个数据源,如果数组有一个是
reactive
或者shallow
,那么forceTrigger
就是true
,就会强制触发回调函数,getter
函数返回的的数组,数组使用map
,将每一项都依照上面的逻辑进行处理。如果是reactive
,就使用traverse
处理,返回一个set
,触发内部所有的get
,从而收集到依赖。 - 如果是函数,那么
getter
函数返回的就是函数的返回值,没有入参 - 其他情况,
getter
就是一个无返回值空函数
在这里遇到了两个没有解释很清楚的逻辑,一个是forceTrigger
,一个是traverse
。
forceTrigger
,上文中我们讲过是用来强制触发回调函数的,那么为什么要强制触发呢?
因为一般来说,watch
中只有值变化的时候,才会触发回调函数,也就是新值不等于旧值,但是如果getter
函数返回的是对象,或者经过shallow
浅层化的响应式数据,那么旧值一定等于新值,因此就不能单纯判断是否相等,所以这里的逻辑是只要发送变化,那么就触发回调函数,而控制这个逻辑的开关,就是forceTrigger
。
当然,这里需要补充的是,watch
归根结底都是监听的是响应式数据,也就是如果你使用shallow
浅层化了一个响应式数据,那么修改未经响应化的深层,也不会触发回调,而修改经过响应式化的数据,由于watch
是基于effect
,而effect
的触发是比较新旧值的,因此即使是forceTrigger
,也会由于effect
新旧值比较相同,而不会触发effect
的run
,从而不会触发回调函数。
针对ref
对象,watch
默认deep
是false
,因此构建effect
的时候,不会使用traverse
,而只是收集到value
的浅层依赖 getter = () => source.value
。
针对shallowRef
,因为只针对value
本身被赋值的情况进行响应,因此也无法收集到深层依赖。
针对reactive
,默认使用深度监听,也就是使用traverse
触发内部所有的get
。
那么接下来,需要了解traverse
具体实现了
typescript
export function traverse(value: unknown, seen?: Set<unknown>) {
// 如果不是对象或者具有ReactiveFlags.SKIP标志的属性,直接返回该值
if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
return value
}
// 初始化一个Set来存储已经遍历过的对象,避免循环引用
seen = seen || new Set()
// 如果该对象已经被遍历过,直接返回该值
if (seen.has(value)) {
return value
}
// 将当前对象加入已遍历集合中,避免循环引用
seen.add(value)
// 如果是响应式引用(Ref对象),递归遍历其内部的值
if (isRef(value)) {
traverse(value.value, seen)
}
// 如果是数组,递归遍历数组的每个元素
else if (isArray(value)) {
for (let i = 0; i < value.length; i++) {
traverse(value[i], seen)
}
}
// 如果是集合(Set对象)或者映射(Map对象),递归遍历集合或者映射的每个元素
else if (isSet(value) || isMap(value)) {
value.forEach((v: any) => {
traverse(v, seen)
})
}
// 如果是普通对象,递归遍历对象的每个属性值
else if (isPlainObject(value)) {
for (const key in value) {
traverse(value[key], seen)
}
}
// 返回遍历后的响应式对象
return value
}
我们看到,所有嵌套的数据结构都会被转换成响应式对象,并且使用了Set
,因此不会出现循环依赖的问题,只有可以在数据发生变化时触发相应的更新。也就是我们所说的deep : true
。
因此,我们也可以得到跟文档相同的结论。在数组中的响应式对象,也会被深度监听。
接下里我们继续看看函数逻辑,当存在回调且是深度监听的时候,使用traverse
包装getter
函数,这个时候还不会触发traverse
。
接着定义了副作用清理函数onCleanup
,副作用清理函数会 传入的回调函数的第三个值。并且接受一个函数。
这个函数将会在下次effect
的调度函数触发的时候触发。而下次effect
调度函数触发的时候,实际上也是watch
回调函数的触发的时候。也就是说,副作用清理函数的参数是一个函数A
,每次触发effect
,如果上一次触发effect
的时候传入了A
函数,那么就触发他。
同时onCleanup
还会挂载在effect的onStop
上,这样销毁的时候也会触发onCleanup
(下文会讲)
官方给予的使用场景是
typescript
watch(id, async (newId, oldId, onCleanup) => {
const { response, cancel } = doAsyncWork(newId)
// 当 `id` 变化时,`cancel` 将被调用,
// 取消之前的未完成的请求
onCleanup(cancel)
data.value = await response
})
如果连续触发回调函数,且以最新的回调函数的结果为准,那么可以将本次回调函数清理副作用逻辑的函数传给onCleanup
,那么将会在下次触发回调函数之前,触发他。
再然后省略SSR
的逻辑。之后初始化oldValue
,也就是旧值。但这里只是初始化一个结构------INITIAL_WATCHER_VALUE
,实际是一个空,这里旧值还会分情况初始化,如果是源数据是数组的话,会初始化,相同长度空对象数组。
然后是响应函数的构建。我们之前讲过,类似 () => queueJob(update)
的update
,当依赖的响应式数据有变化的时候,会执行这个update
。
构建完响应函数,会给予这个函数allowRecurse
属性,如果存在回调函数,这个allowRecurse
是true
,代表可以递归调用。
接下来会根据传入的flush
,将构建不同的scheduler
,scheduler
如同之前的() => queueJob(update)
如果flush
是sync
,也就是watchSyncEffect
,那么scheduler
就是响应函数,不会经过queueJob
进行排队。
如果flush
是post
,也就是watchPostEffect
,那么响应函数会通过queuePostRenderEffect
进行包装。queuePostRenderEffect
在当前渲染周期结束后执行。也就是延后执行。
默认flush
是pre
,在这里,scheduler
会被包装成 () => queueJob(job)
,默认是渲染更新之前执行。
然后构建effect
,依赖收集的函数就是上文的getter
。调度函数就是刚刚创建的scheduler
。
如果回调函数存在,且immediate
为true
,就会立即执行响应函数,响应函数会获取当前getter
的结果,作为新值,如果旧值依然是INITIAL_WATCHER_VALUE
,那么就是undefined
,如果旧值是数组,判断的是数组的第一个值是不是INITIAL_WATCHER_VALUE
那么就是[]
。然后新值赋值给旧值。
如果immediate
不是true
。那么就会使用effect.run()
,收集依赖的同时,会获取返回值,然后赋值给旧值。
如果flush
是post
,那么effect.run()
的执行会使用bind保存指针,然后延后。
如果没有回调函数,会直接执行effect.run()
收集依赖。
最后定义取消监听函数并返回,本质是执行effect
的stop
中的cleanupEffect
(this.active
默认为true
),清除所有的依赖,然后将当前effect
从实例中移除。如果之前注入了onStop
,也就是给onCleanup
传了函数,还会执行这个函数。