Vue3源码-响应式系统-依赖收集和派发更新流程浅析

9,454 阅读32分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

本文基于Vue 3.2.30版本源码进行分析

为了增加可读性,会对源码进行删减、调整顺序、改变的操作,文中所有源码均可视作为伪代码

由于ts版本代码携带参数过多,大部分伪代码会采取js的形式展示,而不是原来的ts代码

本文重点关注依赖收集和派发更新的流程,为了简化流程,会以Object为基础分析响应式的流程,不会拘泥于数据类型不同导致的依赖收集和派发更新逻辑不同

本文内容

  • ProxyReflect的介绍
  • 整体依赖收集的流程图以及针对流程图的相关源码分析
  • 整体派发更新的流程图以及针对流程图的相关源码分析
  • ReactiveEffect核心类的相关源码分析和流程图展示
  • computed的相关分析,分为流程图和针对流程图的相关源码分析
  • watchwatchEffect的初始化源码分析,包括各种配置参数以及schedule
  • watchwatchEffect依赖收集和派发更新流程图展示

每一个点都有配套的流程图,源码分析是为流程图服务,结合流程图看源码分析效果更佳

前置知识

Vue2源码-响应式原理浅析的文章分析中,我们知道Vue2使用的是Object.defineProperty的方式,而在Vue3中使用Proxy进行代替数据的响应式劫持,下面将简单介绍Vue3中所使用的ProxyReflect

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方法的效果

ReflectVue3中用来代替对象的一些方法操作,如下面代码所示,直接访问obj.xxxReflect.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,则抛出TypeError
Reflect.get()Reflect.get()返回属性的值。如果目标不是Object,则抛出TypeError
Reflect.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

依赖收集

流程图

由于掘金限制,只能使用png图片,由于本文源码分析都是基于流程图,如果下面的png图片看不清,可以点击查看清晰的svg图片Vue3依赖收集.svg

Vue3依赖收集.png

基本流程

使用Proxy进行target对象的代理初始化,由上面前置知识可知,proxy会劫持target的多个方法,下面代码中使用mutableHandlersProxyhandler方法

// 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.ndep.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)
    }
}

派发更新

流程图

由于掘金限制,只能使用png图片,由于本文源码分析都是基于流程图,如果下面的png图片看不清,可以点击下载查看清晰的svg图片Vue3派发更新.svg

Vue3派发更新.png

基本流程

由上面依赖收集的流程可以知道,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,可以认为所持有的响应式对象

初始化

将目前需要执行的fnscheduler传入

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=xxxset操作,那么由于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,如下面精简源码所示,Vue3ReactiveEffect.run()执行this.fn()前,会将上次的activeEffectshouldTrack状态保存,执行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()的执行,实际上就是重新执行一遍带有响应式变量的方法,这个时候会触发响应式变量Proxyget()方法,从而触发上面分析的依赖收集,然后触发核心的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就是子effectparent,比如上面示例代码块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.wdep.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函数的深度,使用二进制的形式,比如10100100010000
  • effectTrackDepth:表示递归嵌套执行effect函数的深度,使用十进制的形式,比如1234
  • 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,这里的depseffect所持有的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流程图总结

由于掘金限制,只能使用png图片,由于本文源码分析都是基于流程图,如果下面的png图片看不清,可以点击下载查看清晰的svg图片Vue3-ReactiveEffect.svg

Vue3-ReactiveEffect.png

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>

依赖收集流程图总结

由于掘金限制,只能使用png图片,由于本文源码分析都是基于流程图,如果下面的png图片看不清,可以点击查看清晰的svg图片Vue3-computed依赖收集.svg

Vue3-computed依赖收集.png

依赖收集流程图源码分析

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就是界面渲染effectComputedRefImpl.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();
        }
    }
}

派发更新流程图

由于掘金限制,只能使用png图片,由于本文源码分析都是基于流程图,如果下面的png图片看不清,可以点击查看清晰的svg图片Vue3-computed派发更新.svg

Vue3-computed派发更新.png

派发更新流程图源码分析

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数据,将传入的数据、refreactive进行整理形成规范的数据(下文会详细分析)
  • cleanup:注册清除方法,在watchEffectsource触发执行/watch监听值改变时,会暴露出一个onCleanup()方法,我们可以通过onCleanup传入一个fn()方法,在适当的时机会调用清除目前的监听方法(下文会详细分析)
  • 初始化job(): 根据cb构建回调,主要是根据oldValuenewValue进行回调(下文会详细分析)
  • 根据flush('pre'、'sync'、'post')构建scheduler(下文会详细分析)
  • 初始化new ReactiveEffect(getter, scheduler)
  • 初始化运行:根据传入的参数
    • 如果是watch类型,并且是immediate立刻执行job(),立刻触发cb(newValue, undefined)回调,然后更新oldValue=newValue
    • 如果是watch类型,immediate=false,则计算结果缓存oldValue
    • 如果不是watch类型(还有watchEffectwatchPostEffectwatchSyncEffect),并且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都进行依赖收集,目前的activeEffectwatch类型的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
  • watchEffectgetter()就是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]);
        };
    }
}

如果是数组数据,即有多个响应式对象时,需要判断是否是refreactivefunction类型,然后根据上面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;
    }
}

依赖收集

由于掘金限制,只能使用png图片,由于本文源码分析都是基于流程图,如果下面的png图片看不清,可以点击查看清晰的svg图片Vue3-watch_watchEffect依赖收集.svg

Vue3-watch_watchEffect依赖收集.png

派发更新

由于掘金限制,只能使用png图片,由于本文源码分析都是基于流程图,如果下面的png图片看不清,可以点击查看清晰的svg图片Vue3-watch_watchEffect-派发更新.svg

Vue3-watch_watchEffect-派发更新.png

其它细节分析

createSetter中target === toRaw(receiver)

如下面代码所示,Proxyset()方法中存在着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.countget()方法,由于proxy没有count这个属性,因此会去访问它的prototype,即baseProxy
  • 由于proxybaseProxy都是响应式对象,因此会触发proxybaseProxy两个对象的dep收集effect
  • proxy.count改变的时候,触发Reflect.set()方法,因此会触发proxy收集的effct重新执行一次
  • 但是由于proxyprototype才有count,因此proxy.count改变的时候,会触发baseProxy.countset()方法执行,从而触发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=objreceiver=proxy
  • 因为target没有这个count属性,因此触发target的原型获取count属性,也就是
    • proxy.count->Reflect.set(target, key, value, receiver),此时target=objreceiver=proxy
    • ======>触发了
    • Reflect.set(target, key, value, receiver),此时target=objProtoreceiver=proxytoRaw(receiver)=obj,此时因为target不等于toRaw(receiver)而阻止了trigger派发更新逻辑
    • 新的值已经成功设置,然后返回return result
    • ======>
    • 继续刚开始的proxy.count->Reflect.set(target, key, value, receiver),此时target=objreceiver=proxytoRaw(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时,内外effecttrackOpBit值是不同,因此内层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);
    }
    // ...
}

参考文章

  1. Proxy是代理,Reflect是干嘛用的?

Vue系列文章

  1. Vue2源码-响应式原理浅析
  2. Vue2源码-整体渲染流程浅析
  3. Vue2源码-双端比较diff算法 patchVNode流程浅析
  4. Vue3源码-响应式系统-依赖收集和派发更新流程浅析
  5. Vue3源码-响应式系统-Object、Array数据响应式总结
  6. Vue3源码-响应式系统-Set、Map数据响应式总结
  7. Vue3源码-响应式系统-ref、shallow、readonly相关浅析
  8. Vue3源码-整体渲染流程浅析
  9. Vue3源码-diff算法-patchKeyChildren流程浅析