Vue3 响应式原理探索Part 3 - Proxy + Reflect + activeEffect

608 阅读3分钟

“这是我参与更文挑战的第8天,活动详情查看: 更文挑战

前文摘要

通过之前的文章,我们简单实现了一个响应式,并学习了 ProxyReflect

之前的实现存在需要手动 tracktrigger 的问题,本文将利用 Proxy + Reflect 实现自动的响应式。

组合 Proxy + Effect 存储

基于之前的学习,我们很容易想到,我们可以:

  • Proxyget handler 内去做响应式的 track
  • Proxyset handler 内去做响应式的 trigger

示例代码如下:

function reactive(target) {
  const handler = {
    get(target, key, receiver) {
      let result = Reflect.get(target, key, receiver)
        // Track
      return result
    },
    set(target, key, value, receiver) {
      let oldValue = target[key]
      let result = Reflect.set(target, key, value, receiver)
      if (result && oldValue != value) { // Only if the value changes 
        // Trigger
      } 
      return result
    }
  }
  return new Proxy(target, handler)
}

结合之前的代码,我们的代码具体如下:

const targetMap = new WeakMap();
function track(target, key) {
    let depsMap = targetMap.get(target);
    if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()));
    }
    let dep = depsMap.get(key);
    if (!dep) {
        depsMap.set(key, (dep = new Set()));
    }
    dep.add(effect);
}
function trigger(target, key) {
    const depsMap = targetMap.get(target);
    if (!depsMap) return;
    let dep = depsMap.get(key);
    if (dep) {
        dep.forEach(element => {
            element();
        });
    }
}

function reactive(target) {
    const handler = {
        get(target, key, receiver) {
            let result = Reflect.get(target, key, receiver);
            track(target, key);
            return result;
        },
        set(target, key, value, receiver) {
            let oldValue = target[key];
            let result = Reflect.set(target, key, value, receiver);
            if (result && oldValue != value) {
                trigger(target, key);
            }
            return result;
        }
    }
    return new Proxy(target, handler);
}

let param = reactive({ width: 5, height: 2 });
let size = 0;
let effect = () => {
    size = param.width * param.height;
}

effect();
console.log(size); // => 10;

param.height = 3;
console.log(size); // => 15

param.width = 6;
console.log(size); // =>  18

想必你已经观察到现在已经不需要手动的去调用 triggertrack, 因为他们已经恰当的自动在 reactive 函数内部被调用了。

新的问题以及 activeEffect

新的问题

但我们现在碰到两个问题。假设 param 有三个属性,widthheightradius, 具体如下:

...
let param = reactive({ width: 5, height: 2 });
let size = 0;
let effect = () => {
    size = param.width * param.height;
}

console.log(param.radius); // 调用了 radius key 的 track 
param.radius = 15; // 调用了 width、height key 的 track, 并调用了 radius key 的 trigger

console.log(param.width); // 调用了 width key 的 track 

可以看到问题:

  • 并不需要响应 radius 键,因为 size 并不依赖它。但我们在 radius 调用或修改时,都无意中触发了 tracktrigger

  • effect 外部获取 param.width 时,也调用了 width 键的 track,实际上不需要的。

activeEffect

我们可以考虑用 activeEffect 解决这个问题。

const targetMap = new WeakMap();
let activeEffect = null // The active effect running

function track(target, key) {
    if (!activeEffect) return; // 如果没有 activeEffect 就返回
    let depsMap = targetMap.get(target);
    if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()));
    }
    let dep = depsMap.get(key);
    if (!dep) {
        depsMap.set(key, (dep = new Set()));
    }
    dep.add(activeEffect); // 依赖添加 activeEffect 
}
...
// trigger、 reactive 两个函数实现保持不变

function effect(eff) {
    activeEffect = eff  // 将 eff 函数设为 activeEffect
    activeEffect()      // 执行 activeEffect,因为它内部会获取各个 target 的 属性,所以会收集依赖,执行相应 key 的 track。 
    activeEffect = null // Unset it
}

let param = reactive({ width: 5, height: 2, radius: 10 });
let size = 0;
let newWidth = 0;

effect(() => {
    size = param.width * param.height;
});

effect(() => {
    newWidth = param.width * 3;
});


console.log(`size is ${size}, newWidth is ${newWidth}`); // size is 10, newWidth is 15

param.width = 10;

console.log(`size is ${size}, newWidth is ${newWidth}`); // size is 20, newWidth is 30

console.log(param.width, param.radius); // 不会触发 width \ radius 的 track

param.radius = 10; // 不会触发 trigger ,因为没有被依赖到

真正关键的是

function track(target, key) {
    if (!activeEffect) return; // 如果没有 activeEffect 就返回
    ...
    dep.add(activeEffect); // 依赖添加 activeEffect 
}

function effect(eff) {
    activeEffect = eff
    activeEffect()
    activeEffect = null
}

effect(() => {
    size = param.width * param.height;
});

实现非常让人惊叹。可以看到具体原理如下:

  1. effect 将形参 eff 赋值给 activeEffect,并执行,eff 因为依赖了响应式的width height, 会执行其相应的 get handler
  2. Proxy 内部 get handler 会执行 track, 因为 activeEffect 还未被重置,所以 activeEffect 被添加为widthheight 的依赖
  3. activeEffect执行后,被重置为 null,所以和 eff 不相关的属性都不会被收集成依赖,进而不会触发多余的 tracktrigger

小结

本文重点实现了2个技术问题:

  • 利用 Proxy + Reflect 实现了自动的响应式
  • 利用 activeEffect 解决触发无效 track 及 trigger

本文参考: