乐趣区

关于vue.js:Vue3源码响应式系统依赖收集和派发更新流程浅析

本文基于 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

依赖收集

流程图

根本流程

应用 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)
    }
}

派发更新

流程图

根本流程

由下面依赖收集的流程能够晓得,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 流程图总结

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

派发更新流程图

派发更新流程图源码剖析

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;
    }
}

依赖收集

派发更新

其它细节剖析

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 流程浅析
退出移动版