🍒 自动收集依赖以及触发effect

70 阅读6分钟

原文地址: alvis.org.cn/posts/60b86…

从 0 到 1 手撕 Reactive 响应式教程导航 🚥🚥🚥

  1. 🥬 建立一个响应式系统
  2. 🍒 自动收集依赖以及触发 effect ⇦ 当前位置 🪂
  3. 🎋 Ref响应式 effect

在上一章结尾,我们简单讨论了,如何自动的触发tracktrigger。所以现在的问题就变成了,我们应该如何很好的拦截对象,做一些操作。在 vue3 中,采用了 ES6 的Proxy代理拦截。我们简单的介绍一下ProxyReflect的工作方式。

let product = { price: 5, quantity: 2 };

const proxyProduct = new Proxy(product, {
  // get 拦截
  get(target, key, receiver) {
    console.log("当前对象的属性" + key);
    return Reflect.get(target, key, receiver);
  },
  // set 拦截
  set(target, key, value, receiver) {
    console.log("当前要给对象的" + key + "属性赋值为" + value);
    return Reflect.set(target, key, value, receiver);
  },
});

// 我们可以在控制台调试
proxyProduct.quantity = 4; // 我们会发现proxy代理中的set拦截器函数执行了
console.log(proxyProduct.quantity); // 同样的发现proxy代理中的get拦截器函数执行了

1. Reactive 函数

简单的开篇之后,我们直接进入今天的正题。

在认识了代理和反射之后,我们直接创建一个reactive函数,该函数返回的是代理对象。然后我们在代理对象的getset拦截器函数中做一些我们想做的事情。废话不多说,直接敲代码。

function reactive(target) {
  let hanlder = {
    get(target, key, receiver) {
      let res = Reflect.get(target, key, receiver); // track我们将收集依赖映射(当前对象weakmap -> 属性map -> effect(set集合))
      track(target, key);
      return res;
    },
    set(target, key, value, receiver) {
      // 获取到之前的数值
      let oldVal = target[key];
      let result = Reflect.set(target, key, value, receiver);
      if (result && oldVal !== value) {
        // 更新
        trigger(target, key);
      }
      return result;
    },
  };
  return new Proxy(target, hanlder);
}

哇哦,真是越看这段代码越舒服,这段代码帮助我们解决了很大的问题。现在让我们在回到之前的代码,对之前的代码进行优化。

let product = reactive({ price: 5, quantity: 2 });
let total = 0;
let effect = () => {
  total = product.price * product.quantity;
};
let targetMap = new WeakMap();

/**
 * 当执行函数的时候
 * product.price 触发了 reactive中的get函数,在get中触发了track。然后形成了product(weakmap) -> price(map) -> effect(set)
 * product.quantity 触发了 reactive中的get函数,在get中触发了track。然后形成了product(weakmap) -> quantity(map) -> effect(set)
 */
effect();

console.log(total); // 10
/**
 * 改变数量
 * 触发了set函数,set函数中触发了trigger函数
 */
product.quantity = 10;

console.log(total); // 50
// 改变价格
product.price = 10;
console.log(total); // 100
// 收集依赖
function track(target, key) {
  //...
}

// 更新
function trigger(target, key) {
  // ...
}

function reactive(target) {
  let hanlder = {
    get(target, key, receiver) {
      let res = Reflect.get(target, key, receiver); // track我们将收集依赖映射(当前对象weakmap -> 属性map -> effect(set集合))
      track(target, key);
      return res;
    },
    set(target, key, value, receiver) {
      // 获取到之前的数值
      let oldVal = target[key];
      let result = Reflect.set(target, key, value, receiver);
      if (result && oldVal !== value) {
        // 更新
        trigger(target, key);
      }
      return result;
    },
  };
  return new Proxy(target, hanlder);
}

我们终于解放了双手,从此不在自己调用 track 和 trigger。在第一次执行 effect 函数的时候,product.priceproduct.quantity分别触发了 Proxy 的 get 拦截器,此时会进入到我们之前写好的 track 函数中,收集依赖。然后如果我们对product.price或者product.quantity进行赋值的时候,会触发到 Proxy 代理中的 set 拦截器,执行 trigger 方法。将该对象下,该属性对应的 effect 集合执行一次。这样就简单的实现了一个基本的响应式系统。

2. activeEffect

为了让我们的reactive函数更接近于 vue 的源码,我们不妨在继续思考一个问题。在 vue 中我们可能是通过 render 函数去改变响应式数据,或者是通过 watch 函数改变响应式数据,在或者是其他一些函数改动响应式数据,那么我们是如何知道的是谁在使用这个属性呢???或者说当某个函数在执行过程中,用到了响应式数据,响应式数据是如何知道那个函数在用自己的呢???

看过前几篇文章的小伙伴不知道有没有一种错觉。哎,我们不是有track收集依赖的函数吗?他会自动帮我们添加effect副作用函数啊,这个副作用函数不就是我们用到了响应式数据的函数吗?确实是这样的,但是请小伙伴在仔细看一下代码,我们当时只是做了映射关系,具体的添加依赖函数我们并没有去做,只是简单的添加了一个 effect 依赖函数。如果我们还有一个 effect 函数怎么办呢?

其实 vue 用了一个很巧妙的办法,你不要直接执行函数,而是把函数交给一个watcherEffect执行,他会设置一个全局的变量,让全局变量记录当前执行的函数。

// 设置当前激活的响应effect函数
let activeEffect = null;
function watcherEffect(eff) {
  activeEffect = eff;
  activeEffect();
  activeEffect = null;
}

// 在完善一下我们的track收集依赖函数
function track(target, key) {
  // 只有当前是激活的effect才会收集依赖,也就是effect函数中执行的才会收集依赖
  if (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);
  }
}

恭喜你,我们的reactive响应式终于 🆗 了。我们来简单回顾一下,老样子,先贴代码:

let product = reactive({ price: 5, quantity: 2 });
let total = 0;
let salePrice = 0;
let targetMap = new WeakMap();
// 设置当前激活的响应effect函数
let activeEffect = null;

watcherEffect(() => {
  total = product.price * product.quantity;
});

watcherEffect(() => {
  salePrice = product.price * 0.9;
});

console.log(
  `Befor updated total(should be 10) = ${total} salePrice(should be 4.5) = ${salePrice}`
);
product.quantity = 3;
console.log(
  `After updated total(should be 15) = ${total} salePrice(should be 4.5) = ${salePrice}`
);
product.price = 10;
console.log(
  `After updated total(should be 30) = ${total} salePrice(should be 9) = ${salePrice}`
);

/**
以下是响应式核心代码
*/

function watcherEffect(eff) {
  activeEffect = eff;
  activeEffect();
  activeEffect = null;
}

// 收集依赖
function track(target, key) {
  // 只有当前是激活的effect才会收集依赖,也就是effect函数中执行的才会收集依赖
  if (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);
  }
}

// 更新
function trigger(target, key) {
  // 通过当前对象获取到对应的map(当前对象的属性和vlaue)
  let depsMap = targetMap.get(target);
  if (!depsMap) return; // 通过对象的属性,获取到当前属性依赖的effect函数(set集合)
  let dep = depsMap.get(key);
  if (dep) {
    dep.forEach((effect) => effect());
  }
}

function reactive(target) {
  let hanlder = {
    get(target, key, receiver) {
      let res = Reflect.get(target, key, receiver); // track我们将收集依赖映射(当前对象weakmap -> 属性map -> effect(set集合))
      track(target, key);
      return res;
    },
    set(target, key, value, receiver) {
      // 获取到之前的数值
      let oldVal = target[key];
      let result = Reflect.set(target, key, value, receiver);
      if (result && oldVal !== value) {
        // 更新
        trigger(target, key);
      }
      return result;
    },
  };
  return new Proxy(target, hanlder);
}

3. 总结

通过 Proxy 代理中的 get 和 set 拦截对象属性,当在 get 的时候说明在读取属性,需要我们进行对象-属性-依赖函数之间的关系映射,并且要将当前执行的函数添加到该属性对应的 set 集合中。要想知道当前执行的函数是谁,我们通过将要执行的函数交给一个watcherEffect函数,一个巧妙的全局变量,存储当前正在执行的函数,然后将该函数添加到 set 集合中。然后在给响应式对象赋值的时候,会进入到 proxy 的 set 拦截函数,这个时候就会替我们执行trigger函数,执行我们在track时候收集到该属性对应的依赖函数。实现了一个简单的响应式。