Vue2数据响应式原理

143 阅读3分钟

一、Vue2数据响应式原理

通过数据劫持 defineProperty[dɪˈfaɪnˈprɑːpərti]+ 发布订阅者模式,当 vue 实例初始化后 observer[əbˈzɜːrvər]会针对实例中的 data 中的每一个属性进行劫持,并通过 defineProperty() 设置值后,在 get() 中向发布者添加该属性的订阅者,这里在编译模板时就会初始化每一属性的 watcher [ˈwɑːtʃər]在数据发生更新后调用 set 时会通知发布者 notify[ˈnoʊtɪfaɪ]通知对应的订阅者做出数据更新,同时将新的数据根性到视图上显示。

二、Object.defineProperty方法介绍

创建一个对象:var obj = { name: “sun” }

Object.defineProperty[dɪˈfaɪnˈprɑːpərti]这个方法接收三个参数
1、属性所在的对象(obj);
2、属性的名字(name、age);
3、一个描述符对象:描述符对象分为:数据属性访问器属性

2.1、数据属性:

①configurable[kənˈfɪgjərəbl]表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性,默认值为false。

Object.defineProperty(obj,"name",{
    configurable : false,
})
console.log(p1); //{ name: 'sun' }
delete p1.name;
console.log(p1); //{ name: 'sun' }

通过这个方法设置好configurable 这个属性,delete就不能把name属性给删除掉了

②enumerable:表示能否通过for in循环访问属性,默认值为false;

Object.defineProperty(obj,"age",{
    enumerable:false
})
for(var i in obj){
    console.log(obj[i]);
} // sun

通过这个方法给enumerable设置为false,这样对象就不能通过迭代器遍历出age这个属性的值

③writable:表示能否修改属性的值,默认值为false;

④value:包含这个属性的数据值,默认值为undefined。

Object.defineProperty(obj,"age",{
    writable :false,
    value : 15,
})
console.log(p1.age); //15
p1.age = 20;
console.log(p1.age); //15

给 obj 这个对象新加了一个age属性,并且设置成只读的。就无法修改这个age属性了。

2.2、访问器属性:

①get:get是读取属性,get不带参数,get必须用return返回;

②set:set是修改属性,set有且仅有一个参数,set不需要返回;

var book = {
    _year : 2004,
    edition : 1
}

Object.defineProperty(book,"year",{
    get: function(){
        return this._year
    },
    set: function(newYear){
        if(newYear > 2004){
            this._year = newYear;
            this.edition += newYear - 2004
        }
    }
})
book.year = 2005;
console.log(book.edition); // 2
console.log(book._year); //2005

由于get方法返回_year的值,set方法通过计算来确定正确的版本。  
因此把year的值设置为2005会导致edition的值变为2.

三、数据劫持

数据劫持指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。

比较典型的是基于ES5 Object.defineProperty() 和 ES2016 中新增的 Proxy对象。数据劫持最著名的应用当属双向绑定;例如 Vue 2.x 使用的是Object.defineProperty(),Vue 在 3.x 版本之后改用 Proxy 进行实现

Object.defineProperty()

通过 Object.defineProperty 遍历对象的每一个属性,把每一个属性变成一个 getter 和 setter 函数,读取属性的时候调用 getter,给属性赋值的时候就会调用 setter; 当运行 render 函数的时候,发现用到了响应式数据,这时候就会运行 getter 函数,然后 watcher(发布订阅)就会记录下来。当响应式数据发生变化的时候,就会调用 setter 函数,watcher 就会再记录下来这次的变化,然后通知 render 函数,数据发生了变化,然后就会重新运行 render 函数,重新生成虚拟 dom 树。

简单原理:利用 Object.defineProperty(),并且把内部解耦为 Observer, Dep, 并使用 Watcher 相连。

Object.defineProperty() 的问题主要有三个:

  • 1、不能监听数组的变化,导致通过数组添加元素,不能实时响应;(无法实时响应方法:push、pop、shift、unshift、splice、sort、reverse)
  • 2、object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。Proxy可以劫持整个对象,并返回一个新的对象。
  • 3、Proxy不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。

v-model 源码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
    />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>

    <style>
      #myInput {
        width: 400px;
        height: 50px;
        font-size: 40px;
        color: red;
      }
      #contain {
        margin-top: 20px;
        width: 400px;
        height: 200px;
        border: 1px solid salmon;
      }
    </style>
  </head>
  <body>
    <input id="myInput" type="text" />
    <div id="contain"></div>

    <script>
      var text;
      window.data = {};
      var oIn = document.getElementById("myInput");
      var oDiv = document.getElementById("contain");

      oIn.addEventListener("input", function (e) {
        text = e.target.value;
        console.log(text);
        window.data.value = text;
      });
      
      // Object.defineProperty 方法
      Object.defineProperty(window.data, "value", {
        get() {
          return "";
        },
        set(v) {
          oDiv.innerHTML = v;
        },
      });
    </script>
  </body>
</html>