Vue的数据响应式

197 阅读5分钟

Vue的数据响应式

知识铺垫--ES6 getter/setter & Object.defineProperty

​ 在JS对象中,属性的的全称其实是属性描述符,而属性描述符又分为数据描述符存取描符,我们常用的就是数据描述符,如a:'b'。而getter/setter其实就是存取描述符。虽然这两种描述符名称看起来有点区别,但归根结底它们都是属性,用法大同小异,但是在声明上有点区别。

​ 参考文章:ES5 数据属性描述符和存取描述符

getter/setter

  1. getter
    • 语法:const obj = { get propertyName(){return ...} }
    • 使用:obj.propertyName就能获取到 propertyName 的返回值作为属性的值
  2. setter
    • 语法:const obj = { set propertyName(value){函数体} }
    • 使用:setter接收一个参数value,我们可以通过这个value设置属性的值,也可以做其他的事情。我们这样传递这个value:obj.propertyName = value,这里是使用=传值,注意区分一般的函数调用和setter的使用。
  3. 总结:
    1. getter/setter声明一个属性,在声明阶段和普通的数据描述符的声明方法不同,它们使用的是一个函数进行声明。
    2. 但是在使用的时候它们和普通的属性又是一样的,无论是获取属性的值,还是设置属性的值,当然他们还有更多的好处,如:我们可以在函数中debug,使用setter我们不仅可以设置属性,还能做别的事情(如value的检测),因为这本质上就是一个简单的函数调用。
    3. getter/setter可以成对出现,也可能只有一个(一般是只有getter)。
    4. 使用getter/setter时,一般需要一个数据的载体,或者使用代理的方法。

Object.defineProperty

  1. 顾名思义,这个API就是用于定义对象的属性的。

  2. 语法:Object.defineProperty(obj, prop, descriptor) descriptor是一个对象,具体用法看MDN。 MDN.

  1. 前面我们说了getter/setter,那么我们要怎么在一个已经声明好的对象中添加新的getter/setter呢?这就需要使用到Object.defineProperty这个方法了。

    let obj = {
        firstName : 'David',
        lastName : 'Gilmour'
    }
    let _xxx = null // 这是getter/setter数据的载体
    Object.defineProperty(obj,'n',{
        get(){
            return _xxx
        },
        set(value) {
            _xxx = value
        }
    })
    console.log(obj.n) // null
    obj.n = 'helloWorld'
    console.log(obj.n) // helloWord
    

Vue的data对我们传入的对象做了什么?

  1. 首先我们创建一个Vue实例,传入对象作为data,并打印这个对象
const myData = {
    firstName : 'David',
    lastName : 'Gilmour'
}
const vm = new Vue({
    data:myData
})
console.log(vm.data)

我们发现,打印出来的对象和我们一开始声明的对象不一样了。我们使用的是普通的数据描述符声明的属性,但是打印出来发现,myData中的属性都被转换成了存储描述符。为什么要这么做呢?为了监听数据!

  1. 根据我们使用Vue的经验我们知道,Vue可以监听我们的数据,当数据发生改变的时候执行适当的函数,那要怎么做呢?我们通过一个简单的例子来解释。

    let myData = {n:0}
    let data = proxy({data:myData}) 
    function proxy({data}){ // 解构赋值
        // 第一步,把我们传入的myData对象中的数据描述符全部改写成存储描述符
        // 正常来说这里需要遍历key来实现,这里简化了,默认了n的存在
        let value = data.n 
        Object.defineProperty(data,'n',{
            get(){
                return value
            },
            set(newValue){
                // 数据变化时这里也可以有其他的响应
                if(newValue<0)return
                value = newValue
            }
        })
        // 第一步完成后,外面的myData就已经改变了,之后我们即使直接操作myData也是通过setter进行,数据的变化也能够被监听到。
        
        // 第二步,使用代理,这里的obj就是一个代理,表面上我们操作obj,实际上我们操作data
        const obj = {}
        // 这里在实际使用中也是需要遍历
        Object.defineProperty(obj,'n',{
            get(){
                return data.n
            },
            set(value){
                data.n = value
            }
        })
        return obj // 返回obj
    }
    console.log(`${data5.n}`)
    myData5.n = -1;
    console.log(`${data5.n}`); // 0 
    myData5.n = 1;
    console.log(`${data5.n}`); // 1
    
  2. 上面的例子中演示了一个简单的监听代理的过程,let data = proxy({data:myData})这行代码是不是看着很眼熟?let vm = new Vue({data:myData}),这就是Vue数据响应的简单的实现思想。

Vue的data中2个小细节

  1. 使用未被提前声明的数据

    let vm = new Vue({
        data : {
            obj:{
                a:'is a'
            }
        },
        template:
        `
    		<button @click='setB'>click</button>
    		<div>{{n}}</div>
    		<div>{{obj.b}}</div>
    	`,
        methods:{
            setB(){	
                this.obj.b = 'is b'
            }
        }
    })
    

    前面我们知道了Vue是怎么处理传入的data,在上面的例子中,我们使用了2个没有事先声明的属性,n和obj.b,在控制台中我们看到Vue报了警告。这个警告Vue只会检测第一层属性,即:如果属性是一个对象如:data{obj{...}},那么如果们使用obj.b这个没有声明的属性,Vue是不会报警告的,因为它检测到了obj确实存在。

在页面中,我们只能看到一个click按钮,因为,没有被提前声明的属性,Vue会默认他们都是undefined,而Vue是不会渲染值为undefined的数据的。

当我们点击click时,页面上也没有出现'is b'的字样。因为Vue一开始就没有监听obj.b这个属性,所以无法对数据的变化做出响应。

解决办法:

  1. 提前声明,如果有一个属性a,我们现在不使用,但是未来有可能会用到,那么我们可以给这个a赋值为undefined或null,总之就是要让Vue在初始化data的时候监听到这个属性。

  2. 使用Vue.setthis.$set()这两个是同一个函数,,语法:Vue.set( target, propertyName/index, value )如:Vue.set(obj,'b',1),注意第一个参数target可以是对象也可以是数组,但是不能是 Vue 实例,或者 Vue 实例的根数据对象,所以如果不是对象下的属性(如:obj.b)还是要提前声明。这个方法可以给新增的属性添加代理,后续对这个新属性的操作就直接正常操作即可,不需要再用这个函数。

  3. Vue的data中的数组问题

    1. 前面我们了解到了,如果一开始不声明,后面再声明的属性是无法使用的。那么对于数组呢?我们发现新增的数组的项也无法渲染到页面中。但是我们经常需要对数组进行push等操作,这个怎么办呢?上面说了我们可以使用this.$set这个方法,但是这也太麻烦了,为了解决这个问题,尤雨溪在原先Array的原型上又增加了一层原型,这个原型对象中封装了 push等常用的数组操作(在原来这些方法的基础上添加一些其他的功能),这个方法尤雨溪在文档中称之为变异方法

      原型链Array下还有一层Array,上面一层是尤雨溪扩展的Array。

  1. 如果要修改数组中已有的内容呢?

    参考文章:同步DOM结构--廖雪峰

    {
        data:{
            array:['a','b',{
                name:'David',
                age:18
            }]
        }
    }
    
    // 1.使用Vue.set
    Vue.set(vm.array,'1','d')  // ['a','d',{...}]
    Vue.set(vm.array,'2',"{name:'Ben',age:18}") // ['a','d',{name:'Ben'...}]
    // 2.如果数组中的元素是非简单元素我们也可以直接修改这非简单元素的内容
    vm.array[2].name = 'Ben'
    // 3.使用变异方法如splice()替换数组内容
    const newElement = {...}
    vm.array.splice(index,1,newElement)
    // 4.错误!错误!错误!Vue无法监听数组中元素的直接赋值。
    vm.arr[0] = 'f'// ['f','b',{...}] 数据上是改变了,但是视图层并不会更新。
    

    总结:如果涉及到修改栈内存上的数据时使用Vue.set。切忌直接:arr[0]=...,因为Vue无法监听数组中元素的直接赋值,可以改变数据,但不会触发视图的重新渲染。

  2. 变异方法的实现思想探索

    注:以下内容非Vue真实实现

    ES6

    class VueArray extends Array {
        push(...args) {
            const oldLength = this.length // this
            super.push(...args)
            console.log(' push ')
            for (let i = oldLength; i < this.length; i++) {
                Vue.set(this, i, this[i])
                // key Vue
            }
        }
    }
    

    ES5

    const vueArrayPrototype = {
        push: function () {
            console.log(' push ')
            return Array.prototype.push.apply(this, arguments)
        }
    }
    vueArrayPrototype.__proto__ = Array.prototype
    const array = Object.create(vueArrayPrototype)
    array.push(1)