本文基于Vue 3.2.30
版本源码进行分析
为了增加可读性,会对源码进行删减、调整顺序、改变的操作,文中所有源码均可视作为伪代码
由于ts版本代码携带参数过多,大部分伪代码会采取js的形式展示,而不是原来的ts代码
本文重点关注依赖收集和派发更新的流程,为了简化流程,会以Object
为基础分析响应式的流程,不会拘泥于数据类型不同导致的依赖收集和派发更新逻辑不同
本文内容
Proxy
和Reflect
的介绍- 整体依赖收集的流程图以及针对流程图的相关源码分析
- 整体派发更新的流程图以及针对流程图的相关源码分析
ReactiveEffect
核心类的相关源码分析和流程图展示computed
的相关分析,分为流程图和针对流程图的相关源码分析watch
和watchEffect
的初始化源码分析,包括各种配置参数以及schedule
watch
和watchEffect
依赖收集和派发更新流程图展示
每一个点都有配套的流程图,源码分析是为流程图服务,结合流程图看源码分析效果更佳
前置知识
在Vue2源码-响应式原理浅析的文章分析中,我们知道Vue2
使用的是Object.defineProperty
的方式,而在Vue3
中使用Proxy
进行代替数据的响应式劫持,下面将简单介绍Vue3
中所使用的Proxy
和Reflect
Proxy介绍
摘录于Proxy - JavaScript | MDN
Proxy
对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
Proxy只能代理对象,不能代理非对象值,比如String、Number、String、undefined、null等原始值
基础语法
// handler还有其它方法,下面示例只摘取Vue3中比较常用的5个方法
const handler = {
get(target: Target, key: string | symbol, receiver: object) { },
set(target: object, key: string | symbol, value: unknown, receiver: object) { },
deleteProperty() { },
has() { },
ownKeys() { }
}
const p = new Proxy(target, handler);
参数
target
要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
handler
一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为
Reflect的介绍
摘录于Reflect - JavaScript | MDN
Reflect
是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers(en-US)的方法相同。
常用方法的语法
Reflect.set(target: object, propertyKey: PropertyKey, value: any, receiver?: any): boolean;
Reflect.get(target: object, propertyKey: PropertyKey, receiver?: any): any;
在Vue3中的作用
Reflect的一些方法等价于Object方法的效果
Reflect
在Vue3
中用来代替对象的一些方法操作,如下面代码所示,直接访问obj.xxx
与Reflect.get(obj, "xxx", obj)
的效果是一样的,唯一不同点是Reflect
的第三个参数,当与Proxy
一起使用时,receiver
是代理target
的对象
const obj = {count: 1}
const temp = obj.count; // 1
// 上面代码等价于下面的代码
const temp1 = Reflect.get(obj, "count", obj); // 1
Reflect的一些方法具有操作的返回值,而Object具备同等效果的方法没有返回值
Reflect.set()
:如果在对象上成功设置了属性,则Reflect.set()返回true,否则返回false。如果目标不是Object,则抛出TypeErrorReflect.get()
:Reflect.get()返回属性的值。如果目标不是Object,则抛出TypeErrorReflect.deleteProperty()
:如果属性从对象中删除,则Reflect.deleteProperty()返回true,否则返回false
改变特殊原始对象的this
指针问题
从下面的代码可以知道,当有一些原始Object
存在this
指针时,如果没有使用Reflect
处理this
对象时,会导致effect()
无法正常收集依赖的情况
比如下面的代码块,我们理想中的状态是p1.count1
->get count1()
->return this.count
->打印出9999
,但是实际打印的确是22
,此时的get count1() {}
的this
指向obj
,而不是p1
const obj = {
count: 22,
get count1() {
console.log("count1")
// 此时的this指向obj,而不是p
return this.count;
}
}
const p = new Proxy(obj, {
get(target, key, receiver) {
// target: 原始对象,此时为obj
// key: 触发的key
// receiver: proxy对象,此时为p
return target[key];
}
});
const p1 = {
__proto__: p,
count: 9999
}
console.log(p1.count1); // 22
因此我们可以建立下面的代码块,理想中,我们在effect
中打印出p.count1
,理想情况下,当p.count
改变时,我们希望effect
重新执行,输出最新的p.count1
也就是p.count
的值。但是实际上什么都没有发生,因为console.log(p.count1)
并不会触发p.count
的依赖收集,因为下面代码中实际访问的是obj.count
,而不是p.count
,没有依赖收集,p.count
自然也不会有派发更新通知effect
重新执行
const obj = {
count: 22,
get count1() {
console.log("count1")
// 此时的this指向obj,而不是p
return this.count;
}
}
const p = new Proxy(obj, {
get(target, key, receiver) {
// target: 原始对象,此时为obj
// key: 触发的key
// receiver: proxy对象,此时为p
return target[key];
}
});
effect(()=> {
// 从上面p1的例子可以看出
// 此时的count1中的this指向obj,会导致p.count无法收集该effect,导致p.count更新时无法通知到effect重新执行
console.log(p.count1);//22
});
onMounted(()=> {
p.count = 66666; // 不会触发上面的effect重新执行
});
当使用了Reflect
之后,我们就可以使用receiver
参数明确调用主体,把代理对象p
当作this
的主体,防止很多意外可能性发生(如下面的代码)
const obj = {
count: 22,
get count1() {
return this.count;
}
}
const p = new Proxy(obj, {
get(target, key, receiver) {
// target: 原始对象,此时为obj
// key: 触发的key
// receiver: proxy对象,此时为p
return Reflect.get(target, key, receiver);
}
});
const p1 = {
__proto__: p,
count: 9999
}
console.log(p1.count); // 9999
依赖收集
流程图
基本流程
使用Proxy
进行target
对象的代理初始化,由上面前置知识可知,proxy
会劫持target
的多个方法,下面代码中使用mutableHandlers
为Proxy
的handler
方法
// packages/reactivity/src/reactive.ts
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
)
}
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
//......一系列的处理,包括判断target是否已经是Proxy,target是不是对象,已经有proxyMap缓存等等
const proxy = new Proxy(
target,
// TargetType.COLLECTION = Map/Set/WeakMap/WeakSet
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
return proxy
}
从下面代码可以看出,Object
类型最终依赖收集的核心方法是track()
方法
如果res=target[key]仍然是Object,则继续调用reactive()方法进行包裹,因此reactive(Object)中Object的所有key,包括深层的key都具备响应式
最终createGetter()返回的是原始值(Number/String等)或者是一个Proxy(包裹了Object)类型的值
// packages/reactivity/src/baseHandlers.ts
const mutableHandlers: ProxyHandler<object> = {
get, // const get = createGetter()
set,
deleteProperty,
has,
ownKeys
}
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
//...省略Array的处理,后面再分析
//...省略isReadonly类型的处理 + shallow类型的处理 + ref数据类型的处理,后面再分析
const res = Reflect.get(target, key, receiver)
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
if (isObject(res)) { return isReadonly ? readonly(res) : reactive(res) }
return res
}
}
track()核心方法
shouldTrack和activeEffect将在下一小节中讲解,目前认为shouldTrack就是代表需要收集依赖的标志,activeEffect代表目前正在执行的函数(函数中有响应式数据,触发依赖收集)
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) {
// const dep = new Set<ReactiveEffect>(effects) as Dep
// dep.w = 0
// dep.n = 0
depsMap.set(key, (dep = createDep()))
}
trackEffects(dep)
}
}
从下面代码块可以知道,使用dep.n
作为标志位,监测是否需要进行依赖收集,如果需要收集,则进行effect
和当前响应式对象所持有的dep
的互相绑定
dep.add(activeEffec)
activeEffect.deps.push(dep)
dep.n
和dep.m
以及effectTrackDepth和maxMarkerBits是为了优化 每次执行effect函数都需要先清空所有依赖,然后再收集依赖的流程。因为有些依赖是一直没有变化的,不需要每次都清除,具体分析将放在下面分析
function trackEffects(dep: Dep) {
let shouldTrack = false
if (effectTrackDepth <= maxMarkerBits) {
if (!newTracked(dep)) {
dep.n |= trackOpBit // set newly tracked
shouldTrack = !wasTracked(dep)
}
} else {
// Full cleanup mode.
shouldTrack = !dep.has(activeEffect!)
}
if (shouldTrack) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
}
}
派发更新
流程图
基本流程
由上面依赖收集的流程可以知道,Proxy
劫持了对象的set()
方法,实际上就是createSetter()
方法
// packages/reactivity/src/baseHandlers.ts
const mutableHandlers: ProxyHandler<object> = {
get, // const get = createGetter()
set, // const set = createSetter()
deleteProperty,
has,
ownKeys
}
从下面的代码,我们可以看出,最终createSetter()
方法触发的逻辑主要有
Reflect.set()
进行原始值的数据更新- 获取要触发
trigger()
的类型:判断key
是否存在target
上,如果不是,后面将使用TriggerOpTypes.SET
类型,否则就使用TriggerOpTypes.ADD
类型 - 判断
target === toRaw(receiver)
,处理target
以及target.__proto
都是Proxy()
对象导致访问target.xxx
时触发effect()
函数调用两次的问题(在文章最后一小节会分析,这里先放过这些细节问题) - 使用
Object.is()
判断新旧值是否改变,包括NaN
;只有改变时才会触发trigger()
方法
function createSetter(shallow = false) {
return function set(target: object, key: string | symbol, value: unknown, receiver: object): boolean {
//... 省略ref、shallow、readonly的数据处理逻辑
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(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
// packages/shared/src/index.ts
export const hasChanged = (value: any, oldValue: any): boolean =>
!Object.is(value, oldValue)
trigger()核心方法
export function trigger(...args) {
const depsMap = targetMap.get(target)
if (!depsMap) {
return
}
let deps: (Dep | undefined)[] = []
// ...省略逻辑:根据TriggerOpTypes类型,比如CLEAR、SET、ADD、DELETE、Map.SET等等去获取对应的deps集合
if (deps.length === 1) {
if (deps[0]) {
triggerEffects(deps[0])
}
} else {
const effects: ReactiveEffect[] = []
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
triggerEffects(createDep(effects))
}
}
function triggerEffects(dep: Dep | ReactiveEffect[]) {
for (const effect of isArray(dep) ? dep : [...dep]) {
if (effect !== activeEffect || effect.allowRecurse) {
if (effect.scheduler) {
// watch类型的effect调用
effect.scheduler()
} else {
// 直接进行effect()方法的重新执行
effect.run()
}
}
}
}
从上面代码可知,最终trigger()
根据传来的参数,比如增加key
、更新对应key的数据
、删除对应的key
来拼接对应的deps
集合,然后调用对应的deps
所持有的effect
数组集合,比如TriggerOpTypes.ADD
:
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
这种根据type=add/delete/clear/set进行effect集合的拼凑,涉及到Object、Array、Map、Set等多种数据的处理,为了降低本文复杂度,本文不进行展开分析,将在后面的文章进行多种type+多种类型数据的响应式拦截分析
核心ReactiveEffect
为了更好区分Proxy-key持有的deps(Set对象,存储Effect数组)以及effect.deps(Array对象,存储Proxy-key持有的deps)
下面示例代码将改造原来源码,Proxy-key
持有的deps
名称改为:effectsSet
,代表收集effect
集合effect.deps
改为:depsArray
(item是 effectsSet),代表每一个effect
持有的响应式对象的deps
,可以认为所持有的响应式对象
初始化
将目前需要执行的fn
和scheduler
传入
var ReactiveEffect = class {
constructor(fn, scheduler = null, scope) {
this.fn = fn;
this.scheduler = scheduler;
this.active = true;
this.depsArray = [];
this.parent = void 0;
recordEffectScope(this, scope);
}
};
核心源码
run() {
if (!this.active) {
return this.fn();
}
let parent = activeEffect;
let lastShouldTrack = shouldTrack;
while (parent) {
// v3.2.29旧版本代码为下面这一行:
//if (!effectStack.length || !effectStack.includes(this)) {}
if (parent === this) {
return;
}
parent = parent.parent;
}
try {
this.parent = activeEffect;
activeEffect = this;
shouldTrack = true;
return this.fn();
} finally {
activeEffect = this.parent;
shouldTrack = lastShouldTrack;
this.parent = void 0;
if (this.deferStop) {
this.stop();
}
}
}
普通effect
执行Proxy.set
的源码处理
示例
B(effect)
触发了proxy.count = new Date().getTime()
,会触发A(effect)
重新执行,不会触发B(effect)
重新执行
// A
effect(() => {
console.error("测试A:" + proxy.count);
});
// B
effect(() => {
console.error("测试B:" + proxy.count);
proxy.count = new Date().getTime();
});
源码分析
由上面派发更新的源码流程可以知道,最终派发更新触发的流程是trigger()
->triggerEffects()
->ReactiveEffect.run()
从下面源码可以知道,如果只是简单在effect
中执行proxy.count=xxx
的set
操作,那么由于triggerEffects()
源码中会进行effect!==activeEffect
的判断,会阻止在当前effect
进行依赖收集又同时进行依赖更新的事情,因此上面示例中的console.error("测试B:" + proxy.count)
被阻止执行
普通effect
执行Proxy.set
的源码处理不太关ReactiveEffect.run()
的事情,这里讲解普通effect
执行Proxy.set
的源码处理是为了下面的嵌套effect
铺垫
function triggerEffects(dep, debuggerEventExtraInfo) {
// spread into array for stabilization
for (const effect of isArray(dep) ? dep : [...dep]) {
if (effect !== activeEffect || effect.allowRecurse) {
if (effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo));
}
if (effect.scheduler) {
effect.scheduler();
}
else {
effect.run();
}
}
}
}
嵌套effect 和 在嵌套effect执行Proxy.set的处理
嵌套effect特殊情况示例
为了避免嵌套effect()
产生的依赖收集错乱,比如下面示例代码所示,我们需要将obj.count
与内层的effect()
关联,将obj.count1
与外层的effect()
关联,当obj.count
发生改变时,触发内层effect重新执行一次,外层effect不执行
const obj = new Proxy({count: 1, count1: 22});
effect(()=> {
effect(()=> {
console.log(obj.count);
});
console.log(obj.count1);
});
obj.count=22; // 触发内层effect重新执行一次,外层effect不执行
嵌套effect特殊情况源码分析
由上面派发更新的源码流程可以知道,最终派发更新触发的流程是trigger()
->triggerEffects()
->ReactiveEffect.run()
为了处理嵌套effect,如下面精简源码所示,Vue3
在ReactiveEffect.run()
执行this.fn()
前,会将上次的activeEffect
和shouldTrack
状态保存,执行this.fn()
完毕后,将状态还原到上一个状态,实现一种分支切换的功能
run() {
let parent = activeEffect;
let lastShouldTrack = shouldTrack;
try {
this.parent = activeEffect;
activeEffect = this;
shouldTrack = true;
return this.fn();
} finally {
activeEffect = this.parent;
shouldTrack = lastShouldTrack;
this.parent = void 0;
}
}
而this.fn()
的执行,实际上就是重新执行一遍带有响应式变量的方法,这个时候会触发响应式变量Proxy
的get()
方法,从而触发上面分析的依赖收集
,然后触发核心的track()
方法,如下面代码所示,在嵌套effect
中的activeEffect
就是每一个嵌套子effect
,保证了响应式变量依赖收集到正确的effect
中
trackEffects()
方法内也有一个shouldTrack
变量->局部变量,track()
方法内的shouldTrack
变量->全局变量,不要搞混....
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) {
// const dep = new Set<ReactiveEffect>(effects) as Dep
// dep.w = 0
// dep.n = 0
depsMap.set(key, (dep = createDep()))
}
trackEffects(dep)
}
}
在嵌套effect执行Proxy.set特殊情况讲解
如下面两个代码块所示,如果Vue源码
不进行处理,比如github调试代码所示,当B(effect)
执行Proxy.set()
操作时,会触发A(effect)
的重新执行,而A(effect)
中具有Proxy.set()
操作时,又会触发B(effect)
的重新执行,从而形成无限递归循环调用
- 代码块1:
proxy.count
=proxy.count
+11111111
===>B(effect)
->A(effect)
->B(effect)
- 代码块2:
proxy.count
=new Date().getTime()
===>B(effect)
->A(effect)
->B(effect)
// A(effect)
effect(() => {
console.error("测试:" + proxy.count);
proxy.count = 5;
});
// B(effect)
effect(() => {
// proxy.count = 111只会触发set,不会触发parent返回逻辑
// proxy.count = proxy.count + 11111111既触发get,也触发set,有进行依赖收集
proxy.count = proxy.count + 11111111;
});
// proxy.count = proxy.count + 11111111 ===> B(effect)->A(effect)->B(effect)
// proxy.count = 333 =====> B(effect)->A(effect)
// A(effect)
effect(() => {
console.error("测试:" + proxy.count);
// B(effect)
effect(() => {
proxy.count = new Date().getTime();
});
});
// A(effect)->B(effect)->A(effect)
在嵌套effect执行Proxy.set特殊情况源码分析
由上面派发更新的源码流程可以知道,最终派发更新触发的流程是trigger()
->triggerEffects()
->ReactiveEffect.run()
从下面源码可以看出,当我们执行ReactiveEffect.run()
时,会使用this.parent=activeEffect
,然后再执行this.fn()
当嵌套effect
发生时,上一个嵌套effect
就是子effect
的parent
,比如上面示例代码块2中,A(effect)
就是B(effect)
的parent
,即B(effect)
的this.parent=A(effect)
,因此A(effect)->B(effect)->A(effect)
的流程中,会因为parent === this
而直接中断整个无限递归的发生
run() {
if (!this.active) {
return this.fn();
}
let parent = activeEffect; // 上一个Effect
let lastShouldTrack = shouldTrack;
while (parent) {
if (parent === this) {
// B(effect).parent=A(effect)
// this=A(effect)
return;
}
parent = parent.parent;
}
try {
this.parent = activeEffect;
//.....
return this.fn();
}
}
cleanupEffect()清空effect和优化逻辑
全部清除
为了避免类似v-if
/currentStatus?obj.count:obj.data
这种切换状态后依赖过期的情况,我们可以在每次依赖收集时进行依赖的清空,然后再收集依赖cleanupEffect()
提供了清除全部effect
的能力,在effectTrackDepth > maxMarkerBits
/ ReactiveEffect.stop()
时调用
function cleanupEffect(effect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
全部清除优化
每次执行依赖收集的过程中,都会进行cleanup(),但是在一些场景下,依赖关系是很少变化的,为了减少每次依赖收集时effect
的添加和删除操作,我们需要标识每一个依赖集合dep
的状态,标识它是新收集的,还是已经被收集过的,对这种清空依赖的逻辑进行优化
如下面代码所示,我们为Proxy-key持有的dep
(响应式数据持有的effect
集合)增加两个属性dep.w
和dep.n
dep.w
:代表在某个递归深度下,是否被收集的标志位dep.h
:代表在某个递归深度下,最新状态下是否被收集的标志位
如果dep.w成立,dep.h不成立,说明该响应式数据在该effect
已经过期,应该删除
function track(target, type, key) {
if (shouldTrack && activeEffect) {
// ....
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, dep = createDep());
}
//...
}
}
// packages/reactivity/src/dep.ts
var createDep = (effects) => {
const dep = new Set(effects);
dep.w = 0;
dep.n = 0;
return dep;
};
initDepMarkers()触发dep.w(effectsSet.w)的构建
我们在ReactiveEffect.run()
中使用三个属性进行递归深度的标识
- trackOpBit:表示递归嵌套执行
effect
函数的深度,使用二进制的形式,比如10
、100
、1000
、10000
- effectTrackDepth:表示递归嵌套执行
effect
函数的深度,使用十进制的形式,比如1
、2
、3
、4
- maxMarkerBits:表示递归嵌套的最大深度,默认为30,跟
effectTrackDepth
进行比较
为了更好区分Proxy-key持有的deps(Set对象,存储Effect数组)以及effect.deps(Array对象,存储Proxy-key持有的deps)
下面示例代码将改造原来源码,Proxy-key持有的deps名称改为:effectsSet
effect.deps改为:depsArray
run() {
// ...省略
try {
this.parent = activeEffect;
activeEffect = this;
shouldTrack = true;
trackOpBit = 1 << ++effectTrackDepth;
if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this);
} else {
cleanupEffect(this);
}
return this.fn();
} finally {
if (effectTrackDepth <= maxMarkerBits) {
finalizeDepMarkers(this);
}
trackOpBit = 1 << --effectTrackDepth;
activeEffect = this.parent;
shouldTrack = lastShouldTrack;
this.parent = void 0;
if (this.deferStop) {
this.stop();
}
}
}
从上面的代码可知,首先触发了initDepMarkers()
进行该effect
持有的depsArray
进行depsArray[i].w |= trackOpBit
的标记,其中depsArray[i]
就是响应式对象所持有的effect集合
,为了方便理解,我们看作depsArray[i]
就是一个响应式对象,为每一个响应式对象的w属性
,进行响应式对象.w |= trackOpBit
的标记
var initDepMarkers = ({ depsArray }) => {
if (depsArray.length) {
for (let i = 0; i < depsArray.length; i++) {
// depsArray[i] = effectsSet(每一个target的key进行createDep()创建的Set数组)
// depsArray[i].w = effectsSet.w
depsArray[i].w |= trackOpBit;
}
}
};
Effect.run()-this.fn()执行触发Proxy对象的get()请求,从而触发依赖收集,使用dep.n(effectsSet.n)标识最新情况的依赖
为了更好区分Proxy-key持有的deps(Set对象,存储Effect数组)以及effect.deps(Array对象,存储Proxy-key持有的deps)
下面示例代码将改造原来源码,Proxy-key持有的deps名称改为:effectsSet
effect.deps改为:depsArray
- 从下面的代码可以知道,我们使用
effectsSet.n |= trackOpBit
进行目前effect
所持有的响应式对象的标记,如果最新一轮依赖收集已经标记过(即newTracked(effectsSet)=true
),那就不用标记dep.n
了,也不用新增追踪(即shouldTrack2=false
) - 如果没有标记过(即
newTracked(effectsSet)=false
),那就进行标记,并且判断之前是否已经收集过shouldTrack2 = !wasTracked(dep)
- 以上两种是
effectTrackDepth <= maxMarkerBits
的情况,当超过递归深度时,执行shouldTrack2 = !effectsSet.has(activeEffect)
,因为超过递归深度,所有effectsSet.w
都会失效了(ReactiveEffect.run
调用了cleanupEffect()
)
function track(target, type, key) {
// ...
trackEffects(effectsSet, eventInfo);
}
function trackEffects(effectsSet, debuggerEventExtraInfo) {
let shouldTrack2 = false;
if (effectTrackDepth <= maxMarkerBits) {
//newTracked=(dep)=>(effectsSet.n & trackOpBit)>0
if (!newTracked(effectsSet)) {
effectsSet.n |= trackOpBit;
shouldTrack2 = !wasTracked(dep);
}
} else {
shouldTrack2 = !effectsSet.has(activeEffect);
}
// ...
}
执行Effect.run()-this.fn()完成后,执行finalizeDepMarkers()方法,根据dep.w和dep.n进行过期dep的筛选
从下面代码可以知道,
- 如果
wasTracked(dep)=true && newTracked(dep)=false
,说明该effect
已经不依赖这个响应式对象了,直接进行响应式对象的dep.delete(effect)
,这里的dep
是响应式对象持有的effect集合
,也就是我们分析改造名称的effectsSet
- 如果上面条件不成立,说明对于
effect()
函数来说,这个响应式对象是新增的/之前已经存在,现在仍然需要,则执行deps[ptr++] = dep
,这里的deps
是effect
所持有的depsArray
var finalizeDepMarkers = (effect2) => {
const { deps } = effect2;
if (deps.length) {
let ptr = 0;
for (let i = 0; i < deps.length; i++) {
const dep = deps[i];
if (wasTracked(dep) && !newTracked(dep)) {
// wasTracked:var wasTracked = (dep) => (dep.w & trackOpBit) > 0;
// newTracked: var newTracked = (dep) => (dep.n & trackOpBit) > 0;
dep.delete(effect2);
} else {
deps[ptr++] = dep;
}
dep.w &= ~trackOpBit;// 将目前递归深度对应那一个bit置为0
dep.n &= ~trackOpBit;// 将目前递归深度对应那一个bit置为0
}
deps.length = ptr;
}
};
ReactiveEffect流程图总结
computed类型响应式分析
例子
<div id='el'>
{{computedData}}
</div>
<script>
const { effect, onMounted, reactive, createApp, computed } = Vue;
const App = {
setup(props, ctx) {
const obj = {count: 1};
const proxy = reactive(obj);
const computedData = computed(()=> {
return proxy.count+1;
});
return {
computedData
};
},
};
const app = createApp(App);
app.mount("#el");
</script>
依赖收集流程图总结
依赖收集流程图源码分析
computed初始化
- 判断传入的getter是否是函数,如果传入的是函数,则手动设置一个空的
setter()
方法 - 创建ComputedRefImpl实例
function computed(getterOrOptions, debugOptions, isSSR = false) {
let getter;
let setter;
const onlyGetter = isFunction(getterOrOptions);
if (onlyGetter) {
getter = getterOrOptions;
setter = () => {
console.warn('Write operation failed: computed value is readonly');
};
}
else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR);
return cRef;
}
实际核心ComputedRefImpl初始化
初始化ReactiveEffect,传入getter
作为fn
,以及设置scheduler
class ComputedRefImpl {
constructor(getter, _setter, isReadonly, isSSR) {
// ...省略代码
this._dirty = true;
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true;
triggerRefValue(this);
}
});
this.effect.computed = this;
}
get value() {
const self = toRaw(this);
trackRefValue(self);
if (self._dirty || !self._cacheable) {
self._dirty = false;
self._value = self.effect.run();
}
return self._value;
}
}
class ReactiveEffect {
constructor(fn, scheduler = null, scope) {
this.fn = fn;
this.scheduler = scheduler;
}
}
依赖收集:ComputedRefImpl初始化数据
- 由上面的示例可以知道,最终界面会触发
ComputedRefImpl.value
的获取,触发依赖收集 - 获取数据时,会触发
trackRefValue()
进行依赖收集,如果compute data
在界面渲染effect
中,此时的activeEffect
就是界面渲染effect
,ComputedRefImpl.dep
收集界面渲染effect
function trackRefValue(ref) {
trackEffects(ref.dep || (ref.dep = createDep()));
}
function trackEffects(effectsSet, debuggerEventExtraInfo) {
let shouldTrack2 = false;
if (effectTrackDepth <= maxMarkerBits) {
//newTracked=(dep)=>(dep.n & trackOpBit)>0
if (!newTracked(effectsSet)) {
effectsSet.n |= trackOpBit;
shouldTrack2 = !wasTracked(dep);
}
} else {
shouldTrack2 = !dep.has(activeEffect);
}
if (shouldTrack) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
}
}
- 将
self._dirty
置为false
,并且触发self.effect.run()
,此时会触发this.fn()
,即proxy.count+1
这个computed
初始化时的方法执行,最终run()
返回this.fn()
,即proxy.count+1
的值 - 访问
proxy.count+1
时会触发Proxy的get()方法
,从而触发响应式数据的依赖收集,由上面依赖收集的流程分析可以知道,会执行track()->trackEffects()
,最终将目前的computed effect加入到响应式数据的dep中
- 此时
computed effect
的依赖收集结束
run() {
if (!this.active) {
return this.fn();
}
let parent = activeEffect;
let lastShouldTrack = shouldTrack;
while (parent) {
// v3.2.29旧版本代码为:
//if (!effectStack.length || !effectStack.includes(this)) {}
if (parent === this) {
// 如果在嵌套effect中发现之前已经执行过目前的effect
// 则阻止执行,避免无限嵌套执行
return;
}
parent = parent.parent;
}
try {
this.parent = activeEffect;
activeEffect = this;
shouldTrack = true;
return this.fn();
} finally {
activeEffect = this.parent;
shouldTrack = lastShouldTrack;
this.parent = void 0;
if (this.deferStop) {
this.stop();
}
}
}
派发更新流程图
派发更新流程图源码分析
ComputedRefImpl数据变化
- 如果
computed effect
里面的响应式数据发生变化,由上面派发更新的分析可以知道,会触发trigger->triggerEffects
,最终触发effect.scheduler()
的执行
function trigger(target, type, key, newValue, oldValue, oldTarget) {
//...省略deps的拼接判断逻辑
const effects = [];
for (const dep of deps) {
if (dep) {
effects.push(...dep);
}
}
triggerEffects(createDep(effects), eventInfo);
}
function triggerEffects(dep, debuggerEventExtraInfo) {
for (const effect of isArray(dep) ? dep : [...dep]) {
if (effect !== activeEffect || effect.allowRecurse) {
if (effect.scheduler) effect.scheduler();
else effect.run();
}
}
}
- 即下面的代码,将
this._dirty
置为true,手动触发triggerRefValue->triggerEffects
,此时触发的是ComputedRefImpl.dep的effects
,也就是computed对象收集的渲染effects
执行 渲染effect重新执行
,会重新触发响应式对象的get()方法
,即访问computed.value
数
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true;
triggerRefValue(this);
}
});
function triggerRefValue(ref, newVal) {
ref = toRaw(ref);
if (ref.dep) {
// 上面依赖收集的ComputedRefImpl.dep对象
// 收集的是渲染effect
triggerEffects(ref.dep);
}
}
function triggerEffects(dep: Dep | ReactiveEffect[]) {
for (const effect of isArray(dep) ? dep : [...dep]) {
if (effect !== activeEffect || effect.allowRecurse) {
if (effect.scheduler) {
effect.scheduler()
} else {
// 直接进行effect()方法的重新执行
effect.run()
}
}
}
}
从下面的代码可知,当访问get value()
时,由于已经将self._dirty
置为true
,因此后续的流程跟上面分析的computed依赖收集的流程
一致,最终返回新的计算的effect.run()
的值
class ComputedRefImpl {
constructor(getter, _setter, isReadonly, isSSR) {
// ...省略代码
this._dirty = true;
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true;
triggerRefValue(this);
}
});
this.effect.computed = this;
}
get value() {
const self = toRaw(this);
trackRefValue(self);
if (self._dirty || !self._cacheable) {
self._dirty = false;
self._value = self.effect.run();
}
return self._value;
}
}
computed依赖数据没有变化
由下面代码可以发现,当第一次渲染完成,调用.value
数据后,self.dirty
会置为false,因此当computed依赖的数据没有变化时
,会一直返回之前计算出来的self._value
,而不是触发重新计算self.effect.run()
class ComputedRefImpl {
get value() {
const self = toRaw(this);
trackRefValue(self);
if (self._dirty || !self._cacheable) {
self._dirty = false;
self._value = self.effect.run();
}
return self._value;
}
}
watch类型响应式分析
初始化
整体概述
function watch(source, cb, options) {
return doWatch(source, cb, options);
}
function watchEffect(effect, options) {
return doWatch(effect, null, options);
}
- 拼接
getter
数据,将传入的数据、ref
、reactive
进行整理形成规范的数据(下文会详细分析) cleanup
:注册清除方法,在watchEffect
的source
触发执行/watch
监听值改变时,会暴露出一个onCleanup()
方法,我们可以通过onCleanup
传入一个fn()
方法,在适当的时机会调用清除目前的监听方法(下文会详细分析)- 初始化
job()
: 根据cb
构建回调,主要是根据oldValue
和newValue
进行回调(下文会详细分析) - 根据
flush('pre'、'sync'、'post')
构建scheduler
(下文会详细分析) - 初始化
new ReactiveEffect(getter, scheduler)
初始化运行:根据传入的参数
- 如果是
watch
类型,并且是immediate
立刻执行job()
,立刻触发cb(newValue, undefined)
回调,然后更新oldValue
=newValue
- 如果是
watch
类型,immediate=false
,则计算结果缓存oldValue
- 如果不是
watch
类型(还有watchEffect
、watchPostEffect
、watchSyncEffect
),并且flush === 'post'
,也就是watchPostEffect
时,不立刻执行effect.run()
,而是推入post
队列延后执行 - 如果是
watchEffect
类型,初始化就会执行一次,直接运行effect.run()
- 如果是
- 返回
function
:调用可以立刻停止watch
监听,会同时调用上面通过onCleanup(fn)
注册的方法fn()
function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ) {
// 初始化job(): 根据cb构建回调,主要是根据oldValue和newValue进行回调
let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE;
const job = () => {
// ...改写代码,只展示核心部分
if (cleanup) { cleanup(); }
const newValue = effect.run();
let currentOldValue = oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue;
// 最终调用 args = [newValue, oldValue, onCleanup]
// res = args ? cb(...args) : cb();
callWithAsyncErrorHandling(cb, instance, 3, [
newValue,
currentOldValue
onCleanup
]);
oldValue = newValue;
}
// ...拼接getter数据
// ...省略代码,根据flush('pre'、'sync'、'post')构建scheduler
const effect = new ReactiveEffect(getter, scheduler);
// initial run
if (cb) {
if (immediate) job();
else oldValue = effect.run();
} else if (flush === 'post') {
queuePostRenderEffect(effect.run.bind(effect), instance && instance.suspense);
} else {
effect.run();
}
return () => {
effect.stop();
};
}
getter构建流程分析
如果是ref
数据,则解构出source.value
值,监测是否是isShallow
,如果是的话,则forceTrigger=true
isShallow类型相关分析将在后面文章统一介绍
if (isRef(source)) {
getter = () => source.value;
forceTrigger = isShallow(source);
}
如果是reactive
数据,则触发深度遍历,为所有key
都进行依赖手机,目前的activeEffect
是watch类型的effect
else if (isReactive(source)) {
getter = () => source;
deep = true;
}
如果是function
类型,需要判断目前是watch
/watchEffect
类型的effect
通过cb是否存在来区分watch/watchEffect类型
watch
:getter()
就是source()
的执行,也就是watch(()=>count.value, cb)
中的count.value
watchEffect
:getter()
就是watchEffect(fn)
中的fn
方法,从下面源码可以知道,一开始会执行fn(onCleanup)
如果watch直接监听ref/reactive,会进行.value/深度遍历object,如果我们想要只监听一个对象的某一个深度属性,我们可以直接使用function类型,直接watch(()=> obj.count, cb),那么依赖最终只会收集obj.count,而不会整个obj都进行依赖收集
else if (isFunction(source)) {
if (cb) {
// getter with cb
getter = () => callWithErrorHandling(source, instance, 2 /* WATCH_GETTER */);
}else {
// no cb -> simple effect
getter = () => {
// ...
if (cleanup) {
cleanup();
}
// 最终调用 args = [onCleanup]
// callWithAsyncErrorHandling => res = source(...[onCleanup]);
return callWithAsyncErrorHandling(source, instance, 3 /* WATCH_CALLBACK */, [onCleanup]);
};
}
}
如果是数组数据,即有多个响应式对象时,需要判断是否是ref
、reactive
、function
类型,然后根据上面3种进行处理,getter()
得到的就是一个处理好的数组情况,比如[count.value, Proxy(object), wactch中监听的source, watchEffect(onCleanup)]
function类型: watch(()=>count.value, ()=> {})中,()=>count.value就是function类型
else if (isArray(source)) {
isMultiSource = true;
forceTrigger = source.some(isReactive);
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, 2 /* WATCH_GETTER */);
}
else {
warnInvalidSource(s);
}
});
}
job()构建流程分析
- 根据
effect.active
判断目前的effect
的运行状态 - 根据是否有
cb
判断是正常的watch
还是watchEffect
,如果是watch
,直接effect.run()
获取最新的值 - 如果是
watchEffect
,直接运行effect.run()
,即watchEffect(()=> {xxx})
传入的函数执行
const job = () => {
if (!effect.active) {
return;
}
if (cb) {
// watch(source, cb)
const newValue = effect.run();
if (deep || forceTrigger ||
(isMultiSource ?
newValue.some((v, i) => hasChanged(v, oldValue[i]))
: hasChanged(newValue, oldValue)
)
) {
// cleanup before running cb again
if (cleanup) {
cleanup();
}
callWithAsyncErrorHandling(cb, instance, 3 /* WATCH_CALLBACK */, [
newValue,
// pass undefined as the old value when it's changed for the first time
oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
onCleanup
]);
oldValue = newValue;
}
}
else {
// watchEffect
effect.run();
}
};
scheduler构建流程分析
从下面代码可以总结出,一共有三种调度模式
sync
:同步执行,数据变化时同步执行回调函数post
:调用queuePostRenderEffect
运行job
,在组件更新后才执行pre
:判断目前是否已经isMounted
,如果已经isMounted=true
,则调用queuePreFlushCb(job)
;如果还没完成mounted
,则直接运行,即第一次在组件挂载之前执行回调函数,之后更新数据则在组件更新之前调用执行函数
if (flush === 'sync') {
scheduler = job; // the scheduler function gets called directly
} else if (flush === 'post') {
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense);
} else {
// default: 'pre'
scheduler = () => {
if (!instance || instance.isMounted) {
queuePreFlushCb(job);
}
else {
// with 'pre' option, the first call must happen before
// the component is mounted so it is called synchronously.
job();
}
};
}
queuePostRenderEffect
&queuePreFlushCb
根据pre
/post
存入不同的队列中,然后触发queueFlush()
function queuePreFlushCb(cb) {
queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex);
}
function queuePostFlushCb(cb) {
queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex);
}
function queueCb(cb, activeQueue, pendingQueue, index) {
if (!isArray(cb)) {
if (!activeQueue ||
!activeQueue.includes(cb, cb.allowRecurse ? index + 1 : index)) {
pendingQueue.push(cb);
}
}
else {
// if cb is an array, it is a component lifecycle hook which can only be
// triggered by a job, which is already deduped in the main queue, so
// we can skip duplicate check here to improve perf
pendingQueue.push(...cb);
}
queueFlush();
}
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true;
currentFlushPromise = resolvedPromise.then(flushJobs);
}
}
flushJobs
- 先执行
flushPreFlushCbs
所有的方法,执行完毕后检查是否还有新的方法插入,重新调用一次flushPreFlushCbs
- 然后执行
queue
所有的方法 - 最后执行
flushPostFlushCbs
所有的方法,然后检查一遍是否有新的item,如果有的话,重新出发一次flushJobs
,不是重新调用一次flushPostFlushCbs
flushPreFlushCbs
不断遍历pendingPreFlushCbs
列表的job
,并触发执行,然后检查一遍是否有新的item添加到pendingPreFlushCbs
,不断循环直到所有pendingPreFlushCbs
的方法都执行完毕
function flushPreFlushCbs(seen, parentJob = null) {
if (pendingPreFlushCbs.length) {
currentPreFlushParentJob = parentJob;
activePreFlushCbs = [...new Set(pendingPreFlushCbs)];
pendingPreFlushCbs.length = 0;
seen = seen || new Map();
for (preFlushIndex = 0; preFlushIndex < activePreFlushCbs.length; preFlushIndex++) {
if (checkRecursiveUpdates(seen, activePreFlushCbs[preFlushIndex])) {
continue;
}
activePreFlushCbs[preFlushIndex]();
}
activePreFlushCbs = null;
preFlushIndex = 0;
currentPreFlushParentJob = null;
// recursively flush until it drains
flushPreFlushCbs(seen, parentJob);
}
}
queue执行
// 父子Component的job()进行排序,父Component先执行渲染
queue.sort((a, b) => getId(a) - getId(b));
const check = (job) => checkRecursiveUpdates(seen, job);
try {
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex];
if (job && job.active !== false) {
if (true && check(job)) {
continue;
}
// console.log(`running:`, job.id)
callWithErrorHandling(job, null, 14 /* SCHEDULER */);
}
}
}
flushPostFlushCbs
- 执行一遍
pendingPostFlushCbs
列表的job
,并触发执行,然后检查一遍是否有新的item,如果有的话,重新出发一次flushJobs
(因为pre
的优先级是最高的,如果因为执行pendingPostFlushCbs
产生了pre
等级的job,那么也应该先执行该job
)
finally {
flushIndex = 0;
queue.length = 0;
flushPostFlushCbs(seen);
isFlushing = false;
currentFlushPromise = null;
// some postFlushCb queued jobs!
// keep flushing until it drains.
if (queue.length ||
pendingPreFlushCbs.length ||
pendingPostFlushCbs.length) {
flushJobs(seen);
}
}
cleanup()清除
onCleanup传入fn,进行cleanup初始化
let cleanup;
let onCleanup = (fn) => {
cleanup = effect.onStop = () => {
callWithErrorHandling(fn, instance, 4 /* WATCH_CLEANUP */);
};
};
watchEffect
初始化getter
时,默认传入[onCleanup]
进行初始化,由于watchEffect
一开始就会调用一次getter
,也就是会执行一次watchEffect(fn1)
的fn1
,我们可以在fn1
中拿到onCleanup
方法,传入fn
// no cb -> simple effect
getter = () => {
if (instance && instance.isUnmounted) {
return;
}
if (cleanup) {
cleanup();
}
return callWithAsyncErrorHandling(source, instance, 3 /* WATCH_CALLBACK */, [onCleanup]);
};
在watchEffect
初始化时,我们可以手动调用onCleanup(()=> {})
传入fn()
watchEffect((onCleanup)=> {
onCleanup(()=> {
// 这是清除方法fn
});
});
watch
初始化job()
时,会在回调函数中进行onCleanup
的暴露
const job = () => {
if (cb) {
const newValue = effect.run();
if (deep || forceTrigger || (isMultiSource
? newValue.some((v, i) => hasChanged(v, oldValue[i]))
: hasChanged(newValue, oldValue))) {
// cleanup before running cb again
if (cleanup) {
cleanup();
}
callWithAsyncErrorHandling(cb, instance, 3 /* WATCH_CALLBACK */, [
newValue,
// pass undefined as the old value when it's changed for the first time
oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
onCleanup
]);
oldValue = newValue;
}
}
};
在watch
初始化时,我们可以在cb
回调函数中手动调用onCleanup(()=> {})
传入fn()
const proxy = reactive({count: 1});
const testWatch = watch(proxy, (newValue, oldValue, onCleanup)=> {
onCleanup(()=> {
console.warn("watch onCleanup fn");
});
});
cleanup执行时机
watchEffect
每次响应式数据发生变化时,会触发ReactiveEffect.scheduler()
->job()
->ReactiveEffect.run()
->this.fn()
->watchEffect getter()
如下面的代码所示,在触发watchEffect(fn)
的fn
之前会先调用一次cleanup()
// no cb -> simple effect
getter = () => {
if (instance && instance.isUnmounted) {
return;
}
if (cleanup) {
cleanup();
}
return callWithAsyncErrorHandling(source, instance, 3 /* WATCH_CALLBACK */, [onCleanup]);
};
watch
每次响应式数据发生变化时,会触发ReactiveEffect.scheduler()
->job()
如下面的代码所示,在触发cb(newValue, oldValue, onCleanup)
回调前会先调用一次cleanup()
const job = () => {
if (cb) {
const newValue = effect.run();
if (deep || forceTrigger || (isMultiSource
? newValue.some((v, i) => hasChanged(v, oldValue[i]))
: hasChanged(newValue, oldValue))) {
// cleanup before running cb again
if (cleanup) {
cleanup();
}
callWithAsyncErrorHandling(cb, instance, 3 /* WATCH_CALLBACK */, [
newValue,
// pass undefined as the old value when it's changed for the first time
oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
onCleanup
]);
oldValue = newValue;
}
}
effect.stop()
在停止watch
监听时触发effect.stop()
->effect.onStop()
->cleanup()
// doWatch初始化
let cleanup;
let onCleanup = (fn) => {
cleanup = effect.onStop = () => {
callWithErrorHandling(fn, instance, 4 /* WATCH_CLEANUP */);
};
};
// Reactive.stop()
stop() {
if (this.active) {
cleanupEffect(this);
if (this.onStop) {
this.onStop();
}
this.active = false;
}
}
依赖收集
派发更新
其它细节分析
createSetter中target === toRaw(receiver)
如下面代码所示,Proxy
的set()
方法中存在着target === toRaw(receiver)
的判断
function createSetter(shallow = false) {
return function set(target: object, key: string | symbol, value: unknown, receiver: object): boolean {
//... 省略ref、shallow、readonly的数据处理逻辑
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(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
// packages/shared/src/index.ts
export const hasChanged = (value: any, oldValue: any): boolean =>
!Object.is(value, oldValue)
target === toRaw(receiver)
的场景如下面代码所示,在effect()
中,会触发proxy.count
的get()
方法,由于proxy
没有count
这个属性,因此会去访问它的prototype
,即baseProxy
- 由于
proxy
和baseProxy
都是响应式对象,因此会触发proxy
和baseProxy
两个对象的dep
收集effect
- 当
proxy.count
改变的时候,触发Reflect.set()
方法,因此会触发proxy
收集的effct
重新执行一次 - 但是由于
proxy
的prototype
才有count
,因此proxy.count
改变的时候,会触发baseProxy.count
的set()
方法执行,从而触发baseProxy
收集的effct
重新执行一次 - 由上面的分析可以知道,如果没有
target === toRaw(receiver)
,proxy.count
改变最终会触发effect()
内部重新执行两次
const obj = {origin: "我是origin"};
const objProto = {count: 1, value: "Proto"};
const proxy = reactive(obj);
const baseProxy = reactive(objProto);
Object.setPrototypeOf(proxy, baseProxy);
effect(()=> {
// 需要把vue.global.js的createSetter()的target === toRaw(receiver)注释掉,然后就会发现触发了effect两次执行
console.error("测试:"+proxy.count);
})
onMounted(()=> {
setTimeout(()=> {
proxy.count = new Date().getTime(); // 触发上面effec执行两次
}, 2000);
});
为了解决上面这种effect()
内部重新执行两次的问题,Vue3
使用了target === toRaw(receiver)
的方式,主要流程是
createSetter()
增加了__v_raw
的判断,因为执行toRaw()
时,本质是获取key === "__v_raw"
,这个时候就直接返回没有进行Proxy
代理前的原始对象obj
function createSetter(shallow = false) {
if (key === "__v_raw" /* RAW */ &&
receiver === (isReadonly2 ? shallow ? shallowReadonlyMap : readonlyMap :
shallow ? shallowReactiveMap : reactiveMap).get(target)) {
return target;
}
}
增加target === toRaw(receiver)
后,我们在vue.config.js
增加打印调试数据
const result = Reflect.set(target, key, value, receiver)
if (target === toRaw(receiver)) {
console.info("target", target);
console.info("receiver", receiver);
if (!hadKey) {
trigger(target, "add" /* ADD */, key, value);
}
else if (hasChanged(value, oldValue)) {
trigger(target, "set" /* SET */, key, value, oldValue);
}
} else {
console.warn("target", target);
console.warn("receiver", receiver);
console.warn("toRaw(receiver)", toRaw(receiver));
}
最终打印结果如下面所示,我们可以发现
- 先触发了
proxy.count
->Reflect.set(target, key, value, receiver)
,此时target=obj
,receiver=proxy
因为
target
没有这个count
属性,因此触发target
的原型获取count
属性,也就是proxy.count
->Reflect.set(target, key, value, receiver)
,此时target=obj
,receiver=proxy
- ======>触发了
Reflect.set(target, key, value, receiver)
,此时target=objProto
,receiver=proxy
,toRaw(receiver)=obj
,此时因为target
不等于toRaw(receiver)
而阻止了trigger
派发更新逻辑- 新的值已经成功设置,然后返回
return result
- ======>
- 继续刚开始的
proxy.count
->Reflect.set(target, key, value, receiver)
,此时target=obj
,receiver=proxy
,toRaw(receiver)=obj
=>成功触发trigger()
warn: target {count: 1, value: 'Proto'}
warn: receiver Proxy {origin: '我是origin', count: 1670680524851}
warn: toRaw(receiver) {origin: '我是origin', count: 1670680524851}
info: target {origin: '我是origin', count: 1670680524851}
info: receiver Proxy {origin: '我是origin', count: 1670680524851}
// const obj = {origin: "我是origin"};
// const objProto = {count: 1, value: "Proto"};
// const proxy = reactive(obj);
// const baseProxy = reactive(objProto);
// Object.setPrototypeOf(proxy, baseProxy);
核心ReactiveEffect类为什么需要标识递归深度
存在下面一种嵌套访问同一个obj.count
的情况
const obj = reactive({count: 1, count1: 22});
effect(()=> {
console.log(obj.count);
effect(()=> {
console.log(obj.count);
});
})
- 由上面
核心ReactiveEffect
的分析可以知道,最终effect.run()
会执行finalizeDepMarkers
进行依赖的最终整理 - 上面例子中最外层的
effect()
访问到obj.count
时,会触发obj.count
收集最外层的effect
- 当内层的
effect()
访问到obj.count
时,由于trackOpBit
已经左移一位,对于同一个响应式对象来说,内层effect
执行触发依赖收集的trackOpBit
跟外层effect
执行触发依赖收集的trackOpBit
值是不同的,因此当有一个effect
的状态发生变化,比如内层effect
有一个v-if
导致obj.count
不用再收集内层effect
时,内外effect
的trackOpBit
值是不同,因此内层effect
不会影响外层effect
的依赖收集(无论是effectsSet.w
还是effectsSet.n
对于内外层effect
来说都是独立的)
function track(target, type, key) {
// ...
let effectsSet = depsMap.get(key);
if (!effectsSet) {
depsMap.set(key, effectsSet = createDep());
}
trackEffects(effectsSet, eventInfo);
}
function trackEffects(effectsSet, debuggerEventExtraInfo) {
let shouldTrack2 = false;
if (effectTrackDepth <= maxMarkerBits) {
//newTracked = (dep)=>(dep.n & trackOpBit)>0
if (!newTracked(effectsSet)) {
effectsSet.n |= trackOpBit;
//wasTracked = (dep)=>(dep.w & trackOpBit)>0
shouldTrack2 = !wasTracked(dep);
}
} else {
shouldTrack2 = !dep.has(activeEffect);
}
// ...
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。