浅谈Vue数据响应式

259 阅读4分钟

vue-js.jpg

什么是响应式?

作为一名前端关于“响应式”我们听到的最多的应该是响应式网页,特点就是当浏览器的视口变化时其页面的显示效果也会跟着变化,这便是一个响应式网页。总结就是有一个东西变化时另一个东西也跟着变化

那么数据响应式顾名思义就是当数据变化时,页面也跟着变化。

数据响应式如何实现?

数据响应式首先要有数据,我们以我们的Vue参数中的data属性为例,我们需要做的就是无论data中的数据如何改变我们都可以监听到。

既然要监听对象属性的修改,很明显我们不能直接将对象的属性进行修改(那样无法监听到),我们需要使用一种代理方法来对对象上的属性进行修改。其原理就是使用一个对象的虚拟属性来访问与操作我们实际的对象

function proxy({data}){
            let obj = {};
            Object.defineProperty(obj,'n',{
                get(){
                    return data.n;
                },
                set(value){
                    if(value < 0) return; // 这里以限制数据不为负数为例,实际对数据有更多的限制
                    data.n = value;
                }
            })
            return obj;
        }
let obj2 = proxy({data:{n:0}});
        console.log(obj2.n);
        console.log(obj2.n = -1);
        console.log(obj2.n);
        console.log(obj2.n = 3);
        console.log(obj2.n);

这样我们就完成了在内部修改数据时对数据的监听那么问题来了,如果我们直接在外部修改数据,那么数据的变化就无法被监听到了。

	 function proxy({data}){
            let obj = {};
            Object.defineProperty(obj,'n',{
                get(){
                    return data.n;
                },
                set(value){
                    if(value < 0) return; // 对数据进行限制
                    data.n = value;
                }
            })
            return obj;
        }
        let tempObj = {
            n:0
        };
        tempObj.n = -1;
        let obj2 = proxy({data:tempObj});
        console.log(obj2.n); // -1

我们可以发现这样我们就绕过了代理,成功修改了数据且不被发现,很明显这不是我们想要的那么我们该如何做呢?

我们的解决思路是使用一个变量保存原始对象的属性,然后使用虚拟属性将之前的属性进行覆盖,这样当我们再在外部修改属性的时候,属性的变化也可以被监听了,这一部分称之为“监听”。

          function proxy({data}){
            // 监听
            let value = data.n;
            Object.defineProperty(data,'n',{
                get(){
                    return value;
                },
                set(newValue){
                    if(newValue < 0) return;
                    value = newValue;
                }
            })
            // 代理
            let obj = {};
            Object.defineProperty(obj,'n',{
                get(){
                    return data.n;
                },
                set(value){
                    if(value < 0) return; // 对数据进行限制
                    data.n = value;
                }
            })
            return obj;
        }
        let tempObj = {
            n:0
        };
        let obj2 = proxy({data:tempObj});
        console.log(obj2.n); // 0
        tempObj.n = -1;
        console.log(obj2.n); // 0


这样我们就简单的完成了,无论是在实例对象外部还是内部对对象进行修改我们都可以监听到数据的变化,监听到数据变化之后我们便可以使用render将数据重新渲染到界面上便好。

关于data的一些bug:


我们先来看几个例子:

<template>
  <div id="app">
    {{obj.a}}
    {{b}}
  </div>
</template>

<script>

export default {
  name: 'App',
  data(){
    return {
      obj:{
        a:1
      }
    }
  }
}
</script>

image.png


我们在data中定义了一个对象然后引用这个对象的属性,我们在界面中可以看到其被显示出来了,但是如果我们在template引用一个未定义过的属性控制台便会报错。
那么如果接下来我们将b改为obj.b会怎么样呢?

<template>
  <div id="app">
    {{obj.a}}
    {{obj.b}}
  </div>
</template>

<script>

export default {
  name: 'App',
  data(){
    return {
      obj:{
        a:1
      }
    }
  }
}
</script>

image.png


我们可以发现我们的界面即没有显示出来,控制台也没有报错,所以我们猜测Vue只能监听data第一层数据的改变。那么我们应该如何防止这个bug呢?

我们有两个选择,第一个是提前给obj添加一个undefined属性,第二个方法是使用Vue.set(this.obj,'b',value);这样也可以达到让Vue监听的目的。

<template>
  <div id="app">
    {{obj.a}}
    {{obj.b}}
    <button @click="setB">set b</button>
  </div>
</template>

<script>

export default {
  name: 'App',
  data(){
    return {
      obj:{
        a:1,
        b:undefined
      }
    }
  },
  methods:{
    setB(){
      // this.obj.b = 2;
      this.$set(this.obj,'b',2);
    }
  }
}
</script>


那么问题又来了,如果我们需要给数组添加一个元素呢?对于数组我们是无法适应Vue.set()方法的大家可以试一下。

我们知道修改数组元素的方法有pushpop等这些方法原生方法很明显是不可以被监听,我们发现在Vue中如果直接使用数组的push方法添加数据是可以完成响应式的。

<template>
  <div id="app">
    {{arr}}
    <button @click="push">push 4</button>
  </div>
</template>

<script>

export default {
  name: 'App',
  data(){
    return {
      arr:[1,2,3]
    }
  },
  methods:{
    push(){
      this.arr.push(4);
    }
  }
}
</script>

image.png
所以我们可以推测尤雨溪对数组的原生方法进行了改动。我们来打印一下这个数组:

image.png

由图可以得知尤雨溪给数组的原型链上添加了一层原型,原型上新增了push,pop等方法,来完成修改数组元素时对元素的监听。

那么这是怎么做到的呢?这里只表达一下大概的思路:

我们创建一个新的类然后让我们的类继承Array对象,我们在我们的创建的类中添加一个push实例方法,这个方法首先会调用原生数组的push方法,然后将push后的元素使用set让Vue进行监听便好了。

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

大概思路代码。

总结一下:


关于数据响应式我们优先想到的是data数据修改时界面的同步更新,那么我们便需要对data中数据的修改进行监听,监听的方法在内部主要使用“代理”,在外部的修改使用“监听”,核心就是使用defineProperty这个方法。之后我们说到了关于data的一些bug以及解决方法,在数组的方法中尤雨溪还对Vue中的push,pop等方法进行了再次封装。