# Vue 响应式原理解析

​ 数据的改变驱动视图的刷新是 Vue 的特性之一 而 其非侵入性的响应性系统 是我们研究的方向 今天我们就来深入探究一波其中的奥秘

# 基础案例

  • 观察基础案例 思考商品价格修改时 更新几处数据以及如何追踪这一切

    <div id="app">
      <div>单价: {{ product.price }}</div>
      <div>数量: {{ product.price * product.num }}</div>
      <div>总价: {{ getTotalPrice() }}</div>
      <div>优惠单价: {{ getProductSalePrice() }}</div>
      <div>优惠总价: {{ getSalePrice }}</div>
      <button @click="addNum">增加数量</button>
    </div>
    <script src="https://unpkg.com/vue@next"></script>
    <script>
      Vue.createApp({
        data() {
          return {
            product: {
              price: 100,
              num: 2,
            },
          };
        },
        methods: {
          addNum() {
            this.product.num++;
          },
          getTotalPrice() {
            return this.product.price * this.product.num;
          },
          getProductSalePrice() {
            return this.product.price * 0.8;
          },
        },
        computed: {
          getSalePrice() {
            return this.product.price * this.product.num * 0.8;
          },
        },
      }).mount('#app');
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37

# 分析与构建基础响应式数据

  • JaveScript 的工作方式是过程式的 而不是响应式的 所以 没办法时刻侦听到数据的变化

    let product = {
      price: 100,
      num: 2,
    };
    let totalPrice = 0;
    let productSalePrice = 0;
    
    totalPrice = product.price * product.num;
    productSalePrice = product.price * 0.8;
    
    console.log('更新前的总价:' + totalPrice);
    console.log('更新前的商品优惠价:' + productSalePrice);
    product.num++;
    console.log('更新后的总价:' + totalPrice);
    console.log('更新后的商品优惠价:' + productSalePrice);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  • 要想数据的到响应的变化 我们则需要重新求得结果

    • 遇到重复的代码 通常我们都会将其封装成为函数

    • 这里的 getTotalPrice 函数和 getProductSalePrice 函数就是 记录函数 effect

      let product = {
        price: 100,
        num: 2,
      };
      let totalPrice = 0;
      let productSalePrice = 0;
      
      function getTotalPrice() {
        totalPrice = product.price * product.num;
      }
      function getProductSalePrice() {
        productSalePrice = product.price * 0.8;
      }
      
      getTotalPrice();
      getProductSalePrice();
      console.log('更新前的总价:' + totalPrice);
      console.log('更新前的商品优惠价:' + productSalePrice);
      
      product.num++;
      getTotalPrice();
      getProductSalePrice();
      console.log('更新后的总价:' + totalPrice);
      console.log('更新后的商品优惠价:' + productSalePrice);
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
  • 注意观察数据和方法之间的关系

    • 在观察者设计模式中 依赖项会有订阅者 当依赖项改变状态后 订阅者会得到通知

    • 修改 product.price 必须重新运行 getTotalPricegetProductSalePrice 求得结果

    • 修改 product.num 必须重新运行 getTotalPrice 求得结果

    • 所以 我们要将数据和函数的关联记录下来

      // 数据与事件的关系记录表
      let depMap = new Object();
      
      // 保存数据与事件关系
      function track(key, eventName) {
        if (depMap[key]) {
          depMap[key].push(eventName);
        } else {
          depMap[key] = [eventName];
        }
      }
      
      // 修改数据时触发对应的事件
      function trigger(key) {
        let dep = depMap[key];
        if (dep) {
          dep.forEach(eventName => eventName());
        }
      }
      
      let product = {
        price: 100,
        num: 2,
      };
      
      let totalPrice = 0;
      let productSalePrice = 0;
      
      // 获取总价
      function getTotalPrice() {
        totalPrice = product.price * product.num;
      }
      
      // 获取商品优惠价
      function getProductSalePrice() {
        productSalePrice = product.price * 0.8;
      }
      
      // 手动记录关系
      track('price', getTotalPrice);
      track('price', getProductSalePrice);
      track('num', getTotalPrice);
      
      // 运算结果
      getTotalPrice();
      getProductSalePrice();
      console.log('更新前的总价:' + totalPrice);
      console.log('更新前的商品优惠价:' + productSalePrice);
      
      // 手动更新数据
      product.price = 200;
      trigger('price');
      console.log('更新后的总价:' + totalPrice);
      console.log('更新后的商品优惠价:' + productSalePrice);
      
      // console.log(depMap);
      // depMap = {
      //   price: ['getTotalPrice', 'getProductSalePrice'],
      //   num: ['getTotalPrice'],
      // };
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
  • 如果存在多个商品信息如何储存数据和函数的关系

    • 多个商品具有同样的属性 例如 价格 price 数量 num

      // 数据与事件的关系记录表
      const depMap = new Object();
      
      // 保存数据与事件关系
      function track(key, eventName) {
        if (depMap[key]) {
          depMap[key].push(eventName);
        } else {
          depMap[key] = [eventName];
        }
      }
      
      // 修改数据时触发对应的事件
      function trigger(key) {
        let dep = depMap[key];
        if (dep) {
          dep.forEach(eventName => eventName());
        }
      }
      
      // 商品1
      let product1 = {
        price: 100,
        num: 2,
      };
      
      // 商品2
      let product2 = {
        price: 1000,
        num: 3,
      };
      
      let totalPrice = 0;
      let product1SalePrice = 0;
      let product2SalePrice = 0;
      
      // 获取总价
      function getTotalPrice() {
        totalPrice = product1.price * product1.num + product2.price * product2.num;
      }
      
      // 获取 商品1 优惠价
      function getProduct1SalePrice() {
        product1SalePrice = product1.price * 0.8;
      }
      
      // 获取 商品2 优惠价
      function getProduct2SalePrice() {
        product2SalePrice = product2.price * 0.8;
      }
      
      // 手动记录关系
      track('price', getTotalPrice);
      track('price', getProduct1SalePrice);
      track('price', getProduct2SalePrice);
      track('num', getTotalPrice);
      
      // console.log(depMap);
      // depMap = {
      //   price: ['getTotalPrice', 'getProduct1SalePrice', 'getProduct2SalePrice'],
      //   num: ['getTotalPrice'],
      // };
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
  • 推荐使用 WeakMap (opens new window) 数据类型记录将数据和函数的关系下来

    • 请问为什么要通过 WeakMap 数据类型而不是 Object

      // 数据与事件的关系记录表
      let depMap = new WeakMap();
      
      // 保存数据与事件关系
      function track(dataObj, key, eventName) {
        let dep = depMap.get(dataObj);
        if (!dep) {
          depMap.set(dataObj, (dep = {}));
        }
        if (dep[key]) {
          dep[key].push(eventName);
        } else {
          dep[key] = [eventName];
        }
      }
      
      // 修改数据时触发对应的事件
      function trigger(dataObj, key) {
        let dep = depMap.get(dataObj);
        if (dep) {
          dep[key].forEach(eventName => eventName());
        }
      }
      
      let product1 = {
        price: 100,
        num: 2,
      };
      
      let product2 = {
        price: 2000,
        num: 3,
      };
      
      let totalPrice = 0;
      let product1SalePrice = 0;
      let product2SalePrice = 0;
      
      // 获取总价
      function getTotalPrice() {
        totalPrice = product1.price * product1.num + product2.price * product2.num;
      }
      
      // 获取 商品1 优惠价
      function getProduct1SalePrice() {
        product1SalePrice = product1.price * 0.8;
      }
      
      // 获取 商品2 优惠价
      function getProduct2SalePrice() {
        product2SalePrice = product2.price * 0.8;
      }
      
      // 手动记录关系
      track(product1, 'price', getTotalPrice);
      track(product1, 'num', getTotalPrice);
      track(product2, 'price', getTotalPrice);
      track(product2, 'num', getTotalPrice);
      track(product1, 'price', getProduct1SalePrice);
      track(product2, 'price', getProduct2SalePrice);
      
      // 运算结果
      getTotalPrice();
      getProduct1SalePrice();
      getProduct2SalePrice();
      console.log('更新前的总价:' + totalPrice);
      console.log('更新前的 商品1 优惠价:' + product1SalePrice);
      console.log('更新前的 商品2 优惠价:' + product2SalePrice);
      
      // 手动更新 商品1 数据
      product1.price = 200;
      trigger(product1, 'price');
      console.log('更新后的总价:' + totalPrice);
      console.log('更新后的 商品1 优惠价:' + product1SalePrice);
      
      // 手动更新 商品2 数据
      product2.price = 3000;
      trigger(product2, 'price');
      console.log('更新后的总价:' + totalPrice);
      console.log('更新后的 商品2 优惠价:' + product2SalePrice);
      
      // console.log(depMap);
      // depMap = {
      //   product1: {
      //     price: [getTotalPrice, getProduct1SalePrice],
      //     num: [getTotalPrice],
      //   },
      //   product2: {
      //     price: [getTotalPrice, getProduct2SalePrice],
      //     num: [getTotalPrice],
      //   },
      // };
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92

# 自动响应式数据

  • Proxy (opens new window) 的基本使用

    let product = {
      price: 100,
    	num: 2,
    };
    
    let proxiedProduct = new Proxy(product, {
      get(target, key, receiver) {
      	return target[key];
      },
      set(target, key, newVal, receiver) {
      	return (target[key] = newVal);
      },
    });
    
    console.log(proxiedProduct.price);
    proxiedProduct.price = 200;
    console.log(proxiedProduct.price);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
  • Reflect (opens new window) 的基本使用 思考为什么要搭配 Reflect

    let product = {
      price: 100,
      num: 2,
    };
    
    let proxiedProduct = new Proxy(product, {
      get(target, key, receiver) {
        return Reflect.get(target, key, receiver);
      },
      set(target, key, newVal, receiver) {
        return Reflect.set(target, key, value, receiver);
      },
    });
    
    console.log(proxiedProduct.price);
    proxiedProduct.price = 200;
    console.log(proxiedProduct.price);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
  • 使用 ProxyReflect 进行数据拦截器的自动绑定 思考下面几个问题

  • 函数 reactive 的作用

  • 变量 activeEvent 的作用

  • 函数 runEvent 的作用

  • 为什么最终 depMap 会出现 null 的事件呢

  • 如果多次调用 runEvent(getTotalPrice) 获取总价格 那么会出现什么问题

    // 数据与事件的关系记录表
    let depMap = new WeakMap();
    
    // 当前运行的事件
    let activeEvent = null;
    
    // 保存数据与事件关系
    function track(dataObj, key, eventName) {
      let dep = depMap.get(dataObj);
      if (!dep) {
        depMap.set(dataObj, (dep = {}));
      }
      if (dep[key]) {
        dep[key].push(eventName);
      } else {
        dep[key] = [eventName];
      }
    }
    
    // 修改数据时触发对应的事件
    function trigger(dataObj, key) {
      let dep = depMap.get(dataObj);
      if (dep) {
        dep[key].forEach(eventName => eventName());
      }
    }
    
    // 设置数据的拦截器 traps
    function reactive(target) {
      return new Proxy(target, {
        get(target, key, receiver) {
          let result = Reflect.get(target, key, receiver);
          track(target, key, activeEvent);
          return result;
        },
        set(target, key, newVal, receiver) {
          if (target[key] != newVal) {
            Reflect.set(target, key, newVal, receiver);
            trigger(target, key);
          }
          return newVal;
        },
      });
    }
    
    // 保存当前运行的函数
    function runEvent(eventName) {
      activeEvent = eventName;
      activeEvent();
      activeEvent = null;
    }
    
    let product = {
      price: 100,
      num: 2,
    };
    
    let totalPrice = 0;
    let productSalePrice = 0;
    
    // 初始化商品的拦截器
    let proxiedProduct = reactive(product);
    
    // 获取总价
    function getTotalPrice() {
      totalPrice = proxiedProduct.price * proxiedProduct.num;
    }
    
    // 获取商品优惠价
    function getProductSalePrice() {
      productSalePrice = proxiedProduct.price * 0.8;
    }
    
    // 运算结果
    runEvent(getTotalPrice);
    runEvent(getProductSalePrice);
    console.log('更新前的总价:' + totalPrice);
    console.log('更新前的商品优惠价:' + productSalePrice);
    
    // 更新商品数据
    proxiedProduct.price = 200;
    console.log('更新后的总价:' + totalPrice);
    console.log('更新后的商品优惠价:' + productSalePrice);
    
    // console.log(depMap);
    // depMap = {
    //   product: {
    //     price: ['getTotalPrice', 'getProductSalePrice', null, null],
    //     num: ['getTotalPrice', null],
    //   },
    // };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
  • reactive 数据双向绑定 最终版

    • 设置 proxiedProduct.price = 200 时 会自动触发 pricegetTotalPricegetProductSalePrice 事件 而事件内部因为获取 proxiedProduct 的属性 所以又会触发拦截器里面的 get 又一次绑定目前的事件

    • 多次调用 runEvent(getTotalPrice) 获取总价格 会触发拦截器里面的 get 重复绑定 getTotalPrice 事件 所以 我们要使用 Set (opens new window) 自动过滤重复事件

    // 数据与事件的关系记录表
    let depMap = new WeakMap();
    
    // 当前运行的事件
    let activeEvent = null;
    
    // 保存数据与事件关系
    function track(dataObj, key, eventName) {
      let dep = depMap.get(dataObj);
      if (!dep) {
        depMap.set(dataObj, (dep = {}));
      }
      if (!dep[key]) {
        dep[key] = new Set();
      }
      dep[key].add(eventName);
    }
    
    // 修改数据时触发对应的事件
    function trigger(dataObj, key) {
      let dep = depMap.get(dataObj);
      if (dep) {
        dep[key].forEach(eventName => eventName());
      }
    }
    
    // 设置数据的拦截器 traps
    function reactive(target) {
      return new Proxy(target, {
        get(target, key, receiver) {
          if (activeEvent) {
            track(target, key, activeEvent);
          }
          return Reflect.get(target, key, receiver);
        },
        set(target, key, newVal, receiver) {
          if (target[key] != newVal) {
            Reflect.set(target, key, newVal, receiver);
            trigger(target, key);
          }
          return newVal;
        },
      });
    }
    
    // 保存当前运行的函数
    function runEvent(eventName) {
      activeEvent = eventName;
      activeEvent();
      activeEvent = null;
    }
    
    let product = {
      price: 100,
      num: 2,
    };
    
    let totalPrice = 0;
    let productSalePrice = 0;
    
    // 初始化商品的拦截器
    let proxiedProduct = reactive(product);
    
    // 获取总价
    function getTotalPrice() {
      totalPrice = proxiedProduct.price * proxiedProduct.num;
    }
    
    // 获取商品优惠价
    function getProductSalePrice() {
      productSalePrice = proxiedProduct.price * 0.8;
    }
    
    // 运算结果
    runEvent(getTotalPrice);
    runEvent(getTotalPrice);
    runEvent(getTotalPrice);
    runEvent(getProductSalePrice);
    console.log('更新前的总价:' + totalPrice);
    console.log('更新前的商品优惠价:' + productSalePrice);
    
    // 更新商品数据
    proxiedProduct.price = 200;
    console.log('更新后的总价:' + totalPrice);
    console.log('更新后的商品优惠价:' + productSalePrice);
    
    console.log(depMap);
    // depMap = {
    //   product: {
    //     price: ['getTotalPrice', 'getProductSalePrice'],
    //     num: ['getTotalPrice'],
    //   },
    // };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
  • Vue2 的响应式原理

    • 通过 Object.defineProperty() (opens new window) 来自动实现发布订阅

      let depMap = new WeakMap();
      let activeEvent = null;
      
      function track(dataObj, key, eventName) {
        let dep = depMap.get(dataObj);
        if (!dep) {
          depMap.set(dataObj, (dep = {}));
        }
        if (dep[key]) {
          dep[key].push(eventName);
        } else {
          dep[key] = [eventName];
        }
      }
      
      function trigger(dataObj, key) {
        let dep = depMap.get(dataObj);
        if (dep && dep[key]) {
          dep[key].forEach(itemEvent => {
            itemEvent();
          });
        }
      }
      
      function reactive(dataObj) {
        let newDataObj = {};
        newDataObj.originDataObj = Object.assign({}, dataObj);
        for (const key in dataObj) {
          Object.defineProperty(newDataObj, key, {
            get() {
              if (activeEvent) {
                track(this, key, activeEvent);
              }
              return this.originDataObj[key];
            },
            set(newVal) {
              this.originDataObj[key] = newVal;
              trigger(this, key);
            },
          });
        }
        return newDataObj;
      }
      
      function runEvent(eventName) {
        activeEvent = eventName;
        eventName();
        activeEvent = null;
      }
      
      let product = {
        price: 100,
        num: 2,
      };
      
      let definePropertyproduct = reactive(product);
      
      let productTotalPrice = 0;
      let productSalePrice = 0;
      
      function getproductTotalPrice() {
        productTotalPrice = definePropertyproduct.price * definePropertyproduct.num;
      }
      
      function getproductSalePrice() {
        productSalePrice = definePropertyproduct.price * 0.8;
      }
      
      runEvent(getproductTotalPrice);
      console.log('更新前的总价:' + productTotalPrice);
      
      definePropertyproduct.price = 200;
      console.log('更新后的总价:' + productTotalPrice);
      
      // console.log(depMap);
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75

# 参考文献