Vue监测数据变化(包括数组)的原理

1,515 阅读4分钟

概览

Vue实例化对象挂载到根元素后,生成全局Vue对象实例,Vue对象在实例化过程中会传入配置对象(options),options中包括data、methods、computed、watch等等。至于Vue是如何对data中数据的变化进行监测的,原理大致如下:

一、普通数据监测原理及步骤

1. data对象加工成_data对象,并存入Vue实例

Vue首先对options中的data对象进行加工,生成_data对象,并存入全局Vue实例中,_data对象并不是单纯对data对象进行简单复制:

  • 怎么加工:递归为每一个非对象类型的数据添加响应式的get、set方法(reactive getter、reactive setter)(数据劫持

    什么是非对象类型数据?

        data:{
            key1:value1,           //key1为非对象类型数据
            key2:{                 //key2为对象类型数据
                key3:value3,       //key3为非对象类型数据
                key4:{             //key4为对象类型数据
                    key5: value5,  //key5为非对象类型数据
                },
            }
        }
    

    reactive getter和reactive setter怎么添加?(会用到数据代理)

        let data = {
            key1:value1,
            key2:value2,
        }
    
        // 创建一个观察者实例对象,用于监测data的数据变化
        const obs = new Observer(data)
        
        function Observer(obj){
            // 汇总obj中所有属性,形成一个数组keys
            const keys = Object.keys(obj)
            // 遍历keys数组(此处为简化示例,未体现属性是一个对象或一个数组进行递归的情况)
            keys.forEach((key)=>{
                Object.defineProperty(this,key,{
                    get(){
                        return obj[key]
                    },
                    
                    set(value){
                        obj[key] = value
                    }
                })
            })
        }
        
        // 下面这个方式做数据代理是完全不可取的,在设置和读取data时,会形成无限递归,导致内存溢出,
        // 因此使用上面的Observer构造函数来实现
        //Object.defineProperty(data,'key1',{
        //    // 当有人读取data的key1属性时,getter就会被调用,返回data.key1
        //    get(){
        //        return data.key1
        //    }
        //    set(value){//当有人设置data的key属性值时,setter就会被调用,并把value设置给key1
        //        data.key1 = value
        //    }
        //})
    
  • 怎么存入vm._data = data = obs

2. Vue全局实例对象代理_data对象,实现对_data中属性的直接操作

加工生成_data对象,并存入Vue实例对象后,Vue实例对象会通过数据代理的方式,对_data对象进行代理,为每个对象设置getter和setter,从而实现通过this.key1 = new_value1 以及 const value = this.key1 的形式,直接对_data中的key1进行修改和读取。所谓数据代理,就是通过一个对象代理对另一个对象中属性的操作【读/写】,至于数据代理的实现形式,当然就是使用原生JS的Object.defineProperty()方法

关于Object.definProperty()的原理

         let student = {
             name:'tom',
         }

         // 给 student 对象添加一个age属性,通过defineProperty方法添加的属性,默认是不可枚举的,不参加对象的遍历(遍历对象时,不会遍历到这个对象),
         // 只有当设置了enumerable为true,才可以被遍历到。
         Object.defineProperty(student,'age',{
                value:18,
                enumerable:true, //控制属性是否可枚举,默认为false
                writable:true, //控制属性是否可以被修改,默认值是false
                configurable:true, //控制属性是否可以被删除,默认值是false
            })

3. 数据变化监测效果

每个具有reactive setter的数据发生变化时,都会调用这个reactive setter,而这个reactive setter被调用时,会触发重新解析模板、生成新的虚拟DOM,、新旧虚拟DOM对比,更新内容映射到真实DOM、重新渲染这一套流程。

二、 动态新增可被监测的属性 Vue.set()或this.$set()

项目开发过程中,可能会出现新增属性的需求,而新增的属性也必须是响应式的,因此需要用到 Vue.set(targetObject,attributeName,attributeValue)方法或this.$set(targetObject,attributeName,attributeValue)进行响应式属性的动态添加,所谓响应式属性也就是这个属性会被添加到_data对象中,并且具有相应的reactive getter和reactive setter,最后被存入到Vue对象实例里。

需要注意,由于Vue本身有规定,不能把一个响应式的属性直接添加到Vue实例身上,而data对象中的所有属性最终都会通过数据代理添加到Vue实例身上,因此Vue.set()方法和this.$set()方法的targetObject这个参数不能是Vue实例本身,也不能是data对象,只能是data对象中的二级属性对象。

三、 Vue监测数组数据的变化

Vue不会为数组元素添加响应式的getter和setter所以通过下标更改数组数据是无法被Vue所监测到的。 针对数组,只有通过调用pushpopshiftunshiftsplicereversesort这7个改变原数组本身的API,才会引起Vue的响应。