注:本文使用vue版本为3.3.4
前文我们了解了渲染流程,整个流程是基于effect函数的依赖变动,从而不断触发patch,保持页面为数据的最新渲染,那么我们这次来了解一下他的依赖是怎么建立起来的。
或者说vue的响应式是什么。众所周知,vue的响应式数据包括ref, reactive, computed,我们一个一个来看。
ref、shallowRef
ref方法和shallowRef都可以创建一个响应式数据,这个响应式数据的value就是我们使用的数据,对这个value进行修改都会触发响应式逻辑。
他们之中不同点是
ref可以进行深层次转换
shallowRef不能进行深层次的转换,这里需要注意的是,value本身也算一层。所以,对value整体的赋值,才会触发shallowRef的响应式。
我们直接看看他们的源码。
export function ref(value?: unknown) {
return createRef(value, false)
}
export function shallowRef(value?: unknown) {
return createRef(value, true)
}
可以看到,他们都调用了createRef,不同的是第二个参数不同,可以推断出第二个参数就是控制shallow的开关。
那么我们进入createRef看一下
function createRef(rawValue: unknown, shallow: boolean) {
// 已经是响应式了,那么原值返回
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
可以看到,createRef尽可能拦截多层响应式数据的ref嵌套,如果已经是一个响应式数据,那么将原值返回,否则,进入new RefImpl逻辑。这里返回new的结果,我们可以大胆猜测,new RefImpl的结果就是那个包含value的响应式对象。
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的实现逻辑呢?
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value
简单明了,如果是对象类型,调用reactive,也就是ref的对象类型,是对reactive的一层封装。
我们直接看看reactive的源码。
reactive、shallowReactive
reactive和shallowReactive都可以创建响应式对象,区别同ref一样,带有shallow的只能创建浅层响应式数据。
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的开关。
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
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
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实现了什么逻辑。
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,然后effect的自增行为又触发了effect,可以看这个Issue。
按照之前的逻辑往下走,我们发现了收集依赖函数——track,在这之前我们已经见到trackRefValue了,现在补上之前留下的坑。
// 本质是还原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,而value就是dep的集合,dep就是副作用函数。这个类似Vue2 Observer的dep。
在trackEffects中,响应式数据(ref,effect,computed等)的dep会传入进来,供trackEffects进行处理,里面会遇到新旧依赖的逻辑,这个之后会讲,在收集依赖的逻辑里面,会讲当前的activeEffect视同add加入dep中,也就是收集到了依赖,明确说明一点是activeEffect依赖当前正在trackEffects处理的响应式数据,当这个响应式数据变化的时候,会触发dep里面的当时推入的effect的不同逻辑。
同时activeEffect还有自己的逻辑,他本身还有deps,同时把dep推入自己的deps数组里面。
也就是说,deps的是当前effect依赖的响应式数据所被依赖的合集。比如说
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。
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(因为shallowReadonly不会递归,所以递归的都使用readonly的逻辑)。
总结一下,ReadonlyReactiveHandler通过屏蔽set和deleteProperty,实现只读的功能,通过对shallow开关的控制,来决定是否对非根层级的对象节点实现只读的功能。
那么mutableHandlers和shallowReactiveHandlers使用的MutableReactiveHandler呢?我们再次看看源码
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了——原型链上的先触发,这里再触发。
有人奇怪会是什么情况,这个情况确实不常见,但的确存在一种边界情况。我们引入一个例子
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是什么东西
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的依赖呢?也需要收集触发。
const arr = reactive([]);
effect(() => {
console.log(arr[1])
});
// 此时数组是[0],无事发生
arr.push(0);
// 此时数组是[0,1] ,触发 effect,打印出arr[1]
arr.push(1);
上面的代码中,我们第一次访问了 state[1], 所以,对 state[1] 进行了依赖收集,而第一次的 state.push(0) 设置的是 state 的第 0 个元素,所以不会触发响应式更新。而第二次的 push 触发了对 state[1] 的更新。但依照这个逻辑,数组的map等遍历方法就无法使用响应式触发了,这反而很反直觉,那么实际上呢?
const arr = reactive([]);
// 观测变化
effect(() => {
console.log(arr.map(item => item))
}
arr.push(1) //触发
实际上是触发的,这里面的原因就在依赖捕获上,如果直接访问数组元素,那么触发的get的ke只会是目标元素,但如果数组的方法会触发别的key,我们直接看一下。
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
可以看到,触发map方法会触发get三次,分别是map,length,以及constructor,正是因为这个访问,才收集到了length的依赖,而我们push数据的时候改变了length,从而触发了对应的effect。
那么接着发散一下,对象呢?对象也是可迭代数据,但没有length啊,比如Object.keys,回忆一下MutableReactiveHandler之中,是不是有个ownKeys,是的,他是收集key的访问的,当使用对象迭代的时候,会触发这个拦截器。
这拦截器里面调用了track,关于track这个我们前面讲过了,就有依赖触发,没依赖就设置一个,然后触发。
那么我们接着看,最后的依赖会被triggerEffects触发
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函数。
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也分为四种
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是怎么实现的。
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的逻辑。
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方法进行拦截。
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等数据结构代理,大概率会报这个错误。
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。
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
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。
// 初始化追踪深度为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的关键逻辑。
if (shouldTrack) {
//收集依赖
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
}
activeEffect 就是上文的effect。
当执行完trackEffects后,会接着执行ReactiveEffect.run中的finally,在finally中会执行依赖清理逻辑,如果有多个effect嵌套,就会逐个一层一层还原activeEffect,直至undefined。让activeEffect的指针有来有回,而不是让activeEffect固定在最深的依赖,这就是this.parent的逻辑。
这里需要注意一下清理依赖的逻辑。
依赖清理
假如说有这样一段代码。
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对依赖做了什么样的标记,这样我们才能再必要的时候清理依赖。
// 当前追踪深度为0
let effectTrackDepth = 0
// 定义追踪操作位为1,这个是二进制位
export left trackOpBit = 1
// 最大嵌套层数
const maxMarkerBits = 30
在上文的逻辑中,因为没有超过最大层数,会在run中执行initDepMarkers。
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函数,主要看这串逻辑。
// 如果该依赖对象尚未被新追踪,将其标记为新追踪
if (!newTracked(dep)) {
dep.n |= trackOpBit // 设置为新追踪
shouldTrack = !wasTracked(dep) // 判断是否应该追踪该依赖对象
}
if (shouldTrack) {
dep.add(activeEffect!)
activeEffect!..push(dep)
}
这里面有newTracked和wasTracked两个方法
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会是下面的数据。
[
{"w":0,"n": 00000000000000000000000000000010, [effect]},
{"w":0,"n": 00000000000000000000000000000010, [effect]}
]
然后在finally执行finalizeDepMarkers逻辑
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。
也就是如下
[{"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如下
[
{
"w": 00000000000000000000000000000010,
"n": 00000000000000000000000000000010,
[effect]
},
{
"w": 00000000000000000000000000000010,
"n": 0,
[effect]
}
]
然后执行finalizeDepMarkers ,触发了delete删除,然后执行 state.coiunt++ 的操作,但是因为依赖已经没有count 了不会执行副作用函数。
如果当依赖层级大于30的时候,会触发cleanupEffect
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
那么计算属性是什么逻辑呢?
我们看看计算属性的源码
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做了什么。
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。
很好,看的很仔细,但不够仔细,我们看看第二个参数是什么。
() => queueJob(update),
是一个调度函数包装的update,最终会调用的update函数。那么update函数大家估计有印象了,我贴一下代码
const update: SchedulerJob = (instance.update = () => effect.run())
// ...
update()
也就是说componentUpdateFn的effect,经常调用的是scheduler,而scheduler其实是包装过的run。本质还是run,而run 最后执行了 componentUpdateFn。
因此,对componentUpdateFn的effect来说,scheduler约等于run,但别的effect不一定。
比如计算属性的这个。
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。
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改变后,才会执行dep,让他使用与effect有关的最新的值。比如上文的计算属性,通过触发triggerRefValue,来让自己的dep获取自己最新的value值。
有的人就会问了,既然计算属性依靠的也是effect,那么岂不是在收集依赖的时候,effect可能存在先于计算属性执行的情况(因为effect是立刻执行)?写在计算属性下面的effect,获取上面的计算属性可能获取的是缓存值?这与vue2的直觉不符——计算属性的计算应该在生命周期非常靠前的。
是这样的,因此在triggerEffects里面用了两次循环,特殊处理了计算属性,优先循环是计算属性的effect,优先执行他的scheduler,让他的_dirty 为false,然后再执行依赖他的effects,然后再优先执行里面的计算属性的effect,当_dirty 为false的时候,这样获取这个计算属性的value的时候,就会执行他的run方法,获取他最新的值。
举个例子
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的源码。
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具体实现了
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(下文会讲)
官方给予的使用场景是
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传了函数,还会执行这个函数。
小结
这节我们讲了effect相关的逻辑——包依赖收集、依赖清理、相关函数的实现逻辑,来让我们对整个vue的响应式更进一步了解,当然,这还不够,我们还需要大致了解编译器以及人任务队列相关知识,以及vue内部组件的实现原理,因此需要学习的东西还有很多,我们依然在路上。