由于本人在日常开发中,主要使用React。但是在React中,本身是没有类似响应式的api,广泛使用上基本是 useState 数据存储 + useEffect / useMemo等 副作用函数执行,逻辑上是可行的,但是很是眼馋Vue3 中的
watchEffect使用,开发者可以不用 去追踪依赖,watchEffect会 立即运行一个函数,同时响应式的追踪依赖,并在依赖更改时重新执行。示例如下
所以我 就来简单实现一下
简单逻辑梳理
因为我只是研读过vue2 中的依赖收集原理,所以我这边 就简单表述下 如何去做到 自动响应式追踪依赖,并在依赖更改时重新执行。
大致流程如下。 不大会画图 😭
这边就简单说下,底层逻辑是,执行 副作用函数,先清除 自己依赖项,因为可能存在 不再依赖其中某一项。比如如下, 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 实现,再重新会看下面流程图, 应该会适当清晰点(应该吧)
最终代码
- 发现了一个问题,数组 的
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 实现
所以 这边也可以简单改造一下就是 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
现在知道了吧-。-