likes
comments
collection
share

【js篇 - Proxy】- 4、使用Proxy实现简单的watchEffect和watch函数

作者站长头像
站长
· 阅读数 18

本文属于个人见解,不保证准确性。

背景

下面代码,我们期望count值改变的时候,会触发watch回调函数的执行。比如执行count = 1;打印 1 0。

let count = 0;
watch(count, (newVal, oldVal) => {
  console.log(newVal, oldVal);
});
count = 1;  // 1 0

上面代码想实现想要的效果很难,因为,想要监听某个变量改变的时候做些事情,也就意味着要对变量进行拦截,在js中,基本类型的值是不变的,无法直接对其设置拦截器进行拦截处理,但对象的值是可变的。所以,想要对count进行监听,就要将count转换成对象,从而对其进行拦截处理。

可变与不可变?

这里的可变和不可变可以这样理解:

  • 如果变量的值需要对变量重新赋值才能改变,则认为值是不可变的;
  • 如果变量的值不需要对变量重新赋值也能改变,则认为是可变的。

比如:

let a = 1;  // a的值1是不可变的,只有对a重新赋值,a的值才改变
let obj = { a: 1}  // obj的值是可变的,obj.a = 2,没有对obj重新赋值,但是obj的值已经变成了 {a:2}

可以模仿vue3,将上面代码改成如下:

const count = ref(0);
watch(
  () => count,
  (newVal, oldVal) => {
    console.log(newVal, oldVal);
  }
);
count.value = 1;

分析:watch如何能监听count变化?

要想watch监听count变化时执行某些函数,需要对count进行拦截,然后在访问count的时候,将需要执行的这些函数收集起来,当count改变的时候,依次执行收集的函数

所以refwatch的作用就明确了:

ref:用于将count转换成可以对其进行拦截处理的代理对象。

watch: 用于收集count的依赖函数,当count改变的时候,执行收集的依赖函数。

上面count的依赖函数就是watch的两个参数函数,当count值改变的时候,执行watch的两个参数函数,从而达到watch监听count变化的效果。下面就实现ref和watch。

ref

利用Proxy返回一个代理对象,代码如下:

const ref = (value) => {
  // return reactive({ value })
  const obj = reactive({
    value
  })

  /**
   * 不直接返回reactive({value})的原因?
   * 1、保留 ref 函数返回的对象中包装的 value 属性
   * 2、方便后续需在 ref 对象上添加其他属性或方法
   */
  return {
    get value() {
      return obj.value
    },
    set value(newValue) {
      obj.value = newValue
    }
  }
}

上面reactive函数用于返回一个代理对象。

reactive

访问代理对象某个属性的时候,将该属性的依赖函数收集起来;该属性值改变时,执行其所有的依赖函数。代码实现如下:

const targetMap = new WeakMap();  // 保存所有对象的依赖

/** 
 * 依赖收集 
 */
const addDep = (target, key) => {
  if (!activeEffect) {
    return;
  }
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }
  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Set();
    depsMap.set(key, dep);
  }
  dep.add(activeEffect);
}

/** 
 * 依赖执行
 */
const trigger = (target, key) => {
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    return;
  }
  const dep = depsMap.get(key);
  if (dep) {
    dep.forEach((effect) => {
      effect();
    });
  }
}

const reactive = (obj) => {
  const observed = new Proxy(obj, {
    get(target, key) {
      const res = Reflect.get(target, key);
      addDep(target, key);
      return typeof res === 'object' ? reactive(res) : res;
    },
    set(target, key, val) {
      const res = Reflect.set(target, key, val);
      trigger(target, key);
      return res;
    },
  });
  return observed;
}

如何触发访问,从而收集依赖呢,交给watch处理。

watch

将依赖函数准备好,交给watchEffect处理

const isRef = (val) => {
  return val && val.value !== undefined
}

const watch = (getter, callback) => {
  const initialVal = getter();
  let oldValue = isRef(initialVal) ? initialVal.value : initialVal;
  let isFirstCall = true; //标记

  const effectCallback = () => {
    // 访问代理对象监听的属性,触发依赖收集
    const newValue = isRef(initialVal) ? getter().value : getter();
  
    if (!isFirstCall) {
      if (newValue !== oldValue) {
        callback(newValue, oldValue);
        oldValue = newValue;
      }
    } else {
      isFirstCall = false;
    }
  };

  watchEffect(effectCallback);
}

watchEffect

记录依赖函数,触发收集依赖函数

let activeEffect = null;

const watchEffect = (fn) => {
  activeEffect = fn;  // 先记录依赖函数
  fn();  // 执行一次依赖函数,会访问代理对象的监听属性,从而触发收集当前属性变化的依赖函数
  activeEffect = null;  // 清空,便于下次收集
}

测试案例:

1、监听基本类型数据

const count = ref(0);
watchEffect(() => {
  console.log('---', count.value);
})
watch(
  () => count,
  (newVal, oldVal) => {
    console.log(newVal, oldVal);
  }
);
count.value = 1;  // 控制台输出: --- 0,--- 1, 1 0

执行结果:

【js篇 - Proxy】- 4、使用Proxy实现简单的watchEffect和watch函数

2、监听对象

const obj = reactive({
  a: 1,
  b: {
    b1: 'b1'
  }
});
watchEffect(() => {
  console.log('---', obj.a);
})
watch(
  () => obj.a,
  (newVal, oldVal) => {
    console.log(newVal, oldVal);
  }
);
obj.a = 2; // 控制台输出:--- 1,--- 2, 2 1

执行结果:

【js篇 - Proxy】- 4、使用Proxy实现简单的watchEffect和watch函数