监听和代理以及数据响应式
数据响应式
从一开始使用 Vue 时,对于之前的 jq 开发而言,一个很大的区别就是基本不用手动操作 dom,data 中声明的数据状态改变后会自动重新渲染相关的 dom。
换句话说就是 Vue 自己知道哪个数据状态发生了变化及哪里有用到这个数据需要随之修改。
因此实现数据响应式有两个重点问题:
- 如何知道数据发生了变化?
- 如何知道数据变化后哪里需要修改?
对于第一个问题,如何知道数据发生了变化,Vue3 之前使用了 ES5 的一个 API Object.defineProperty Vue3 中使用了 ES6 的 Proxy,都是对需要侦测的数据进行 变化侦测 ,添加 getter 和 setter ,这样就可以知道数据何时被读取和修改。
第二个问题,如何知道数据变化后哪里需要修改,Vue 对于每个数据都收集了与之相关的 依赖 ,这里的依赖其实就是一个对象,保存有该数据的旧值及数据变化后需要执行的函数。每个响应式的数据变化时会遍历通知其对应的每个依赖,依赖收到通知后会判断一下新旧值有没有发生变化,如果变化则执行回调函数响应数据变化(比如修改 dom)。
所以接下来我们就在代码中学习getter/setter
发现疑点
在学习的时候,发现了一个问题
eg
Vue.config.productionTip = false;//除去Vue的警告
const myData = {
n: 0
}
console.log(myData) //这个log很关键
这时候我们发现这里的myData很正常是
很正常
但是当我们new Vue以后
new Vue({
data: myData,
template: `
<div>{{n}}</div>
`
}).$mount("#app");
setTimeout(()=>{
//在外面设定延时依旧可以改变V实例里的数据
myData.n += 10
// console.log(myData) // 本节课精髓
},3000)
这啥?
为什么一个{n:0}的数据在new vue后,console.log的结果是{n:{...}}
要搞懂这个我们先要搞懂getter/setter
getter/setter
我们用一个例子来讲解
eg
let obj0 = {
姓: "高",
名: "圆圆",
age: 18
};
此时我们需求一是得到姓名
则
let obj1 = {
姓: "高",
名: "圆圆",
姓名() {
return this.姓 + this.名;
},
age: 18
};
console.log("需求一:" + obj1.姓名());
// 姓名后面的括号能删掉吗?不能,因为它是函数
// 怎么去掉括号?
这时候我们产生了需求二。
需求二,姓名不要括号也能得出值
let obj2 = {
姓: "高",
名: "圆圆",
get 姓名() {
return this.姓 + this.名;
},
age: 18
};
console.log("需求二:" + obj2.姓名);
// 总结:getter 就是这样用的。不加括号的函数,仅此而已。
意思就是get可以使得一个函数转变成属性,但是这个属性是计算属性而不是一般的属性,只是以函数的形式表现出来。
那么计算属性是啥?
一般的计算属性值是函数形式,这个函数会返回一个值,并且其函数内部还可能会依赖别的变量
简单来说用法就是在一个计算属性里可以完成各种复杂的逻辑,包括运算、函数调用等,只要最终返回一个结果就可以。除了上例简单的用法, 计算属性还可以依赖多个 Vue 实例的数据,只要其中任一数据变化,计算属性就会重新执行,视图也会更新 。
具体可以参考[Vue.js进阶]从源码角度剖析计算属性的原理
此时我们产生了需求三:姓名可以被写
let obj3 = {
姓: "高",
名: "圆圆",
get 姓名() {
return this.姓 + this.名;
},
set 姓名(xxx){
this.姓 = xxx[0]
this.名 = xxx.slice(1)
},
age: 18
};
obj3.姓名 = '高媛媛'
console.log(`需求三:姓 ${obj3.姓},名 ${obj3.名}`)
// 总结:setter 就是这样用的。用 = xxx 触发 set 函数
姓名也是...的形式
要注意的是,set一定要接受一个新的传参。这个参数就是后来传的,传进来的是高媛媛,我们就可以对其操作。
现在我们可以知道这是啥了
结论
其实n(...)就是set/get后属性的样子,证明这个是可对其进行读写操作,但实际上这个是一个计算属性,是一个无实际存在的属性
object.Defineproperty
接下来我们讨论object.Defineproperty用法
- Object.defineProperty()
- 语法:
Object.defineProperty(obj, prop, descriptor)
-
参数说明:
obj:必需。目标对象
prop:必需。需定义或修改的属性的名字
descriptor:必需。目标属性所拥有的特性 -
返回值:
传入函数的对象。即第一个参数obj -
针对属性,我们可以给这个属性设置一些特性,比如是否只读不可以写;是否可以被
for..in
或Object.keys()
遍历。
给对象的属性添加特性描述,目前提供两种形式:数据描述和存取器描述。 -
这个属性一般是已经定义完却还想加属性的时候用
代理和监听(重点)
接下来我们看看代理和监听的用法,首先
eg
let data0 = {
n: 0
}
需求一:用 Object.defineProperty 定义 n
let data1 = {}
Object.defineProperty(data1, 'n', {
value: 0
})
console.log(`需求一:${data1.n}`)
怎么感觉更加复杂了?
继续看,当我们增加了需求二:n 不能小于 0
即 data2.n = -1 应该无效,但 data2.n = 1 有效
let data2 = {}
data2._n = 0 // _n 用来偷偷存储 n 的值
Object.defineProperty(data2, 'n', {
get(){
return this._n//这是一个虚拟的n,只要不是n即可,也可以写成aaa,bbb,ccc
},
set(value){
if(value < 0) return
this._n = value
}
})
console.log(`需求二:${data2.n}`)
data2.n = -1
console.log(`需求二:${data2.n} 设置为 -1 失败`)
data2.n = 1
console.log(`需求二:${data2.n} 设置为 1 成功`)
那如果对方直接使用 data2._n 呢?
这时候增加了需求三:使用代理
let data3 = proxy({ data:{n:0} }) // 括号里是匿名对象,无法访问
function proxy({data}/* 解构赋值 */){
const obj = {}
// 这里的 'n' 写死了,理论上应该遍历 data 的所有 key,这里做了简化
Object.defineProperty(obj, 'n', {
get(){
return data.n
},
set(value){
if(value<0)return
data.n = value
}//这样可以使得set出来先判断,符合要求才赋值给value
})
return obj // obj 就是代理
}
// data3 就是 obj
console.log(`需求三:${data3.n}`)
data3.n = -1
console.log(`需求三:${data3.n},设置为 -1 失败`)
data3.n = 1
console.log(`需求三:${data3.n},设置为 1 成功`)
代理其实是这个proxy功能,可以使得别的人无法直接修改最直接的数据(也就是不给别人名字使得别人无法直接修改),所有操作都必须通过proxy传递一下。这个相当于中介,只能对proxy操作,它再对data操作。也就是封装思想。
但是光使用代理可以吗?显然不能应对所有情况,这时候有了
需求四
let myData = {n:0}
let data4 = proxy({ data:myData }) // 括号里是匿名对象,无法访问
// data3 就是 obj
console.log(`杠精:${data4.n}`)
myData.n = -1
console.log(`杠精:${data4.n},设置为 -1 失败了吗!?`)
// 可以通过改 myData直接修改
这是不行的所以!
需求五:就算用户擅自修改 myData,也要拦截他
let myData5 = {n:0}
let data5 = proxy2({ data:myData5 }) // 括号里是匿名对象,无法访问
function proxy2({data}/* 解构赋值*/){
// 这里的 'n' 写死了,理论上应该遍历 data 的所有 key,这里做了简化
let value = data.n
Object.defineProperty(data, 'n', {
get(){
return value
},
set(newValue){
if(newValue<0)return
value = newValue
}
})
// 就加了上面几句,这几句话会监听 data
const obj = {}
Object.defineProperty(obj, 'n', {
get(){
return data.n
},
set(value){
if(value<0)return//这句话多余了
data.n = value
}
})
return obj // obj 就是代理
}
// data3 就是 obj
console.log(`需求五:${data5.n}`)
myData5.n = -1
console.log(`需求五:${data5.n},设置为 -1 失败了`)
myData5.n = 1
console.log(`需求五:${data5.n},设置为 1 成功了`)
// 这代码看着眼熟吗?
// let data5 = proxy2({ data:myData5 })
// let vm = new Vue({data: myData})
这就是监听,其实监听的代码只多了
let value = data.n
Object.defineProperty(data, 'n', {
get(){
return value
},
set(newValue){
if(newValue<0)return
value = newValue
}
})
使得若是直接修改mydata5,这个若是小于0则传入的new value将不会赋值给value,达到监听效果