vue3的effect理解

为了理解 vue3的响应式核心 effect,跟着源码调试,并将自己的理解记下。

(可能有的观点不正确。留待后面再review)

vue3 版本是 3.2.47。

响应式 分为 依赖收集 和 触发依赖。

只要访问 响应式对象的数据,就会触发 依赖收集。

一、依赖收集

vscode 中 直接 vite 启动项目。测试代码如下:

let myName = ref('ShowMe');
effect(() => {
  debugger
  console.log("myName:",myName.value)
})

function handleChangeName(){
  myName.value +='123'
}

执行代码

1. 第一行 ref('showMe'),创建一个响应式对象

ref函数会调用 createRef  --->  RefImpl类。在RefImpl 类中 实例化一个ref。

2. 第二行 执行effect函数。effect函数如下。可以看到

        2.1 每次执行effect会生成一个 reactiveEffect 包装类实例

        2.2 传入的第一个参数 fn,在没有第二个参数或第二个参数不存在 lazy: true 属性时,立即运行一次 run 函数。(我的理解 是为了立即得到 effect 函数 的 返回值 ???)

reactiveEffect 类 如下:

其中 activeEffect 是一个全局的变量(指针)。指向当前要收集的effect。

class ReactiveEffect {
    constructor(fn, scheduler = null, scope) {
        this.fn = fn;
        this.scheduler = scheduler;
        this.active = true;
        this.deps = [];
        this.parent = undefined;
        recordEffectScope(this, scope);
    }
    run() {
        // 如果 为false, 跳过 依赖收集, 直接调用传入的函数 (运行一次 effect 就行)
        // 什么时候为 false,网上说 只有 调用了 stop() 方法。停止依赖收集。
        if (!this.active) {
            return this.fn();
        }
        // 保存正在 执行的effect 和 shouldTrack 
        let parent = activeEffect;
        let lastShouldTrack = shouldTrack;
        // parent 
        while (parent) {
            // 如果 当前的effect 就是现在的 effect,
            if (parent === this) {
                return;
            }
            // 向上一直找到 effect 链上的 第一个effect的parent,就是undefined
            parent = parent.parent;
        }
        try {
            this.parent = activeEffect;
            activeEffect = this;
            shouldTrack = true;
            trackOpBit = 1 << ++effectTrackDepth;
            // 对effect 上的  订阅了该依赖的 对象属性 进行 标记。
            if (effectTrackDepth <= maxMarkerBits) {
                initDepMarkers(this);
            }
            // 降级 清空所有的依赖
            else {
                cleanupEffect(this);
            }
            // fn() 执行会重新收集依赖
            return this.fn();
        }
        finally {
            //...  先省略,后面会执行
        }
    }
    stop() {
        // ... 省略 
    }
}

try执行,可以看到最后会 return this.fn(), 此时会正式执行 我们传入的effect第一个参数fn(函数)

执行前 effect 实例 如下

执行 fn()  会访问myName.value, 自然会触发 这个 refImpl 实例 的getter 函数。

目前 ref 实例 如下:

会执行 trackEffect:

function trackRefValue(ref) {
    if (shouldTrack && activeEffect) {
        ref = toRaw(ref);
        {
            trackEffects(ref.dep || (ref.dep = createDep()), {
                target: ref,
                type: "get" /* TrackOpTypes.GET */,
                key: 'value'
            });
        }
    }
}

由于目前 ref.dep 为空,会执行 createDep 函数。会为当前的dep 创建一个 set 集合。用来收集有关的effects(其中的 w 和 n是为了嵌套执行时存储当前嵌套层级和effect的关系,留到后面再说)

const createDep = (effects) => {
    const dep = new Set(effects);
    // 是否已被收集  wasTracked
    dep.w = 0;
    // 是否新收集  newTracked
    dep.n = 0;
    return dep;
};

此时的ref实例如下:

进入 trackEffect 函数,由于此时 ref.dep是为空,所以会执行 dep.n |= trackOpBits; 

shouldTrack 为 true,会执行 dep.add(activeEffect);activeEffect.deps.push(dep); 

此时的activeEffect指向的是effect 实例。会将此effect 加入 ref.dep set集合中。同时ref.dep也会加入effect 实例中的deps数组中 

function trackEffects(dep, debuggerEventExtraInfo) {
    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);
        if (activeEffect.onTrack) {
            activeEffect.onTrack({
                effect: activeEffect,
                ...debuggerEventExtraInfo
            });
        }
    }
}

// wasTracked
const wasTracked = (dep) => (dep.w & trackOpBit) > 0;
// newTracked
const newTracked = (dep) => (dep.n & trackOpBit) > 0;

目前 ref 实例 长这样:可以看到 effect 和 dep 已经各自添加依赖。

此时 fn() 函数执行完毕,进入 finally 内容。

会将 dep 的 w,n 重置。activeEffect 重新指向为 undefined。

class ReactiveEffect {
    constructor(fn, scheduler = null, scope) {
        // ... 省略
    }
    run() {
        // ... 省略
        try {
            // ... 省略
        }
        finally {
            if (effectTrackDepth <= maxMarkerBits) {
                finalizeDepMarkers(this);
            }
            trackOpBit = 1 << --effectTrackDepth;
            activeEffect = this.parent;
            shouldTrack = lastShouldTrack;
            this.parent = undefined;
            if (this.deferStop) {
                this.stop();
            }
        }
    }
    stop() {
        // ... 省略
    }
}

//  finalizeDepMarkers
const finalizeDepMarkers = (effect) => {
    const { deps } = effect;
    if (deps.length) {
        let ptr = 0;
        for (let i = 0; i < deps.length; i++) {
            const dep = deps[i];
            if (wasTracked(dep) && !newTracked(dep)) {
                dep.delete(effect);
            }
            else {
                deps[ptr++] = dep;
            }
            // clear bits
            dep.w &= ~trackOpBit;
            dep.n &= ~trackOpBit;
        }
        deps.length = ptr;
    }
};

依赖收集结束。其实后面还有其他依赖的收集。因为我们的 remplate 模板也有对 ref 的引用,template 会最终转为 render 函数。在render函数中进行ref的依赖收集(此处不展开,后续另开一篇),可以看到此时的fn 为具体的渲染函数。


二、触发依赖

当点击 按钮时会触发 ref 的setter 函数。

会执行 triggerEffects。对 ref.dep 中的effect集合依次执行(看到 computed 的effect 会单独处理)

每个effect  继续执行 triggerEffect。如果有 scheduler,即 effect() 函数传第二个参数(函数),会执行此函数(computed 等),否则执行 run()

function triggerEffects(dep, debuggerEventExtraInfo) {
    // spread into array for stabilization
    const effects = shared.isArray(dep) ? dep : [...dep];
    for (const effect of effects) {
        if (effect.computed) {
            triggerEffect(effect, debuggerEventExtraInfo);
        }
    }
    for (const effect of effects) {
        if (!effect.computed) {
            triggerEffect(effect, debuggerEventExtraInfo);
        }
    }
}

function triggerEffect(effect, debuggerEventExtraInfo) {
    if (effect !== activeEffect || effect.allowRecurse) {
        if (effect.onTrigger) {
            effect.onTrigger(shared.extend({ effect }, debuggerEventExtraInfo));
        }
        if (effect.scheduler) {
            effect.scheduler();
        }
        else {
            effect.run();
        }
    }
}

run函数会执行一遍  fn()。

注意到 此时 ref.dep 上有 两个 effect。 一个为 我们写的 console.log,一个为 render 函数。依次执行后,打印数据,页面数据刷新。

三、effect的嵌套

如果 effect是没有嵌套的简单执行,流程大概如上所示。但很多情况下 effect 是嵌套执行的,这时的dep是长啥样的?

vue3 中新增了effectTrackDepth,trackOpBit。effectTrackDepth最大为 30,即最大嵌套执行 30个(为什么是30,js引擎(V8 等)将数字分为Smi、HeapNumber和BigInt。其中Smi小整数是存在栈上,存取最快。Smi有效位为31位。trackOpBit 存为一个Smi,考虑 trackOpBit 初始为1。最大可以左移 30 位??? 此处理解不知对不对)

为了理解 dep中新增加的 w,n 和effectTrackDepth,trackOpBit标志位,effect的嵌套。简单的测试一下。

测试代码如下: 

// 嵌套依赖
const a = ref(0)
debugger
effect(() => {
  let b = a.value + 1
  effect(() => {
    debugger
    let c = a.value + 2
  })
})

第一个effect执行 run方法,目前的effect实例如下:

进入initDepMarkers,此时的deps为空,即没有 属性 对这个 effect 进行收集, 

effectTrackDepth为1,trackOpBit 为 2。目前是第一层effect。 

之后执行fn()。

fn() 中 访问了a.value, 触发 get 收集依赖

注意到此时的activeEffect 指向的是 第一个 effect 对应的 ReactiveEffect 实例。

进入 trackEffects方法

由于第一层嵌套 a.value 没有收集过本effect ,所以newTracked(dep) 为 false,

进入函数内,wasTracked(dep) 为 false, shouldTrack 为 true,需要在第一层级收集本次的effect。

dep中添加 本次的 effect,同时effect 的deps 中 添加 依赖本effect的 dep。

const newTracked = (dep) => (dep.n & trackOpBit) > 0;
const wasTracked = (dep) => (dep.w & trackOpBit) > 0;

执行完 getter后,目前 a的refImpl 如下:


 继续执行,发现有嵌套的effect, 创建 ReactiveEffect 的实例,执行run方法。

此时的activeEffect 还为 第一个effect 的实例。他的parent 为 undefined。

目前的 effectTrackDepth 为 1,trackOpBit 为 2。

继续执行 try,目前嵌套为第二层,所以 ++effectTrackDepth为 2,trackOpBit 为 4。

目前的第二个 effect 对应的实例如下:目前deps 为空,因为 a.value 还没有收集这个 effect。 其中parent保存的是 第一个effect的实例,方便 执行完 退到 上一级。

继续执行,第二个 effect 也访问了 a.value, 触发 trackRefValue

此时:trackOpBit为4,dep.n为2。 newTracked(dep) 为 false 。

进入函数内,dep.n 为 6,dep.w 为 0,wasTracked(dep) 为 false。shouldTrack 为 true。需要收集依赖


 执行完 依赖收集 后,会进入 finally 阶段。这一步会将对应层级的dep.w 和 dep.n 重置。

首先进入的是 第二个 effect 实例的 finally。

effect中的 deps 只有一个 ,就是 a.value ,  可以看到 a.value 对应的 w 为0,n 为 6。wasTracked(dep) 为false, 所以wasTracked(dep) && !newTracked(dep) 为 false(如果之前收集过本effect,现在没有收集,会在属性的 dep effect列表中删除本effect, 防止属性修改时触发不必要的effect)。同时会执行 与反 操作,将 dep.w 和 dep.n 对应的本次深度的二进制位(4)清空。目前 trackOpBit 为 4,dep.n 为 6,dep.w 为 0。 所以 执行完后 dep.n 为 2,dep.w 为 0。 之后会还原到上一级, effectTrackDepth 为1,trackOpBit为 2。将 activeEffect 指向之前保存的上一级effect实例。

之后会走第一个 effect 实例 对应的 finally函数。

目前 trackOpBit为 2,dep.w 为 0,dep.n 为 2,wasTracked(dep) 为false,所以wasTracked(dep) && !newTracked(dep) 为 false。执行完后 dep.n 为 0,dep.w 为 0。

将 effectTrackDepth 置为 0。trackOpBit 置为 1。(此时 已经没有嵌套的effect了,effectTrackDepth,trackOpBit 都重置为了初始值,方便下一轮的依赖收集)

至此 依赖收集完成。后续a.value 值改变时 会触发两个effect函数。


总结一下:

嵌套执行的每层 effect 对应 effectTrackDepth 的大小,对应 trackOpBit的一位,同时也对应 ref.dep 中的 w,n 的二进制位。以前版本中没有 w、n 这些标志位,每次运行 fn() 之前会运行 cleanupEffect ,直接清空 effect 上的 deps 订阅集合。每次fn() 收集依赖时会重新创建一个set集合,一个个收集。这里可能是性能优化的一个点。

四、parent 的作用

是为了解决嵌套依赖的问题。

在vue2中,是用了targetStack来保存嵌套执行的 watcher。 Dep.target 就类似于activeEffect。

在vue3中,用parent来维护嵌套关系。

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack: Array<DepTarget | null | undefined> = []

export function pushTarget(target?: DepTarget | null) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget() {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

五、总结 

总结一下,在代码中对属性的访问会触发依赖的收集,收集的其实是一个函数fn。

在响应式对象的 dep 属性中保存着 fn 和所依赖的代理对象属性之间的关系。同时 fn 所在的 reactiveEffect 中 deps 属性 保存着依赖于自身的所有代理对象属性 。

在 effect 创建时会执行一次run函数(不考虑 lazy:true的情况),此时会执行fn(),收集依赖。当修改了响应式数据后,会触发 triggerEffect,会执行一次run函数(不考虑 scheduler),会执行fn(),并重新收集依赖(此时fn中的依赖可能发生改变,比如条件判断某些属性不再展示等)。其中w,n,trackOpBit 等属性表明了各个嵌套层级的 effect 是否收集,提高了更新依赖时的性能。

至此,简单理解了effect 的功能。理解可能有不到位的地方,我也边学边改正。

后面会另开一章,在此基础上深入computed、watch、watchEffect等api。

  • 8
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值