理解Vue数据响应式

281 阅读4分钟

什么是响应式?

我上来给你一记军体拳,你说:“草,真疼!”那么你就是响应式的。

即如果一个物体对外界的刺激做出反应,那么它就是响应式的。

被别人锤一拳,完了一句话不说,什么反应都没有,那就不太正常了...

什么是响应式网页?

我们经常会听到响应式网页这个词,不了解的话会感觉很懵。其实就是当窗口大小发生改变时,比如你拖动、缩小或放大浏览器,或者先用电脑浏览器看一个网页,再用手机或平板看这个网页,这个网页会根据你屏幕大小来使用不同的内容分布方式,这就是响应式网页。

比如你用电脑看这个网页,因为电脑屏幕大,图片是横着放的,一排能整三四张图片。但是换到手机上,屏幕变小了,一排只能放一张图片了,多了看不到咋办?那就只能一张一张往下排了,这就是响应式网页。

响应式网页主要用到了CSS里的媒体查询。

什么是Vue数据响应式?

官方文档是这样说的:

Vue最独特的一个特性之一,是其非侵入性的响应式体统。

这样乍得一看会比较懵,直接上代码:

注:下面的所有代码都默认引入了Vue.js

//html:
<div id="app">
	{{n}}
</div>

//js:
new Vue({
	el:"#app",
	data:{
		n:0
	}
})

我们在data选项里写上n的值为0,页面div里的n的值会自动变成0,我们在页面上就能看到0;

当我们把data选项里的n的值改成1后,页面div里的n的值也会跟着变成1,这就是Vue的数据响应式;

我们在创建一个Vue的实例时,把一个对象传进去作为data选项,那么我们在修改对象里属性值的时候,页面里对应的值就会直接跟着改变。也就是说,我们在修改数据模型时,视图就会进行更新,这样就使得状态管理非常简单直接。

浅析Vue数据响应式

先看代码:

const myData = {a:1}
console.log(myData) //{a:1}

new Vue({data:myData})
console.log(myData) //得到的结果变成了{__ob__: we}

在Chrome控制台里打开得到的结果看看: 里面多了好多东西,除了原型_proto_,我们还可以看到 get 和 set ,后面跟着属性a,这就比较神奇,为什么会传给new Vue之后就会变了呢?

这是因为我们把普通的JS对象传入Vue实例作为data选项后,Vue会遍历这个对象里所有的属性(property),并且使用ES6中的Object.defineProperty方法来把这些属性全部转为getter和setter;

IE8和更低版本的浏览器不支持Object.defineProperty,所以这是Vue不支持这些浏览器的原因。

Object.defineProperty是做什么的?

可以给对象添加属性value

let data1 = {}
Object.defineProperty(data1,'n',{
	value:0
})
console.log(data1)

//想象中data1是这样的:
{
	n:{
    	value:0
    }
}

//然而实际上data1却是这样的:
{
	n:0
}

//所以console.log(data1.n)结果是
0

可以给对象添加getter和setter

let data2 = {}
Object.defineProperty(data2,n,{
	get(){},
    set(){}
})

这里的get属性里就可以写getter函数了,当我们用data2.n来访问data2的n属性时,就会调用这个函数,该函数的返回值会被用作这个属性的值。如果里面什么都不写,即没有getter函数,就返回undefined。

同样set属性里的就是setter函数,当我们修改属性值时,会调用这个函数。这个函数接受一个参数,这个参数就是被赋予这个属性的新值。如果什么都不写,就没有setter,就是undefined了。

那么根据这些来继续完善上面的代码:

let data2 = {}
data2.x = 0
Object.defineProperty(data2,n,{
	get(){
    	return this.x
    },
    set(value){
    	this.x = value
    }
})
console.log(data2)
console.log(data2.n)

上面代码的意思是:在data2对象里创建一个n属性,里面有一个get和set:

get负责:如果你访问n,那么就给你返回0

set负责:如果你想修改n,那么就得从我这走一圈~

这样就可以做到对属性的读写进行监控:

let data2 = {}
data2.x = 0
Object.defineProperty(data2,n,{
	get(){
    	return this.x
    },
    set(value){
    	if(value<0){
        	return
        }else{
        	this.x = value
        }  	
    }
})
data2.n = -1
console.log(data2.n) //0
data2.n = 1
console.log(data2.n) //1

这样可以监控到属性值的变化,但是有个问题:

我们访问data2对象的属性值的时候,实际上是调用getter函数,返回的是data2的x属性的属性值,然后设置的时候也是修改data2的x属性的属性值来达到修改n属性的目的。那我不用data2.n = -1来修改,我直接修改data.x的值不就能绕过setter了吗?

data2.x = -1
console.log(data2.n) //-1,说明不受setter的控制

因此,我们需要想办法来隐藏对象的属性,不让对象的属性暴露出来,以免被人随意修改。

这时就需要用到代理:

简单理解代理proxy

之前我们都是先声明对象,然后写一个对象的属性值来提前存储数据,比如:

let data2 = {}
data2.x = 0

现在我们使用匿名对象试试:

let data3 = proxy({data:{n:0}})

proxy({data})是一个函数,真正的对象是{n:0},是一个匿名对象,匿名对象就不好访问到了。因为修改一个对象的属性,你至少要知道对象的名字吧,连对象的名字都不知道,还怎么修改里面的属性的值?

然后简单的实现以下proxy函数:

function proxy({data}){
	let obj = {}
    Object.defineProperty(obj,'n',{
    	get(){
        	return data.n
        },
        set(value){
        	if(value<0){
            	return
            }else{
            	data.n = value
            }  
        }
    })
    return obj
}

当你读取obj的n时,obj就返回data的n,当你修改obj的n的时候,obj就会同步设置data的n;

即你对obj的n属性做什么,就会同时对data的n属性做一样的操作,最后返回obj,这个obj就是一个代理,相当于你对data的操作的所有过程都被obj中转了一次。

这样如果别人还想通过上面例子中,修改data2.x的值来达到绕过setter的目的的话,会发现不知道从哪儿下手,因为proxy里面内容是没有暴露出去的。要用data.n就必须经过setter,不用的话又不知道该用什么对象的什么属性来绕开。

更夸张的情况

上面是使用了匿名对象,然后中间代理了一层,别人不知道里面到底是什么对象在起作用。但是如果我不用匿名函数,我先声明一个函数,然后把这个函数引用到proxy里,不是一样可以达到效果吗:

let myData = {n:0}
let data4 = proxy({data:myData})
myDatafmy.n = -1
console.log(data4.n)//-1

这样就成功了绕开了代理,达到了修改的效果。

那么如何做到,就算用户修改了myData,我们也能拦截呢?

let myData2 = {n:0}
let data5 = proxy2({data:myData2})

function proxy2({data}){
	let value = data.n //用value来存储原始的data.n的值
    delete data.n //把原始的data.n删除
    Object.defineProperty(data,'n',{ //然后创建一个n属性,放在data上
    	get(){
        	return value //访问data.n时,返回value
        },
        set(newValue){
        	if(newValue<0)return
            value = newValue //如果修改data.n的值,就把先看这个值是否符合要求,符合就赋给value
        }
    })
    //这样就能达到监听的效果
    
    const obj = {}
    Object.defineProperty(obj,'n',{
    	get(){
        	return data.n
        },
        set(value){
        	if(value<0)return
            data.n = value
        }
    })
    return obj
}

console.log(data5.n) //0
myData2.n = -1
console.log(data5.n) //还是0,修改失败

总结

当我们这样写时:

let vm = new Vue({data:myData})

会有这样的过程:

  1. vm会变成myData的代理(proxy),对vm取n,就等于对myData取n,修改vm的n属性的值,就等于修改myData的n属性的值。
  2. vm会对myData的所有属性进行监控

为什么要进行监控呢?因为防止myData属性变了,vm不知道。vm知道了又怎么样呢?vm感知到myData对象的属性变化后,就可以调用render()去更新视图层。

回头看Vue数据响应式

Vue数据响应式主要就是用了Object.defineProperty方法,对传入的对象属性进行监控,这样对象属性值在变化时Vue就可以更新视图。

摘抄官方的文档:

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。

这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。