响应式原理

提到Vue难免离不开响应式原理,在学习响应式原理之前,首先需要搞明白一个概念:何谓响应式?Vue官方文档响应式的解释是:响应性是一种允许我们以声明式的方式去适应变化的编程范例,乍一看还挺难以理解,用通俗点的大白话来说:响应式就是在数据发生变化的时候执行某个或者某些回调,使页面发生更新,也就是数据驱动页面的更新。今天我们就动手使用原生JS实现Vue的响应式原理

如何实现响应式

  1. 首先我们需要准备一个对象

    1. let info = {
    2. name:'coderwei',
    3. age:18
    4. }
  2. 首先我们可以先定义一个副作用函数,说白了就是我们需要告诉JS,当对象的属性发生变化后,你要做什么

    1. //副作用函数:要求传递进来一个函数,函数内部直接执行
    2. function watchFn(fn) {
    3. fn();
    4. }
  3. 我们需要利用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); }, });

  1. 4. 然后我们在写一点测试数据
  2. ```javascript
  3. // name发生变化需要执行的逻辑
  4. watchFn(() => {
  5. console.log( infoProxy.name);
  6. });
  7. // age发生变化需要执行的逻辑
  8. watchFn(() => {
  9. console.log( infoProxy.age);
  10. console.log("-------------");
  11. });
  1. 运行代码,我们可以看到当设置对象某个属性的值的时候会来到set方法,当我们获取某个属性的值的时候会来到get方法说明我们劫持成功了
  2. 于是我们就可以在这两个方法上下功夫,首先我们定义一个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数组内的所有函数

  1. 7. 我们前面说过当我们获取对象的值的时候我们会来到get方法,所以我们可以在get方法中将收集依赖,我们修改下Proxy的方法,但是这个有个问题,我们在调用set方法的adddepend时候,需要给他传递调用watchFn里面的函数,所以我们可以定义一个全局变量 ,用于保存这个函数,于是定义的watchFn的函数需要稍微修改下
  2. ```javascript
  3. let activeFn = null;
  4. function watchFn(fn) {
  5. activeFn = fn;
  6. fn();
  7. activeFn = null;
  8. }
  1. 然后重新回到定义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(); }, });

  1. 9. 这里用到了WeakMapWeakMapMap最大的区别就是前者的键是一个对象,并且这是一个弱引用,首先我们通过get方法可以拿到这个对象的Map,判断下有没有值,第一次都是没有值的,如果没有值我们就给他初始化一个Map,然后在根据对象的key,拿到这个key所对应的依赖,当然,如果没有我们也需要初始化一个我们定义的Depend,最后将这个依赖返回出去,我们就可以在调用的时候根据对象和key拿到对应的依赖,从而执行他们对应的依赖
  2. 10. 这里也用到了Reflect,他是ES6新增的一个内置对象,他提供了拦截JS操作的方法,为什么会出现这个方法呢?在我看来,在早期的JS中,有些东西是考虑不周到的,设计的也不合理,所以一些Objcet对象下有很多方法压根就不能属于他,放在它身上很不合适,所以导致Object这个对象特别臃肿。[MDN上对Reflect的详细解释](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect)
  3. 11. 于是我们就完成了Vue3的响应式原理,当数据发生变化的时候就会触发对应的回调
  4. <a name="aa84ec94"></a>
  5. ### 优化
  6. 1. 紧接着我们来重构下部分代码,目前来说代码有点bug,虽然不影响功能,但是当我们watchFn中的函数多次用到了代理对象的值,那么在触发依赖的时候就会多次执行,因为每次获取一次对象的值的时候,都会来到get方法,然后将这个函数存起来,明明都是同一个函数并且还是同一个key的依赖,没必要多次执行浪费性能。所以我们可以将数组更换为set
  7. 2. 其次就是当我们有多个对象需要拦截的时候,难道我们都写一次Proxy方法吗?明显不现实,所以我们可以将拦截对象对的getset方法的操作封装成一个函数
  8. <a name="b6532c30"></a>
  9. #### 最终代码
  10. ```javascript
  11. let activeFn = null;
  12. class Depend {
  13. constructor() {
  14. this.allFn = new Set();
  15. }
  16. adddepend() {
  17. if (activeFn) {
  18. this.allFn.add(activeFn);
  19. }
  20. }
  21. notify() {
  22. this.allFn.forEach((fn) => fn());
  23. }
  24. }
  25. function watchFn(fn) {
  26. activeFn = fn;
  27. fn();
  28. activeFn = null;
  29. }
  30. let info = {
  31. name: "coderwei",
  32. age: 18,
  33. };
  34. const weMap = new WeakMap();
  35. function getDepend(target, key) {
  36. let map = weMap.get(target);
  37. if (!map) {
  38. map = new Map();
  39. weMap.set(target, map);
  40. }
  41. let depend = map.get(key);
  42. if (!depend) {
  43. depend = new Depend();
  44. map.set(key, depend);
  45. }
  46. return depend;
  47. }
  48. //然后修改下Proxy的代码
  49. function reactive(obj) {
  50. return new Proxy(info, {
  51. get(target, key, recesver) {
  52. const depend = getDepend(target, key);
  53. depend.adddepend(activeFn);
  54. return Reflect.get(target, key, recesver);
  55. },
  56. set(target, key, newValue, recesver) {
  57. Reflect.set(target, key, newValue, recesver);
  58. const depend = getDepend(target, key);
  59. depend.notify();
  60. },
  61. });
  62. }
  63. const infoProxy = reactive(info);
  64. // name发生变化需要执行的逻辑
  65. watchFn(() => {
  66. console.log("name:", infoProxy.name);
  67. });
  68. // age发生变化需要执行的逻辑
  69. watchFn(() => {
  70. console.log("age:", infoProxy.age);
  71. console.log("-------------");
  72. });
  73. infoProxy.age = 99988;

Vue2的响应式

  1. 基本上大同小异,只是劫持对象用的方法不一样,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”; ```