We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
本文所需知识:Proxy,Reflect
Proxy是ES6新增的对象,可以对指定对象做一层代理。
Proxy
ES6
当我们从一个对象中获取某一个键时,会触发Proxy中get的拦截,对某一个键赋值的时候,则会触发Proxy中set的拦截,依据此特性,可以实现一个简单的观察者模式。
get
set
在触发set拦截的时候,触发注册的观察函数。
// 存观察函数的数组 const observerList = []; // 创建可观察对象 const observable = (obj) => { const proxy = new Proxy(obj, { set(target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver); // 触发set拦截的时候遍历并调用观察函数 observerList.forEach((fn) => fn()); return result; }, }); return proxy; }; // 添加观察函数 const observer = (fn) => { fn(); observerList.push(fn); }; const obj = observable({ current: 0 }); observer(() => console.log(obj.current)); // 输出 0 obj.current++; // 输出 1
将观察函数放入队列中,每次触发set拦截的时候,触发队列中的观察函数,这样就实现了一个最简单观察者模式。
这样的方式,以一个比较大的问题就是没有包含依赖收集,参考如下情况:
const obj = observable({ a: 0, b: 0 }); observer(() => console.log(obj.a)); // 输出 0 observer(() => console.log(obj.b)); // 输出 0 obj.a++; // console.log(obj.a) => 输出 1 // console.log(obj.b) => 输出 0
在第二个观察函数中,并没有使用到obj.a,但是在触发set拦截的时候,会将所有的观察函数统一触发,所以第二个观察函数也被触发了。
obj.a
想要解决这个问题也比较简单,那就是将观察函数中使用的键名key和当前所在的观察函数做一个映射,即key => 观察函数[],用Map存储则可表示为Map<key,function[]>。
key
key => 观察函数[]
Map
Map<key,function[]>
现在的问题就是如何在对象被观察函数访问时,触发get拦截并获取到此对象所在的观察函数,简而言之就是observer(() => console.log(obj.a));中,如何在obj.a被访问的时候获取到() => console.log(obj.a)。
observer(() => console.log(obj.a));
() => console.log(obj.a)
在调用观察函数之前,我们可以将该函数存入一个数组中,执行观察函数,观察函数中访问的键触发get拦截,从数组中取得最后一项就是当前所在的函数。
语言描述比较无力,直接看代码吧,这部分比较长,我会逐行注释解释。
// 用来缓存正在运行的观察函数 const stack = []; // 储存对象key所对应的观察函数数组 const observerMap = new Map(); // key => 观察函数[] const observer = (fn) => { // 运行观察函数前,先将观察函数压入stack stack.push(fn); // 调用观察函数进行依赖收集 // 这是观察函数中访问对象的键,触发get拦截 fn(); // 依赖收集完后将观察函数从stack弹出 stack.pop(); }; const observable = (obj) => { const proxy = new Proxy(obj, { get(target, key, receiver) { // 这时候对应上方的fn()调用的时候 // 可以从stack数组中获取到最后一个函数,就是当前对象运行的函数 const runningFunction = stack[stack.length - 1]; if (runningFunction) { // 通过访问的key获取对应的观察函数列表 let observerList = observerMap.get(key); // 如果当前key没有对应的观察函数列表 if (!observerList) { // 用Set存储去重,以免多次访问相同key时重复调用 observerList = new Set(); // 存入Map,有了key=>Set的映射 observerMap.set(key, observerList); } // 如果当前的观察函数没有存入Set就存入 if (!observerList.has(runningFunction)) { observerList.add(runningFunction); } } return Reflect.get(target, key, receiver); }, }); return proxy; }; const obj = observable({ a: 0, b: 0 }); observer(() => console.log(obj.a)); observer(() => console.log(obj.b)); // 获取key所对应的观察函数 console.log(observerMap.get("a")); // Set[ () => console.log(obj.a) ] console.log(observerMap.get("b")); // Set[ () => console.log(obj.b) ]
根据输出可以看出,observerMap中已经将键对应的观察函数列表储存了下来,接下来只需要在set拦截中,获取key对应的观察函数,遍历调用就行了
observerMap
// 用来缓存正在运行的观察函数 const stack = []; // 储存对象key所对应的观察函数数组 const observerMap = new Map(); // key => 观察函数[] const observer = (fn) => { // 运行观察函数前,先将观察函数压入stack stack.push(fn); // 调用观察函数进行依赖收集 // 这是观察函数中访问对象的键,触发get拦截 fn(); // 依赖收集完后将观察函数从stack弹出 stack.pop(); }; const observable = (obj) => { const proxy = new Proxy(obj, { get(target, key, receiver) { // 这时候对应上方的fn()调用的时候 // 可以从stack数组中获取到最后一个函数,就是当前对象运行的函数 const runningFunction = stack[stack.length - 1]; if (runningFunction) { // 通过访问的key获取对应的观察函数列表 let observerList = observerMap.get(key); // 如果当前key没有对应的观察函数列表 if (!observerList) { // 用Set存储去重,以免多次访问相同key时重复调用 observerList = new Set(); // 存入Map,有了key=>Set的映射 observerMap.set(key, observerList); } // 如果当前的观察函数没有存入Set就存入 if (!observerList.has(runningFunction)) { observerList.add(runningFunction); } } return Reflect.get(target, key, receiver); }, set(target, key, value, receiver) { // 存下旧值做对比 const oldValue = target[key]; const result = Reflect.set(target, key, value, receiver); // 获取当前key所对应的观察函数列表 const observerList = observerMap.get(key); // 如果列表存在并且新值和旧值不同,遍历调用观察函数 if (observerList && oldValue !== value) { observerList.forEach((fn) => { fn(); }); } return result; }, }); return proxy; }; const obj = observable({ a: 0, b: 0 }); observer(() => console.log("a", obj.a)); // a 0 observer(() => console.log("b", obj.b)); // b 0 obj.a++; // a 1 (没有触发b的观察函数了)
这就是一个简单的、带依赖收集的观察者模式的实现。
现在这个极简版还有非常多的问题,这里列举几个:
observable
Object.keys
obj.a.b++
解决方法:
WeakMap
对象 => <键 => 观察函数Set>
target
键 => 观察函数Set
ownKeys
deleteProperty
这种模式在Vue 3中有所使用,Vue 3是将单独的功能分发为单独的包细分管理的,在@vue/reactivity中,特别提到了nx-js/observer-util,我也是学习这个库后,写了本文进行了一个简单的思路总结。
本文参考:
The text was updated successfully, but these errors were encountered:
No branches or pull requests
本文所需知识:Proxy,Reflect
Proxy
是ES6
新增的对象,可以对指定对象做一层代理。当我们从一个对象中获取某一个键时,会触发
Proxy
中get
的拦截,对某一个键赋值的时候,则会触发Proxy
中set
的拦截,依据此特性,可以实现一个简单的观察者模式。简单的观察者模式
在触发
set
拦截的时候,触发注册的观察函数。将观察函数放入队列中,每次触发
set
拦截的时候,触发队列中的观察函数,这样就实现了一个最简单观察者模式。依赖收集问题
这样的方式,以一个比较大的问题就是没有包含依赖收集,参考如下情况:
在第二个观察函数中,并没有使用到
obj.a
,但是在触发set
拦截的时候,会将所有的观察函数统一触发,所以第二个观察函数也被触发了。想要解决这个问题也比较简单,那就是将观察函数中使用的键名
key
和当前所在的观察函数做一个映射,即key => 观察函数[]
,用Map
存储则可表示为Map<key,function[]>
。现在的问题就是如何在对象被观察函数访问时,触发
get
拦截并获取到此对象所在的观察函数,简而言之就是observer(() => console.log(obj.a));
中,如何在obj.a
被访问的时候获取到() => console.log(obj.a)
。依赖收集的实现
在调用观察函数之前,我们可以将该函数存入一个数组中,执行观察函数,观察函数中访问的键触发
get
拦截,从数组中取得最后一项就是当前所在的函数。语言描述比较无力,直接看代码吧,这部分比较长,我会逐行注释解释。
根据输出可以看出,
observerMap
中已经将键对应的观察函数列表储存了下来,接下来只需要在set
拦截中,获取key
对应的观察函数,遍历调用就行了带依赖收集的简单观察者模式
这就是一个简单的、带依赖收集的观察者模式的实现。
现在这个极简版还有非常多的问题,这里列举几个:
observable
对象并且有相同的键名时,会将观察函数存在observerMap
的同一个key
,触发一个对象的更新会导致另一个对象观察函数执行Object.keys
时不会触发观察函数obj.a.b++
,不会触发观察函数解决方法:
WeakMap
做映射,即对象 => <键 => 观察函数Set>
的映射,在拦截函数里需要用target
对象找到当前的键 => 观察函数Set
的映射ownKeys
的拦截,在此拦截里自定义一个key
来存储对应的观察函数get
拦截里将深层对象转为observable
对象deleteProperty
的拦截这种模式在Vue 3中有所使用,Vue 3是将单独的功能分发为单独的包细分管理的,在@vue/reactivity中,特别提到了nx-js/observer-util,我也是学习这个库后,写了本文进行了一个简单的思路总结。
本文参考:
The text was updated successfully, but these errors were encountered: