不到100行实现Vue3 中的WatchEffect

520 阅读8分钟

由于本人在日常开发中,主要使用React。但是在React中,本身是没有类似响应式的api,广泛使用上基本是 useState 数据存储 + useEffect / useMemo等 副作用函数执行,逻辑上是可行的,但是很是眼馋Vue3 中的 watchEffect 使用,开发者可以不用 去追踪依赖,watchEffect会 立即运行一个函数,同时响应式的追踪依赖,并在依赖更改时重新执行。

示例如下

image.png

所以我 就来简单实现一下

简单逻辑梳理

因为我只是研读过vue2 中的依赖收集原理,所以我这边 就简单表述下 如何去做到 自动响应式追踪依赖,并在依赖更改时重新执行。

image.png 大致流程如下。 不大会画图 😭

image.png

这边就简单说下,底层逻辑是,执行 副作用函数,先清除 自己依赖项,因为可能存在 不再依赖其中某一项。比如如下, c 属性 会影响 a 或者 b 的依赖收集。 只会出现 a + c => effectfn 或者 b + c => effectfn

watchEffect(function test() {
  console.log("effect");
  if (a.c) {
    console.log("a.a:", a.a);
  } else {
    console.log("a.b:", a.b);
  }
});

然后就是 副作用函数 被收集, 这里的被收集就需要依赖于响应式数据,因为要自动嘛。

响应式处理

在Vue 主要是使用 Object.defineProperty 去 拦截 get, 来进行收集,但是 因为 Object.defineProperty 只能对对象的属性进行劫持,无法对整个对象进行劫持,也无法劫持新增的属性。 还有就是无法 拦截数组的变化,所以这边就是用 Proxy 来做响应式处理处理。 之前也简单写了一个文章。数据拦截

 // 存储 响应式处理数据,防止反复去 proxy代理
const cachedData = new WeakMap();

// 这里就很简单啦,只是去拦截 Get 和Set。
function handleProxyData(data, getter, setter) {
  if (typeof data !== "object" || data === null) return data;
  const t = cachedData.get(data);
  if (t) return t;
  const proxyObj = new Proxy(data, {
    get(target, key) {
      getter(target, key)
      // 注意这边 获取的 value 也直接 Proxy 做一次代理。
      return handleProxyData(Reflect.get(target, key), getter, setter);
    },

    set(target, key, val) {
      const oldVal = Reflect.get(target, key);
      // 注意 这边新增的属性 value 也直接 proxy 代理一次
      const isSetSuc = Reflect.set(target, key, handleProxyData(val, getter, setter));
      // 有数据改变就执行, setter callback
      oldVal !== val && setter && setter(target, key, val, oldVal);
      return isSetSuc;
    },
  });

  // 存储一次 proxyData 和data
  cachedData.set(proxyObj, proxyObj);
  cachedData.set(data, proxyObj);
  return proxyObj;
}

以上就是简单的一个 proxy 做响应式数据的处理方式。 按照 上面的逻辑, getter 就是为了 收集副作用函数, setter 是为了重新执行 副作用函数(取消依赖和重新收集)。

makeReactive

分别定义 track(getter callback) 函数来进行 收集 副作用函数, trigger(setter callback) 来执行 副作用函数 集合。所以我们 的makeReactive 就可以你这样表述

function makeReactive(data) {
  return handleProxyData(data, (target, key) => {
    // 将 副作用函数 丢到 target => key 里面
    track(target, key);
  }, (target, key, value, oldValue) => {
    // 执行 target => key 里面的 副作用列表
    trigger(target, key, value, oldValue);
  })
}

track

track 函数类型, 有两个 入参,一个是修改的 数据 target 和具体的哪个属性 key, 为的,就是想把当前挂起的 副作用函数,放到其 setter callback 里面,在后续的 trigger 中可以直接执行。

所以我们可以简单构思下其 存储方式, target 一定是个 引用类型, key 有可能是 string 或者 对象,所以,我们就可以简单定义 依赖存储方式

const depWeakMap = new WeakMap<object, Map<any, Set<() => any>>>();

这边最后是 使用 Set 集合类型的数据 结构来进行 存储副作用列表。

所以 track 就是为了先去 寻找 target 下的 所有key-map,然后查找具体key 下面 副作用集合。 简单代码就如下

const Dep = {
  target: null,
};

function track(target, key) {
  // 当前 副作用函数
  const effectFn = Dep.target;
  if (!effectFn) return;

  let targetMap = depWeakMap.get(target);
  if (!targetMap) {
    targetMap = new Map();
    depWeakMap.set(target, targetMap);
  }

  let targetKeyCallBackSet = targetMap.get(key);
  if (!targetKeyCallBackSet) {
    targetKeyCallBackSet = new Set();
    targetMap.set(key, targetKeyCallBackSet);
  }

  // effect 函数 被收集在哪些 targetKey 里面
  // 这里注意一下, 这里是为了 能够快速清除 副作用依赖,把 自己set 放到副作用函数下的 deps 属性。
  // 下面会讲到 先可以简单过一下
  let effectFnDeps = effectFn.deps;
  effectFnDeps.add(targetKeyCallBackSet);

  // 添加 副作用函数
  targetKeyCallBackSet.add(effectFn);
}

以上就是 track 的实现方式, 逻辑也相对比较简单,目的是为了把当前被挂起的 副作用函数,放到具体的 targetKeyCallBackSet 下面。 那trigger 就更简单了

function trigger(target, key) {
  let targetMap = depWeakMap.get(target);
  if (!targetMap) return;

  let targetKeyCallBackSet = targetMap.get(key);
  if (!targetKeyCallBackSet) return;

  // 注意一下 这里 之所以不用 Set 类型的 forEach 是因为 副作用函数执行会重新收集依赖 targetKeyCallBackSet 会后续不断 add,导致这边 会不断执行,这边就先用 Array.from 去处理一下,后续再看看怎么 优化吧
  const cbs = Array.from(targetKeyCallBackSet);
  cbs.forEach((fn) => {
    console.log("trigger ", target, key);
    fn();
  });
}

好啦,这边有了 makeReactive 做响应式, track 辅助收集副作用函数, trigger 执行响应式数据 Setter后的 副作用列表。所以,最后的 watchEffect 实现代码就简单如下。

function watchEffect(callback) {
  // 把回调函数包装成 effect 函数
  const effectFn = effect(callback);
  // 立即执行一次 effect 函数
  effectFn();
}

function effect(callback) {
  // 创建响应式副作用函数
  const effectFn = () => {
    // 挂起 副作用函数
    Dep.target = effectFn;

    let effectFnDeps = effectFn.deps;
    if (!effectFnDeps) {
      effectFnDeps = effectFn.deps = new Set();
    }
    effectFnDeps.forEach((depSet) => depSet.delete(effectFn));

    // 执行回调函数
    callback();
    Dep.target = null;
  };
  return effectFn;
}

上面要注意一下,这部分代码

function effect(callback) {
  // 创建响应式副作用函数
  const effectFn = () => {
    // 挂起 副作用函数
    //...

    // 这里就是为了清除 上一次的 副作用依赖项
    let effectFnDeps = effectFn.deps;
    if (!effectFnDeps) {
      effectFnDeps = effectFn.deps = new Set();
    }
    // 
    effectFnDeps.forEach((depSet) => depSet.delete(effectFn));

    // 执行回调函数
    //...
  };
  return effectFn;
}

至此就完成了 watchEffect 实现,再重新会看下面流程图, 应该会适当清晰点(应该吧)

image.png

最终代码

  • 发现了一个问题,数组 的 push 的监听不大行, 稍微调试了一下,因为push 是先设置索引,但是设置了索引之后 length 就 变了,后面再去设置 length 就比对不出来了,所以,这边 就修改下
const cachedData = new WeakMap();

function handleProxyData(data, getter, setter) {
  if (typeof data !== "object" || data === null) return data;
  const t = cachedData.get(data);
  if (t) return t;
  const proxyObj = new Proxy(data, {
    get(target, key) {
      getter(target, key);
      return handleProxyData(Reflect.get(target, key), getter, setter);
    },

    set(target, key, val) {
      const oldVal = Reflect.get(target, key);
      const isSetSuc = Reflect.set(
        target,
        key,
        handleProxyData(val, getter, setter)
      );
      if (setter) {
        // 这里调整了下 针对数组的修改,这里就简单这样吧,length 修改了,就盘点过,数组改变了
        if (oldVal !== val) {
          setter(target, key, val, oldVal);
        } else if (key === "length") {
          setter(target, key, val, oldVal);
        }
      }
      return isSetSuc;
    },
  });

  cachedData.set(proxyObj, proxyObj);
  cachedData.set(data, proxyObj);
  return proxyObj;
}

const Dep = {
  target: null,
};

const depWeakMap = new WeakMap();

function track(target, key) {
  const effectFn = Dep.target;
  if (!effectFn) return;
  // effect 函数 被收集在哪些 targetKey 里面
  let effectFnDeps = effectFn.deps;

  let targetMap = depWeakMap.get(target);
  if (!targetMap) {
    targetMap = new Map();
    depWeakMap.set(target, targetMap);
  }

  let targetKeyCallBackSet = targetMap.get(key);
  if (!targetKeyCallBackSet) {
    targetKeyCallBackSet = new Set();
    targetMap.set(key, targetKeyCallBackSet);
  }

  effectFnDeps.add(targetKeyCallBackSet);

  targetKeyCallBackSet.add(effectFn);
}

function trigger(target, key, val, oldVal) {
  let targetMap = depWeakMap.get(target);
  if (!targetMap) return;

  let targetKeyCallBackSet = targetMap.get(key);
  if (!targetKeyCallBackSet) return;

  const cbs = Array.from(targetKeyCallBackSet);

  cbs.forEach((fn) => {
    // console.log("trigger ", target, key);
    fn(val, oldVal);
  });
}

// 处理响应式数据
function makeReactive(data) {
  return handleProxyData(
    data,
    (target, key) => {
      track(target, key);
    },
    (target, key, value, oldValue) => {
      trigger(target, key, value, oldValue);
    }
  );
}

function watchEffect(callback) {
  // 把回调函数包装成 effect 函数
  const effectFn = effect(callback);
  // 立即执行一次 effect 函数
  effectFn();
}

function effect(callback) {
  // 创建响应式副作用函数
  const effectFn = () => {
    // 挂起被收集的依赖
    Dep.target = effectFn;

    // 清除副作用依赖项
    let effectFnDeps = effectFn.deps;
    if (!effectFnDeps) {
      effectFnDeps = effectFn.deps = new Set();
    }
    effectFnDeps.forEach((depSet) => depSet.delete(effectFn));

    // 执行回调函数
    callback();
    Dep.target = null;
  };
  return effectFn;
}

function watch(getter, effectFn) {
  // 挂起被收集的依赖
  Dep.target = effectFn;

  // 清除副作用依赖项
  let effectFnDeps = effectFn.deps;
  if (!effectFnDeps) {
    effectFnDeps = effectFn.deps = new Set();
  }
  effectFnDeps.forEach((depSet) => depSet.delete(effectFn));

  // 执行回调函数
  getter();
  Dep.target = null;
}

const state = makeReactive({ a: [1] });

watchEffect(function () {
  state.a.forEach((it) => {
    console.log(it);
  });
});

state.a.push(2);

使用如下

const state = makeReactive({a: 1, b:2, isAdd: true})
    
watchEffect(function () {
  if (state.isAdd) {
    state.sum = state.a + state.b
  } else {
    state.sum = state.a * state.b
  }
  // 使用sum
  console.log('current sum of data:', state.sum)
})


state.isAdd = false
// current sum of data: 2

state.isAdd = true
// current sum of data: 3

state.a = 200
// current sum of data: 202

state.isAdd = false
// current sum of data: 400

上面就是一个简单的使用方式,当然还有很多细节上并没有去处理,比如, 两个 watchEffect 分别去 使用相同属性, 这边只是使用 Dep.target 挂起 单个副作用函数的话,是不够的,还有这边是 立即执行 setter callback,后续可以 修改 用 微任务去执行 callback。 这些后续可以在研究下,这一版还是可以 简单实用的。

拓展一下

本质上 vue2 里面的watch,vue 实例化之后,就是会去 getter 对应属性 来进行 依赖收集,vue 源码在这 watch 实现

image.png

所以 这边也可以简单改造一下就是 watch的使用方式了

function watch(getter, effectFn){
  // 挂起被收集的依赖
  Dep.target = effectFn;

  // 清除副作用依赖项
  let effectFnDeps = effectFn.deps;
  if (!effectFnDeps) {
    effectFnDeps = effectFn.deps = new Set();
  }
  effectFnDeps.forEach((depSet) => depSet.delete(effectFn));

  // 执行回调函数
  getter();
  Dep.target = null;
}

然后改造下trigger, 本来 trigger 就有四个入参,只不过,watchEffect 不需要我就没写了

function trigger(target, key, val, oldVal) {
  let targetMap = depWeakMap.get(target);
  if (!targetMap) return;

  let targetKeyCallBackSet = targetMap.get(key);
  if (!targetKeyCallBackSet) return;

  const cbs = Array.from(targetKeyCallBackSet);

  cbs.forEach((fn) => {
    // console.log("trigger ", target, key);
    fn(val, oldVal);
  });
}

使用方式

const state = makeReactive({a: 1, b:2, isAdd: true})

watch(() => {
  state.a
}, (val, oldVal) => {
  console.log('a属性改变啦', val, oldVal)
})

state.a = 1111
// a属性改变啦 1111 1

这里就是 真正意义上watch 的实现方式, 可能会说,我看到的 watch 不是这样的呀。 watch 后面是一个 对象来着

👌🏻

那就再安排一下

function vueWtach(obj, reactiveData){
  const keys = Object.keys(obj)
  keys.forEach(k => {
    watch(() => {
      // 这里是可以拓展的 先这样吧
      reactiveData[k]
    }, obj[k])
  })
}

const state = makeReactive({a: 1, b:2, isAdd: true})

vueWtach(
  {
    b(val, oldVal) {
      console.log('b 属性改变啦', val, oldVal)
    }
  }, state
)

state.b = 233
// b 属性改变啦 233 2

现在知道了吧-。-

参考文献