携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情
Vue 响应式
说句实话,在我刚刚开始接触Vue的时候,我觉得他很神奇,它是我第一个接触的框架。它神奇在什么地方呢?第一,比如说他不需要通过getElementById来获取节点,就能直接操作DOM,第二,也不需要手动刷新页面就能实现数据更新,还有组件化开发,可以让一些重复的代码反复利用通过父子组件传值来改变内容显示。
说到这,我一直都在思考它到底是怎么样实现的一个响应式的操作呢😵?我开始查阅资料,在小破站和掘金、CSDN等论坛上面搜索学习,然后总结了一点点自我心得,简单的记录并分享一下😚
ok 回归正题:
数据代理
何为数据代理
首先在讲解Vue数据代理的原理之前,我们应该知道什么是数据代理;数据代理的定义是:一个对象操作(读写)另一个对象中的属性和方法。 定义比较抽象我们可能难以理解,下面我将结合实例向大家讲述什么是数据代理,实例代码如下:
//定义两个对象
let obj1 = {x:100};
let obj2 = {y:200};
//通过Object.defineProperty实现数据代理
//选择obj2 属性名为x的属性 进行设置
Object.defineProperty(obj2,"x",{
//有人访问obj2中的x时,返回obj1中的x
get() {
console.log("有人访问了obj2中的x");
return obj1.x
},
//有人修改obj2中的x时,修改obj1中的x
set(value) {
console.log("有人修改了obj2中的x,值为",value);
obj1.x = value
}
})
从以上代码上可以看出实现数据代理的核心是使用Object.defineProperty()方法,当访问obj2上的x属性时,就会调用get方法读取obj1对象的x属性,当我们修改了obj2上x属性时,就会调用set方法,并且传入修改的值value值,将obj2的x属性改为value。以上我们相当与通过obj2对象操作了obj1的x属性,这就是定义中所说的一个对象代理了另一个对象中的属性的操作
Vue中的数据代理
相信大家通过上面的代码应该对于数据代理这个概念有了一点点的简单的认识,下面我将结合下面实例带大家来看看Vue中的数据代理;代码示例如下
<body>
<!-- 1. Vue中的数据代理:通过vm对象来代理data对象中属性的操作(读/写)
2. Vue中数据代理的好处:更加方便的操作data中的数据
3. 原理:
1. 通过Object.defineProperty()把data对象中所有属性添加到vm上
2. 为每个添加的属性,指定一个getter/setter
3. 在getter/setter内部去操作(读/写)data对应的属性 -->
<div id="app">
<!-- 容器 -->
<h2>姓名:{{name}}</h2>
<h2>性别:{{sex}}</h2>
<h2>年龄:{{age}}</h2>
</div>
<script>
let data = {
name:'老六',
sex:'男',
age:18
}
//Vue实例
var vm = new Vue({
el: '#app',
// data:data
data
})
</script>
</body>
相信大家对于上面的这个代码应该都能熟悉,这只是一个简单的Vue实例,我们在Vue实例的外面声明了一个data对象,然后在data中通过ES6中对象的增强写法给data赋值。而从数据代理的定义上说是一个对象代理了另一个对象的读写操作,因此我们首先要找到Vue中的两个对象,其次是哪个对象代理了哪个对象的操作;如下图所示我们在new Vue()时产生了一个vm实例对象,传入的参数内部又有一个data对象;
那么上面两个对象分别是谁代理谁呢?我们通过控制面板来进行一个演示,我们通过修改实例对象vm身上的属性,来查看data里面的属性是否有变化
我们可以清楚的发现,vm中的data属性发现了改变,这表示我们修改成功了,那我们在打印一下Vue实例外面的data对象的值是否改变?
我们可以清楚的发现data中的数据发现了改变,但是我们操作的确实VM实例中的data呀,根本没有去操作外部变量data这个对象,这表示vm实例对象代理了data对象,即我们通过修改vm实例对象上的属性,实现了修改data对象的属性,即实现了数据代理,这就是Vue中的数据代理。
而实现数据代理就需要用到了Object.defineProperty(obj,name,desc),这个方法是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性
- 一共接受三个参数
- 参数1 -> obj -> 需要定义属性的当前对象
- 参数2 -> name -> 需要定义的属性名
- 参数3 -> desc -> 描述符 一个Object对象 用于更精准的控制对象属性
Object.defineProperty(person,'age',{
value:18, // 属性值
enumerable:true, //控制属性是否可以枚举,默认值是false
writable:true, //控制属性是否可以被修改,默认值是false
configurable:true //控制属性是否可以被删除,默认值是false
})
- 而实现数据代理主要是依靠
desc中的get和set方法- 当有人读取选择的这个属性时,
get()会被调用,返回值为当前的值 - 当有人修改选择的这个属性时,
set()会被调用,返回值为修改的值
- 当有人读取选择的这个属性时,
let number = 18
let person = {
name:'张三',
sex:'男',
}
Object.defineProperty(person,'age',{
//当有人读取person的age属性时,get函数(getter)就会被调用,且返回值就是age的值
get(){
console.log('有人读取age属性了')
return number
},
//当有人修改person的age属性时,set函数(setter)就会被调用,且会收到修改的具体值
set(value){
console.log('有人修改了age属性,且值是',value)
number = value
}
})
// console.log(Object.keys(person))
console.log(person)
而Vue实现响应式的效果就是使用的Object.defineProperty,当然Vue对这个方法进行的封装和添加了其他的操作,大家要是有兴趣的话可以去看一下Vue的官方文档
数据劫持
上面关于数据代理的内容我们就简单的学习完啦,接下来我们来了解一下Vue里面其他的一个响应式核心那就是数据劫持
何为数据劫持
给数据添加监听,一旦数据发生变化,就执行视图的修改操作,这个过程就是数据劫持
//一段简单的代码
<template>
<div>
<input v-model="message">
<p>{{ message }}</p>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello World'
};
}
};
</script>
Vue中的数据劫持
那么vue中的数据劫持究竟是怎么实现的呢?
其实Vue还是通过Object.defineProperty这个方法,这个方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
而在vue2.x中,要想实现data中所有属性都实现数据劫持,就要先遍历data中的所有属性,对每一个属性都使用Object.defineProperty,当属性的值发生变化时,就执行视图渲染操作。
请看以下代码:
<!-- 一个简单的数据挟持案例,未做其他操作 -->
<body>
<script>
let obj = {
name:'张三',
message:'你好啊',
age:18
}
//创建一个监视实例对象,用于监听obj中的属性发生变化
const obs = new Observer(obj);
//准备一个Vue实例对象
let vm = {}
vm._data = obj = obs
//构造函数
function Observer(obj){
//汇总对象中所有的属性名形成一个数组
const keys = Object.keys(obj)
// console.log(keys);
keys.forEach((item)=>{
console.log(item);
//this指向Observer这个函数的实例对象
console.log(this);
//往Observer的实例对象上加一个方法,根据item来选择元素
Object.defineProperty(this,item,{
get(){
//当有人读取时,把obj这个数组中item这个属性返回回去
console.log(`${item}被读取了`);
return obj[item]
},
set(val){
//当有人修改时,把obj这个数组中item这个属性修改成他修改的值
console.log(`${item}被修改了`);
obj[item] = val
}
})
})
}
</script>
</body>
上述代码描述的是一个简单的数据挟持案例,先定义了一个obj对象,然后创建了一个观察者这样的实例,用于监听obj中属性发生变化,然后我们准备一个实例对象(这里就准备一个vm实例),然后我们通过这个的实例创建一个函数,通过Object.keys把所有的属性名形成一个数组,然后通过Array.forEach遍历里面的每一项,方便给每一项都添加上一个Object.defineProperty方法,这样就一个简单的数据劫持案例就完成啦
当我们打印vm这个实例对象的时候,我们可以清楚的发现,他里面的每一个属性都有一个属于自己的get和set,我们打印这三个对象,然后修改vm上面的name属性
我们可以发现三个对象里面的name属性都发生了改变,而当我们去读取name属性时,set触发了,这就是一个简单的数据劫持,而Vue里面也是进行了这样的一个数据劫持操作,只是Vue的更加完善,并且做了其他方法的封装和操作
在Vue实例
data中定义的数据会被Vue内部进行加工,使用vue._data进行一个数据代理,把key和value形成get和set写法,当你通过get读取value时达到响应式操作,当你修改value时触发set达到响应式重新解析模板
Vue里面对于数组也有进行响应式操作,但是只有可以影响原数组Vue才能监听到然后改变页面数组数据,如: push() pop() shift() unshift() splice() sort() reverse()
而Vue对于数组操作的原理:包装数组身上的常用的修改数组的方法实现检测
当通过push()方法添加一个属性,按照一般js是调用的Array.prototype.push这个方法,但是在Vue里面却不是调用这个方法,而是调用了一个Vue管理的包装函数,函数分二步,第一步调用Array.prototype.push这个方法改变数组,第二步解析模板生成虚拟DOM那一套流程
Vue.set
当然Vue本身还提供了一个能让我们做响应式操作的API:Vue.set(追加属性的元素,属性名,属性值)
向一个对象中添加数据,当数据改变时,页面也会随之变化
//向一个对象中添加数据,当数据改变时,页面也会随之变化
userInfo: [1,2,3,4]
// 全局
Vue.set(vm.userInfo,1,5)
// vue实例
vm.$set(vm.userInfo,2,10)
改变数组数据,页面跟着变化的三种方法
- 直接改变数组引用
- 用数组的变异方法
- 用set方法(包括vue的set和实例的set)
改变对象数据,页面跟着变化的三种方法
- 改变引用
- 直接改值
- 用set方法(包括vue的set和实例的set)
set方法,向一个对象中加数据,当数据发生变化,页面变化
对象:
- 全局:Vue.set(obj,key,value)
- vue实例:vm.$set(obj,key,value)
数组:
- 全局:Vue.set(arr,index,value)
- vue实例:vm.$set(arr,index,value)
PS:
Vue.set()不允许对象是Vue实例或者Vue实例上面的根数据对象,只允许往响应式对象中添加一个property,并确保这个新的property同样是响应式的
Vue监听数据的原理
-
Vue会监视
data中所有层次的数据 -
那Vue是如何检测对象中的数据?
- 通过
setter实现监视,且要在new Vue时就传入要检测的数据 - 对象中后追加的属性,Vue默认不做响应式处理
- 如需给后添加的属性做响应式,请使用
Vue.set与vm.$set
- 通过
-
如何检测数组中的数据
- 通过包裹数组更新元素的方法实现,本质就是做了两件事
- 调用原生对应的方法对数组进行更新
- 重新解析模板,进而更新页面
- 通过包裹数组更新元素的方法实现,本质就是做了两件事
Watcher
它是一个观察者的实例;创建实例的时候会把实例添加到dep中,简单的来说就是Watcher能够控制自己属于哪个,是data中的属性的还是watch,或者是computed,Watcher自己有统一的更新入口,只要你通知它,就会执行对应的更新方法。而dep实现的是收集依赖和通知更新。(大家简单的了解一下)
总结
核心说的数据劫持,其实就是通过Object.defineProperty中的descriptor的存取描述符进行数据的劫持。
get事件在属性没有变化时触发并且还会触发dep收集依赖,set事件在属性发生变化触发并且触发dep收集依赖再触发Watch执行更新,这个两个事件在一起就实现了vue中Observer的数据劫持。表面上看起来我们是在操作VM.name,实际上还是通过Object.defineProperty()中的get和set方法劫持实现的。
本篇文章是我对Vue的浅薄之悟,如有理解不足之处,还请大家批评指正,Thank you ~🥰😘