经过前面第一章,我们已经建好了渲染系统和Vdom相关。
那第二步则是构建双向绑定的响应式数据系统,我们知道在Vue2中数据双向绑定是通过Object.defineProperty来实现的,通过这个API来实现的数据双向绑定有一些缺点。主要是:
i. 所有需要进行绑定观察的对象属性必须要先定义好,因为在实现上是通过遍历所有key来修改getter和setter方法来实现的,所以新增的属性就没办法响应式了,除非手动调用
方法来进行处理
ii. 数组类的操作因为不会触发getter和setter,因此Vue2是重写了Array的push,splice等方法来实现的响应式数据,因此直接修改数组指定index的item时候无法触发响应式的更新,
并且重写数组原生方法也可能带来一些意想不到的问题。
基于此,Vue3使用ES6新增的Proxy属性来实现响应式数据绑定,上述所提到的两个问题都解决了,唯一的缺点就是IE系列低版本并不支持这个特性。当然我们因为业务都是在移动端的,
因此并没有这个情况。这个情况应该是Vue3框架他们应当考虑的问题了。关于Proxy可以前往MDN参考Proxy特性
介绍完了基本的情况,下面我们就来开始实现它:
要想实现一个双向数据绑定,并且在数据更新的时候能够通知到使用方,那么我们肯定得在数据读取和设置的两个地方都要有狗子,当我们读取数据的时候,将数据的依赖函数注册,当我们
设置数据的时候,将新的值作为参数传递给注册的函数并调用。这样就实现了响应式的数据系统。
<html> <head> <title>Mini Vue3</title> </head> <body> <script> let activeEffect = null; class Dep { // 构造函数 constructor (value) { // 初始化订阅列表 this.subscribers = new Set(); // 存储需要跟踪依赖的值 this._value = value; } get value() { // 取值时添加依赖 this.depend(); return this._value; } set value(newValue) { // 设置时通知更新 this._value = newValue; this.notify(); } // 添加依赖 depend() { if (activeEffect) { this.subscribers.add(activeEffect); } } // 依赖更新,通知回调 notify() { this.subscribers.forEach(effect => { effect(); }); } } /** * @ddecription 注册事件,当 */ function watchEffect(effect) { activeEffect = effect; effect(); activeEffect = null; } const dep = new Dep(‘hello‘); // 注册依赖 watchEffect(() => { console.log(dep.value); }); // 依赖更新 dep.value = ‘changed‘; </script> </body> </html>
这个依赖关系类起什么作用的呢,我们看到其中有存储注册回调的subscribes,添加依赖的depend,以及通知依赖更新的notify。
当我们在get value时注册依赖,将watchEffect中的effect回调保存下来。然后在set value的时候通知回调执行。这样我们就实现了一个自动管理依赖的class了。
那么这里的watchEffect是个什么函数呢,跟之前Vue2中的watch有什么区别呢?
可以看下目前Vue3的组合式API草案文档中关于watchEffect的描述:watchEffect
与之前watch不同的是,这个会立即执行,不需要immediate参数。并且可以在任意地方引入。
可以看到已经实现了我们的目的,立即执行输出hello,并且在value改变之后触发了执行输出了changed。
因此此时已经建立依赖关系类Dep,但是我们在实际Vue3中用到的则是对一个对象建立响应式观察用的是reactive方法,具体可以参考Reactive
因为这里的Dep我们应当是用来作为依赖管理类,而不是监控自身的值。对于对象值的处理应当由reactive方法处理,因此我们来实现reactive方法如下:
<html> <head> <title>Mini Vue3</title> </head> <body> <script> let activeEffect = null; class Dep { subscribers = new Set(); // 添加依赖 depend() { if (activeEffect) { this.subscribers.add(activeEffect); } } // 依赖更新,通知回调 notify() { this.subscribers.forEach(effect => { effect(); }); } } /** * @decription 注册事件,当依赖的对象发生改变时,触发effect方法执行 */ function watchEffect(effect) { activeEffect = effect; effect(); activeEffect = null; } /** * @description 给对象封装并返回响应式代理 */ function reactive(oldObj) { } // 需要实现的效果类似于 const state = reactive({ count: 0 }); watchEffect(() => { console.log(state.count); }); // 第一次应当输出0 state.count += 1; // 此时应当输出1 </script> </body> </html>
我们应当要实现这样的效果。接下来我们就要填充reactive方法
如果是在Vue2,那么方法类似这样:
/** * @description 给对象封装并返回响应式代理 */ function reactive(oldObj) { Object.keys(oldObj).forEach(key => { const dep = new Dep(); let value = oldObj[key]; Object.defineProperty(oldObj, key, { get () { // 注册依赖 dep.depend(); return value; }, set (newValue) { value = newValue; // 通知依赖更新 dep.notify(); } }); return oldObj; }); }
这也是为什么在Vue2中新增的属性不会自动加入响应式,而需要通过Vue.set手工调用。
而在Vue3中使用的是Proxy,则没有这个问题了。所以在Vue3中的reactive方法实现如下:
<html> <head> <title>Mini Vue3</title> </head> <body> <script> let activeEffect = null; class Dep { subscribers = new Set(); // 添加依赖 depend() { if (activeEffect) { this.subscribers.add(activeEffect); } } // 依赖更新,通知回调 notify() { this.subscribers.forEach(effect => { effect(); }); } } /** * @decription 注册事件,当依赖的对象发生改变时,触发effect方法执行 */ function watchEffect(effect) { activeEffect = effect; effect(); activeEffect = null; } const reactiveHandlers = { get(target, key, receiver) { }, set(target, key, value, receiver) { } }; /** * @description 给对象封装并返回响应式代理 */ function reactive(oldObj) { return new Proxy(oldObj, reactiveHandlers); } // 需要实现的效果类似于 const state = reactive({ count: 0 }); watchEffect(() => { console.log(state.count); }); // 第一次应当输出0 state.count += 1; // 此时应当输出1 </script> </body> </html>
但是这里有个问题,因为reactiveHandlers作为公共handler定义了,那么每个对象的依赖要如何管理呢?
我们通过一个weakMap来管理所有的依赖关系。之所以使用weakMap,因为weakMap只能使用对象做key,所以不能遍历.因此当对象被回收之后,map内的引用全无,因此map也能够被回收,这是Vue3这里之所以使用weakMap的原因。
新的代码如下:
<html> <head> <title>Mini Vue3</title> </head> <body> <script> let activeEffect = null; class Dep { subscribers = new Set(); // 添加依赖 depend() { if (activeEffect) { this.subscribers.add(activeEffect); } } // 依赖更新,通知回调 notify() { this.subscribers.forEach(effect => { effect(); }); } } /** * @decription 注册事件,当依赖的对象发生改变时,触发effect方法执行 */ function watchEffect(effect) { activeEffect = effect; effect(); activeEffect = null; } const targetMap = new WeakMap(); function getDep(target, key) { // 添加整个对象的依赖 let currMap = targetMap.get(target); if (!currMap) { currMap = new Map(); targetMap.set(target, currMap); } // 获取具体key的依赖 let dep = currMap.get(key); // 如果没有添加过则添加依赖 if (!dep) { dep = new Dep(); currMap.set(key, dep); } return dep; } const reactiveHandlers = { get(target, key, receiver) { const dep = getDep(target, key); // 为什么每次读取都要添加依赖,因为有可能会增加新的依赖 dep.depend(); return Reflect.get(target, key, receiver); }, set(target, key, value, receiver) { const dep = getDep(target, key); // 类似于之前的Object.set const ret = Reflect.set(target, key, value, receiver); // 通知执行依赖 dep.notify(); // 因为proxy的set必须要返回一个boolean的值告诉设置成功与否,因此这里返回系统api的结果即可 return ret; } }; /** * @description 给对象封装并返回响应式代理 */ function reactive(oldObj) { return new Proxy(oldObj, reactiveHandlers); } // 需要实现的效果类似于 const state = reactive({ count: 0 }); watchEffect(() => { console.log(state.count); }); // 第一次应当输出0 state.count += 1; // 此时应当输出1 </script> </body> </html>
可以看到效果:
没有问题,关于代码中所使用的Reflect可能有些同学会有疑问,相关内容可以参见Reflect,需要注意的是,因为基于Proxy的响应式数据的响应式其实是在对象上,而非Vue2的对象属性,因此新增属性也没有问题。而且利用Proxy的handler中的方法,除了get和set,连判断属性是否存在的has等方法也都可以追踪和处理依赖,具体的可以去阅读前面所发的Proxy的MDN资料。
当然这里写的都是极为简单的没有考虑边界情况。但是毕竟我们所需要的是了解核心机制,边界情况并非核心机制了
原文:https://www.cnblogs.com/gguoyu/p/13290495.html