响应式原理
提到Vue难免离不开响应式原理,在学习响应式原理之前,首先需要搞明白一个概念:何谓响应式?Vue官方文档响应式的解释是:响应性是一种允许我们以声明式的方式去适应变化的编程范例,乍一看还挺难以理解,用通俗点的大白话来说:响应式就是在数据发生变化的时候执行某个或者某些回调,使页面发生更新,也就是数据驱动页面的更新。今天我们就动手使用原生JS实现Vue的响应式原理
如何实现响应式
首先我们需要准备一个对象
let info = {name:'coderwei',age:18}
首先我们可以先定义一个副作用函数,说白了就是我们需要告诉JS,当对象的属性发生变化后,你要做什么
//副作用函数:要求传递进来一个函数,函数内部直接执行function watchFn(fn) {fn();}
我们需要利用Proxy拦截info对象的get和set方法想要实现响应式
```javascript
const infoProxy = new Proxy(info, { get(target, key, recesver) { console.log(“获取info对象的值”); return Reflect.get(target, key, recesver); }, set(target, key, newValue, recesver) { console.log(“设置info对象的值”); Reflect.set(target, key, newValue, recesver); }, });
4. 然后我们在写一点测试数据```javascript// name发生变化需要执行的逻辑watchFn(() => {console.log( infoProxy.name);});// age发生变化需要执行的逻辑watchFn(() => {console.log( infoProxy.age);console.log("-------------");});
- 运行代码,我们可以看到当设置对象某个属性的值的时候会来到set方法,当我们获取某个属性的值的时候会来到get方法说明我们劫持成功了
- 于是我们就可以在这两个方法上下功夫,首先我们定义一个Depend类
```javascript class Depend { constructor() { this.allFn = []; } adddepend() { if (activeFn) { this.allFn.push(activeFn); } } notify() { this.allFn.forEach((fn) => fn()); } }
//在constructor方法中定义一个函数,每次实例化这个类的时候都需要创建一个新的数组,用于放置需要执行的函数,如果我们将所有的函数都丢在一个数组内,那我们就没办法作区分了,不同的属性修改了他们是可能会有不同的回调, //然后在定义一个adddepend函数,用于将函数push进数组中 //最后我们定义了一个notify的函数,用于执行allFn数组内的所有函数
7. 我们前面说过当我们获取对象的值的时候我们会来到get方法,所以我们可以在get方法中将收集依赖,我们修改下Proxy的方法,但是这个有个问题,我们在调用set方法的adddepend时候,需要给他传递调用watchFn里面的函数,所以我们可以定义一个全局变量 ,用于保存这个函数,于是定义的watchFn的函数需要稍微修改下```javascriptlet activeFn = null;function watchFn(fn) {activeFn = fn;fn();activeFn = null;}
然后重新回到定义infoProxy的地方,再对代码进行一点修改
```javascript //首先我们先封装一个方法,用于获取每个属性对应的depend const weMap = new WeakMap(); function getDepend(target, key) { let map = weMap.get(target); if (!map) { map = new Map(); weMap.set(target, map); }let depend = map.get(key); if (!depend) { depend = new Depend(); map.set(key, depend); } return depend; }
//然后修改下Proxy的代码 const infoProxy = new Proxy(info, { get(target, key, recesver) { const depend = getDepend(target, key); depend.adddepend(activeFn); return Reflect.get(target, key, recesver); }, set(target, key, newValue, recesver) { Reflect.set(target, key, newValue, recesver); const depend = getDepend(target, key); depend.notify(); }, });
9. 这里用到了WeakMap,WeakMap和Map最大的区别就是前者的键是一个对象,并且这是一个弱引用,首先我们通过get方法可以拿到这个对象的Map,判断下有没有值,第一次都是没有值的,如果没有值我们就给他初始化一个Map,然后在根据对象的key,拿到这个key所对应的依赖,当然,如果没有我们也需要初始化一个我们定义的Depend,最后将这个依赖返回出去,我们就可以在调用的时候根据对象和key拿到对应的依赖,从而执行他们对应的依赖10. 这里也用到了Reflect,他是ES6新增的一个内置对象,他提供了拦截JS操作的方法,为什么会出现这个方法呢?在我看来,在早期的JS中,有些东西是考虑不周到的,设计的也不合理,所以一些Objcet对象下有很多方法压根就不能属于他,放在它身上很不合适,所以导致Object这个对象特别臃肿。[MDN上对Reflect的详细解释](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect)11. 于是我们就完成了Vue3的响应式原理,当数据发生变化的时候就会触发对应的回调<a name="aa84ec94"></a>### 优化1. 紧接着我们来重构下部分代码,目前来说代码有点bug,虽然不影响功能,但是当我们watchFn中的函数多次用到了代理对象的值,那么在触发依赖的时候就会多次执行,因为每次获取一次对象的值的时候,都会来到get方法,然后将这个函数存起来,明明都是同一个函数并且还是同一个key的依赖,没必要多次执行浪费性能。所以我们可以将数组更换为set2. 其次就是当我们有多个对象需要拦截的时候,难道我们都写一次Proxy方法吗?明显不现实,所以我们可以将拦截对象对的get和set方法的操作封装成一个函数<a name="b6532c30"></a>#### 最终代码```javascriptlet activeFn = null;class Depend {constructor() {this.allFn = new Set();}adddepend() {if (activeFn) {this.allFn.add(activeFn);}}notify() {this.allFn.forEach((fn) => fn());}}function watchFn(fn) {activeFn = fn;fn();activeFn = null;}let info = {name: "coderwei",age: 18,};const weMap = new WeakMap();function getDepend(target, key) {let map = weMap.get(target);if (!map) {map = new Map();weMap.set(target, map);}let depend = map.get(key);if (!depend) {depend = new Depend();map.set(key, depend);}return depend;}//然后修改下Proxy的代码function reactive(obj) {return new Proxy(info, {get(target, key, recesver) {const depend = getDepend(target, key);depend.adddepend(activeFn);return Reflect.get(target, key, recesver);},set(target, key, newValue, recesver) {Reflect.set(target, key, newValue, recesver);const depend = getDepend(target, key);depend.notify();},});}const infoProxy = reactive(info);// name发生变化需要执行的逻辑watchFn(() => {console.log("name:", infoProxy.name);});// age发生变化需要执行的逻辑watchFn(() => {console.log("age:", infoProxy.age);console.log("-------------");});infoProxy.age = 99988;
Vue2的响应式
- 基本上大同小异,只是劫持对象用的方法不一样,Vue3用的Proxy,Vue2用的Object.defineProperty,唯一不同的地方也就在这里,我就直接贴上我的代码了,如果有疑问可以联系我
```javascript // 勿在浮沙筑高 let activeFn = null;
// 封装一个Depend类 用于收集依赖 class Depend { constructor() { this.allFn = new Set(); } addDepend() { if (activeFn) { this.allFn.add(activeFn); } } notify() { this.allFn.forEach((fn) => fn()); } }
let info = { name: “coderwhy”, age: 18, };
// vue2 ====> Object.defineProperty function reactive(obj) { Object.keys(obj).forEach((key) => { let value = obj[key]; Object.defineProperty(obj, key, { get() { const depend = getDepend(obj, key); depend.addDepend(); return value; }, set(newValue) { value = newValue; const depend = getDepend(obj, key); depend.notify(); }, }); }); return obj; }
// 封装响应式函数 function watchFn(fn) { activeFn = fn; fn(); activeFn = null; }
// 封装获取正确的depend ===========>WeakMap let wMap = new WeakMap(); function getDepend(target, key) { let map = wMap.get(target); if (!map) { map = new Map(); wMap.set(target, map); }
let depend = map.get(key); if (!depend) { depend = new Depend(); map.set(key, depend); } return depend; }
let infoProxy = reactive(info);
// name发生变化需要执行的逻辑 watchFn(() => { console.log(infoProxy.name); });
// age发生变化需要执行的逻辑 watchFn(() => { console.log(infoProxy.age); console.log(“——————-“); });
let test = { name: “test”, }; let testProxy = reactive(test); infoProxy.name = “zys”; infoProxy.age = “1999”; watchFn(() => { console.log(testProxy.name); }); testProxy.name = “123”; ```
