Vue数据代理和监听

917 阅读8分钟

数据代理

如果使用Object.defineProperty()定义属性,打控制台看见的属性就不会直接显示出来,而是以图片上面的这种形式。因此,从这里看出age和name是使用了代理。

attention!!!

当我们在模板里面使用Vue对象的data对象里面的属性时,Vue经过了一系列的步骤。为什么我们能直接使用data里面的属性,而不需要使用data.prop呢?

原理是:Vue实例上的data在Vue中是无法直接访问的,而是将data对象赋值给了_data,因此我们可以直接在Vue实例上操作_data。然后,使用数据代理Object.defineProperty()把_data对象里面的所有属性添加到Vue实例上,并为他们增加get和set方法。

这里有一个坑:在平时我们对象上定义属性上,在get和set方法中如果是又使用了该属性,就会造成死循环,导致栈溢出。

Object.defineProperty(obj, "name", {
    get () {
        return obj.name
    }
})

obj.name;
RangeError: Maximum call stack size exceede
//     at Object.get [as name] (F:\code\test\1.js:2662:9)
//     at Object.get [as name] (F:\code\test\1.js:2662:20)
//     at Object.get [as name] (F:\code\test\1.js:2662:20)
//     at Object.get [as name] (F:\code\test\1.js:2662:20)
//     at Object.get [as name] (F:\code\test\1.js:2662:20)
//     at Object.get [as name] (F:\code\test\1.js:2662:20)
//     at Object.get [as name] (F:\code\test\1.js:2662:20)
//     at Object.get [as name] (F:\code\test\1.js:2662:20)
//     at Object.get [as name] (F:\code\test\1.js:2662:20)
//     at Object.get [as name] (F:\code\test\1.js:2662:20)

因此,我们可以使用构造函数,将属性定义在实例上面。

function Observe(obj) {
    Object.keys(obj).forEach((key) => {
        Object.defineProperty(this, key, {
            get() {
                return obj[key];
            },
            set(newVal) {
                obj[key] = newVal;
            }
        })
    })
}

这样我们就可以直接在Vue实例上面使用data里面的属性啦~~这样明显可以看出使用数据代理的好处吧,就是在使用data里面的属性时,不需要写很长的一串对象调用(Vue._data.name),而是直接使用Vue实例上定义的属性即可(this.name)。

经过了数据代理以后,只要我们修改Vue实例上的属性时,都会将这个改动映射到_data中。

在这里,我们总结以下数据代理:

1、Vue中的数据代理: 通过Vue实例对象来代理data中属性的操作(读/写)

2、Vue中数据代理的好处: 更加方便的操作data中的数据

3、基本原理: 通过Object.defineProperty()把data上的所有属性添加到Vue实例上 为每一个添加到Vue实例的属性,都指定一个get和set方法 在get和set内部去操作data中对象的属性

数据劫持

其实,在进行数据代理之前,还有一个关键的步骤,就是对_data进行加工。一开始将data赋值给_data的时候就有点疑惑,为什么要有这一步呢?这样做会不会显得有点多余,为何不直接把data拿去做数据代理呢?下面就来探究一下。


试想一下,当我们进行数据代理了以后,一旦我们修改Vue实例上的属性,经过代理,_data里面的属性值也会相应改变。我们希望的效果是:当修改了Vue实例上的属性,页面元素也会发生变化,这里就是答案了。在这里进行了一步数据劫持。当_data里面的数据发生变化时,dom元素也会相应改变,这就需要我们监视_data中属性值的变化啦,我们需要为_data中的所有属性添加get和set方法,一旦发现有变化,就会立即重新编译模板,更新dom元素。

在这里我们需要注意,文章里面说的将data赋值给_data,这里就不是简单的赋值而已,而是通过数据劫持,把data中所有的属性定义在_data中,并为所有的属性添加get和set方法。这样,_data中的数据发生改变时,data里面的数据也会同时改变,我们知道,一旦data里面的属性值发生改变,就会当前的模板重新编译,更新dom元素。这里是一个单向流的操作。

还有需要注意的一点是:一般的我们的data对象都不简单的只有单层结构的属性,里面的数据结构可能会很复杂。先考虑对象的话,data对象里面的属性值可能又是个对象,因此我们在Observe构造函数中,要使用递归,给每个属性值都添加get和set方法。


下面定义一个data对象,

  data () {
    return {
      num: 1,
      student: {
        name: "aa", 
        age: 19,
        address: "成都"
      }
    }
  }

监听对象

接下来有一个需求,在代码里面我们想向student对象里面添加一个sex属性,并且这个数据要是响应式的。

从上面的截图,可以看出,这个sex属性确实被添加到Vue实例上了,但是并没有为它定义get和set方法,所有肯定不会是响应式的。

读取一个对象中不存在的值返回undefined,不会报错。在Vue中,如果值是undefined,该值是不会显示到页面中的。

这里就要用到Vue的API里面的set方法了。

使用Vue.set以后,就可以发现sex属性添加了get和set方法

在这里,Vue.set ()= this.$set()

但是,set有一个缺陷,就是它的一个参数不能是Vue实例或者Vue实例的根数据对象。

监听数组

接下来, 我们在student对象中添加一个数组,

 data () {
    return {
      num: 1,
      student: {
        name: "aa", 
        age: 19,
        address: "成都",
        hobby: ["aa", "bb", "cc"]
      }
    }
  }

明显可以看出,Vue并没有对数组进行数据代理,对数组的某一个元素进行修改,不会实现响应式。

那数组是怎么实现响应式的呢?

Vue将数组被监听的变更方法进行了包裹,只要操作数组时,调用的是这些方法,就会触发视图更新。这些变更方法是:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

变更方法,顾名思义就是会改变调用了这些方法的原始数组,即会改变原数组的方法。

如果调用非变更方法,可以把改变了的数组赋值给原始数组。

在这里还有一个方法可以实现数组的响应式,就是之前提到的Vue.set()

Vue.set(vm._data_student_hobby, 4, "dd"),这样可以实现动态的在数组里面新增一个元素。

关于Vue监视数据,最后做一个总结:

1、Vue能够监视深层次的数据;

2、如何监视对象中的数据:

通过get和set并且要在new Vue时传入要监视的数据

(1)对象后面添加的属性,Vue默认不做响应式

(2)如果需要给后面添加的属性做响应式,就要使用API:

Vue.set(target, prop, value)或 this.$set(target, prop, value)

3、如何监视数组中的数据:

通过包裹数组更新元素的方法,来触发视图更新,本质上就是两步:

(1)调用数组的原生方法,对原数组进行更新;

(2)重新编译模板,视图更新。

4、Vue中修改数组的某个元素一定要用以下方法:

(1)push、pop、shift、unshift、splice、sort、reverse

(2)Vue.set()或者this.$set()

特别注意,Vue.set不能给Vue实例或者Vue实例的根数据元素添加属性!!!

数据劫持就是将data里面原来的所有属性,都佩戴上了get和set方法。

理解数据劫持和数据代理的点是:数据劫持监测到了数据变化,get和set就会将这个操作拦下来,做两件事:

1、先去正常改数据

2、然后触发视图更新

而数据代理只有改变数据,触发视图更新的操作是传递给了数据劫持来做。

Vue2.x的响应式

实现原理:

对象类型:通过Object.defineProperty()对属性的读取、修改进行拦截;

数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)

存在问题:

新增(Vue.set())、删除属性(Vue.delete()),界面不会更新。

直接通过下标修改数组(Vue.set()/原生数组方法),界面不会自动更新

Vue2.x中,要想删除对象上的属性,使用delete,需要在定义属性的时候配置configurable:true

这样就可以删除了,但是这个操作不是响应式的,因为我们在定义属性的时候只给它配置了get和set方法,删除属性时,是不会触发set方法导致视图更新的。如下所示:

 let data = {

            num: 1,
            student: {
                name: "aa",
                age: 19,
                address: "成都"
            }
        }

        function Observe(obj) {
            Object.keys(obj).forEach((key) => {
                // 数据代理,目的是将_data上的属性添加到vm对象上
                Object.defineProperty(this, key, {
                    configurable: true,
                    get() {
                        return obj[key];
                    },
                    set(newVal) {
                        console.log("视图更新啦~~");
                        obj[key] = newVal;
                    }
                })
            })
        }

        let obs = new Observe(data);
        let vm = {};
        vm._data = obs;
        vm._data = data = obs;

        delete data.num;
        console.log(obs);

在上面这段代码中,确实num属性被删除了,但是set方法没有被触发,页面不会更新。

Vue3.0的响应式原理

(Vue2.x的以上问题Proxy都能够规避)

实现原理:

通过Proxy代理:拦截对象中任意属性的变化。包括对属性的增删改查

通过Reflect反射:对被代理的对象的属性进行操作。

设计思路:

一般对一个给定对象,我们要获取对象里面的属性,用obj.xxx就能获取。但是ECMA这个脚本语言,有意将Object上面的一些方法转移到Reflect上,在Vue的响应式实现中,就采取的Reflect来对对象进行操作。

Reflect的一个好处是:

比如,当使用Object.defineProperty()重复定义一个属性时,控制台会报错,js作为单线程语言,就会停止执行后面的代码。因此,为了避免这种现象,提高代码的健壮性(不因为出现错误了就停止当前代码的执行)就需要用到try/catch结构来规避这种错误,让代码能够继续执行下去。这样会造成一个结果就会最终我们的代码里面会有很多try/catch结构。因此,就推荐使用Reflect

Reflect对象上定义了很多方法,它修改了Object对象某些方法的返回结果,比如defineProperty,在Reflect对象上调用defineProperty方法时,会有一个返回值,当在对象上重复定义一个属性时,会返回一个布尔值,表示当前的操作是否成功。如果重复定义了属性,那么该操作就返回false,并不会影响后续的操作。

let obj = {
    name: "aaa",
    age: 18
}

Object.defineProperty(obj, "sex", {
    get() {
        return "男"
    }
})

// Uncaught TypeError: Cannot redefine property: sex
Object.defineProperty(obj, "sex", {
    get() {
        return "女"
    }
})

console.log("执行到这里啦~~"); // 不执行

const x = Reflect.defineProperty(obj, "sex", {
    get() {
        return "男";
    }
})

console.log(x); // true

const y = Reflect.defineProperty(obj, "sex", {
    get() {
        return "男";
    }
})

console.log(y); // false

console.log("执行到这里啦~~"); // 执行到这里啦~~