当数据发生改变时 我们要实时监听到,并且随之做出改变(DOM操作)
概念: **Object.defineProperty() **方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
**备注:**应当直接在 Object 构造器对象上调用此方法,而不是在任意一个 Object 类型的实例上调用。
Object.defineProperty(obj, prop, descriptor)
Object.keys
Object.values
Object.assign()obj要定义属性的对象。prop要定义或修改的属性的名称或Symbol 。descriptor要定义或修改的属性描述符。被传递给函数的对象。
该方法允许精确地添加或修改对象的属性。通过赋值操作添加的普通属性是可枚举(遍历)的,在枚举对象属性时会被枚举到(for...in 或 Object.keys方法),可以改变这些属性的值,也可以删除这些属性。这个方法允许修改默认的额外选项(或配置)。默认情况下,使用 Object.defineProperty() 添加的属性值是不可修改(immutable)的。
对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符**。数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符是由 getter 函数和 setter 函数所描述的属性。一个描述符只能是这两者其中之一;不能同时是两者。
这两种描述符都是对象。它们共享以下可选键值(默认值是指在使用 Object.defineProperty() 定义属性时的默认值):
configurable当且仅当该属性的configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。 默认为 false。enumerable 控制属性是否可以枚举(遍历)
当且仅当该属性的enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。 默认为 false。数据描述符还具有以下可选键值:
value该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。writable当且仅当该属性的writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符 (en-US)改变。 默认为 false。存取描述符还具有以下可选键值:
get属性的 getter 函数,如果没有 getter,则为undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。 默认为 undefined。set属性的 setter 函数,如果没有 setter,则为undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。 默认为 undefined。configurable、enumerable 和writable 的默认值都是false。value、get 和set 字段的默认值为undefined。案例:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script type="text/javascript">
let people = {
name:'张三',
address:'郑州'
age:18,
}
/*
Object.defineProperty(obj,prop,descriptor)
obj:表示要添加属性的对象
prop:表示要添加的属性
descriptor:表示对新增属性的配置(描述)
*/
Object.defineProperty(people,'age',{
value:19,//新增属性age的值
enumerable:true,//控制属性是否可以被遍历
writable:true,//控制属性能否被修改
})
console.log(people)
console.log(Object.keys(people))
</script>
</body>
</html>可以使用Object.defineProperty()方法对 对象中的属性进行更精确的控制。
需求:能否让people对象中的age属性动态发生变化呢?
**尝试方法:**在外界定义变量num 把num的值赋给perople中的age属性。
<script>
let num = 19;
let people = {
name:'张三',
address:'郑州'
age:num,
}
</script>**结论:**此时代码第一次执行时,可以把num赋值给age,但是 后续num的值发生改变后,就无法在赋值了,所以无法完成需求。
**解决方法:**借助于Object.defineProperty()方法 中的get()配置来实现
<script>
let num = 19;
let people = {
name:'张三',
address:'郑州'
}
Object.defineProperty(people,'age',{
//当有代码使用age属性时,就会执行get()方法,return的num就是age属性的值
get(){
return num
},
//当有代码给age属性赋值时 就会执行set()函数 并接收到要修改的值 val 再把val的值赋给 num变量 只要num的值发生改变 就相当于age的值发生了改变。
set(val){
num = val
}
})
</script>
此时通过 Object.defineProperty方法 添加get(){}函数描述符 可以解决问题,此时并不是直接把num的值赋给age,而是,当每次有调用age属性时,就去执行get()函数 去重新读取num的值。此时 只要num的值发生改变 age的值就会随之发生改变。可以完成我们的需求。**数据代理:**通过一个对象来对另一个对象中的属性进行读和写。
代码分析:
<body>
<div id="app">
<h1>名字是:{{name}}</h1>
<h1>年龄是:{{age}}</h1>
</div>
</body>
<script type="text/javascript">
let vm = new Vue({
el:'#app',
data:{
name:'张三',
age:18
}
})
</script>**问题一:**页面标签中显示的数据 是直接从data中获取的吗?
不是,vue在读取到data属性后 会先把data赋值给 vue实例上的另外一个属性 _data ,再把_data中的属性和getter和setter方法复制到vm的实例对象上。
问题二: 页面标签中的数据是直接从_data属性中获取的吗 ?
不是,如果是直接从_data中获取数据,此时我们在页面标签中取数据就应该这样写 {{_data.name}} 这样写法其实也是可以的,但是我们每一次的操作都要加上_data编码不方便,所以 vue又把_data中的属性取出来在vm实例对象上放了一份。所以我们后续的代码写法可以直接写成{{name/age}},而不用添加 _data了。底层的实现就是Object.defineProperty()方法。
问题三: vm实例对象上的_data属性中为什么也会有对应的getter和setter方法呢?使用的是数据代理吗?
不是,注意:此处采用的是数据劫持(就是Vue会先对data中的数据进行处理了,添加getter和setter方法就叫数据劫持),因为vue 要对数据做监听,当数据发生改变时,要同时把改变后的数据同步更新到DOM中。所以此处 被叫做响应式(vue监测数据改变的原理)。
小结:Object.defineProperty()方法的主要作用:
**核心:**是当vue监测到数据改变时,是如何让页面上所有用到这个数据的DOM 发生更新的!!!
**设计模式: ** 观察者模式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jdkuKMmc-1655452041920)(assets/image-20220309145018046.png)]
案例:用户关注公众号,订阅公众号信息,当公众号发布消息时,所有关注这个公众号的用户都可以接收到消息。
ES5 通过原型对象完成发布与订阅:
//定义发布者 Observer
function GongZongHao(name) {
console.log(name + '公众号创建了')
this.name = name;
//定义数组 用来存储关注公众号的用户
this.subList = [];
}
//向原型上添加方法 订阅方法 和 发布方法
GongZongHao.prototype.addUser = function(user) {
console.log(user.name + "关注了AAA公众号")
this.subList.push(user)
}
GongZongHao.prototype.sendMsg = function(msg) {
console.log(this.name + "发布了消息:" + msg)
this.subList.forEach(item => {
//调用用户接收消息的方法
item.receive(msg)
})
}
//定义用户
function User(name) {
this.name = name;
}
//定义用户接收消息的方法
User.prototype.receive = function(msg) {
console.log(this.name + "接收到了消息:" + msg)
}
//定义AAA公众号对象
let AAA = new GongZongHao('aaa')
//定义用户张三,李四,王五,赵六
let zhangsan = new User('张三')
let lisi = new User('李四')
let wangwu = new User('王五')
let zhaoliu = new User('赵六')
//让用户订阅AAA公众号
AAA.addUser(zhangsan)
AAA.addUser(lisi)
AAA.addUser(wangwu)
AAA.addUser(zhaoliu)
//AAA公众号发布消息
AAA.sendMsg('今天是个好天气')ES6 类与对象完成发布与订阅:
//定义发布者 公众号
class GongZongHao{
constructor(name) {
//定义公众号的名字
this.name = name
//定义一个数组用来存储订阅公众号的用户
this.subList = []
}
//定义订阅公众号的方法
addUser(user){
this.subList.push(user)
}
//定义公众号发送消息的方法
sendMsg(msg){
console.log(this.name+"公众号发布了消息:"+msg)
this.subList.forEach(item=>{
item.receive(msg)
})
}
}
//定义订阅者 用户
class User{
constructor(name) {
//定义用户的名字
this.name = name;
}
//定义用户接收信息的方法
receive(msg){
console.log(this.name+"接收到了消息:"+msg)
}
}
//定义AAA公众号
let AAA = new GongZongHao('aaa')
//定义用户张三
let zhangsan = new User('张三')
let lisi = new User('李四')
let wangwu = new User('王五')
let zhaoliu = new User('赵六')
//让用户订阅AAA公众号
AAA.addUser(zhangsan)
AAA.addUser(lisi)
AAA.addUser(wangwu)
AAA.addUser(zhaoliu)
//AAA公众号发布消息
AAA.sendMsg('今天是个好天气')案例:手动实现简易版vue监测对象的数据原理:
**需求:**定义data数据 当其中属性的值发生改变时 就 在控制台输出 某属性发生了改变。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script type="text/javascript">
let data = {
name:'张三',
age:18
}
//这种方式 会造成死循环
/* Object.defineProperty(data,'name',{
get(){
return data.name
},
set(val){
data.name = val
}
})
console.log(data);
data.name = 'Lis' */
//定义构造函数
function Obs(obj){
this.name = obj.name;
this.age = obj.age;
let keys = Object.keys(obj)
keys.forEach(k=>{
Object.defineProperty(this,k,{
get(){
return obj[k]
},
set(val){
console.log(`${k}被修改了`)
obj[k] = val
}
})
})
}
//创建Obs构造函数的实例对象 并把要监测的 data数据对象 传入
let obs = new Obs(data)
//把定义好的数据监测对象赋值给data本身和vue实例对象
let vm = data = obs
data.name = '王五'
data.age = 19
</script>
</body>
</html>案例: 使用发布与订阅模式+Object.defineProperty()方法完成监听数据改变,并更新页面DOM的需求。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<p id='p1'>你好哈哈哈</p>
</body>
<script type="text/javascript">
//定义发布者
class Dep{
constructor(data) {
this.data = data
//定义数组用来存储订阅者
this.subList = []
}
//添加订阅者
addWatcher(watcher){
//把订阅者添加到数组中
this.subList.push(watcher)
}
//通知订阅者
send(){
//通知所有订阅者 并调用对应的操作
this.subList.forEach(w=>{
w.callBack(this.data)
})
}
//监听data中数据的改变
listenData(){
//console.log(this)
let that = this;
//取出要监听的数据中所有的key值
let keys = Object.keys(this.data)
keys.forEach(key=>{
let value = this.data[key]
Object.defineProperty(this.data,key,{
configurable:true,
enumerable:true,
//当外界获取元素时执行
get(){
console.log(`读取了${key}的值`)
return value
},
//当外界设置,或修改元素时执行 val就是修改时 传入的值
set(val){
console.log(`${key}的值发生了改变`)
value = val
//调用通知方法
that.send()
}
})
})
}
}
//定义订阅者
class Watcher{
constructor(fn) {
this.fn = fn
}
callBack(data){
this.fn(data)
}
}
//定义订阅者收到消息后的操作
function fn(data){
console.log('监听到数据发生改变,需要调用渲染dom的方法')
console.log(data)
//更新DOM结构的数据
document.getElementById('p1').innerText = data.name
}
//创建订阅者对象
let w1 = new Watcher(fn)
//定义vue对象的构造函数
class my_vue{
constructor(options) {
this.$data = options.data
this.$el = options.el
//创建发布者
let dep = new Dep(options.data)
//调用添加订阅者方法
dep.addWatcher(w1)
//调用监听数据的改变的方法
dep.listenData()
//通知订阅者
dep.send()
}
}
//定义被代理的数据
let obj = {
name:'张三',
age:18
}
//创建vue实例对象
let vm = new my_vue({
el:'#app',
data:obj
})
</script>
</html>Vue.set()和vm.$set()方法需求:vm实例对象中data属性中含有一个对象类型的数据Person,如何动态的向Person对象中添加一个响应式的属性?
如果我们直接向Person对象中添加一个age属性 可以添加成功,但是此时这个age属性并不是响应式数据,也就是说数据发生改变时,页面视图并不会随之改变。我们可以通过Vue.set()或者vm.set()方法来完成这个需求。
{Object | Array} target{string | number} propertyName/index{any} valuethis.myObject.newProperty = 'hi')
**注意:**对象不能是 Vue 实例,或者 Vue 实例的根数据对象。<body>
<div id="app">
<h2>姓名是:{{person.name}}</h2>
<h2>地址是:{{person.address}}</h2>
<h2 v-if="person.age">年龄是:{{person.age}}</h2>
<button type="button" @click="addAge">点我添加属性age</button>
</div>
</body>
<script type="text/javascript">
new Vue({
el:'#app',
data:{
person:{
name:'小明',
address:'郑州'
}
},
methods:{
addAge(){
//这种添加方式不是响应式的数据
//this.person.age = 18
//通过vue内部提供的方法添加 就是响应式数据
//Vue.set(this.person,'age',19)
this.$set(this.person,'age',20)
}
}
})
</script>**注意:**此方法只能用于在data内部的对象属性上添加内容,而不能直接在data上 或者 vm上添加属性。
小结:
data 对象上存在才能让 Vue 将它转换为响应式的。Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。**需求:**编码实现 点击按钮把张三的个人信息更改成新的信息。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<script src="../js/vue.js" type="text/javascript" charset="utf-8"></script>
</head>
<body>
<div id="app">
<button type="button" @click="changeZhangSan">点我</button>
<ul>
<li v-for="item in list">
编号:{{item.id}},名字:{{item.name}},年龄:{{item.age}}
</li>
</ul>
</div>
</body>
<script type="text/javascript">
//需求:定义按钮,点击触发函数 改变 张三的名字为张三三 年龄为40
new Vue({
el:'#app',
data:{
list:[
{id:1,name:'张三',age:18},
{id:2,name:'李四',age:19},
{id:3,name:'王五',age:20}
]
},
methods:{
changeZhangSan(){
//这样改可以
//this.list[0].name = '张三三'
//创建一个新的对象把整体的张三所在的对象给替换掉
this.list[0] = {id:1,name:'张三三',age:40}
//此处修改不行,vue并不能监测到此时数据的改变
}
}
})
</script>
</html>**注意:**此时发现问题,我们如果直接通过下标去修改数组中的内容,vue是无法监测到数组中数据发生改变的。
Vue 不能检测以下数组的变动:
vm.items[indexOfItem] = newValuevm.items.length = newLengthVue.set(target,index,replace)和vm.$set(target,index,replace)方法实现filter()、concat() 和 slice()。它们不会变更原始数组,而总是返回一个新数组。当使用非变更方法时,可以用新数组替换旧数组push()结尾追加,
pop()删除末尾,
shift()删除开头,
unshift()开头添加,
sort()排序,
splice()指定位置删除,添加,
reverse()反转总结: vue2中的数据代理主要使用的就是Object.defineProperty()方法来完成的。
**作业:**今日所讲内容整理笔记 背住。面试题。
em] = newValue`
vm.items.length = newLengthVue.set(target,index,replace)和vm.$set(target,index,replace)方法实现filter()、concat() 和 slice()。它们不会变更原始数组,而总是返回一个新数组。当使用非变更方法时,可以用新数组替换旧数组push()结尾追加,
pop()删除末尾,
shift()删除开头,
unshift()开头添加,
sort()排序,
splice()指定位置删除,添加,
reverse()反转总结: vue2中的数据代理主要使用的就是Object.defineProperty()方法来完成的。
**作业:**今日所讲内容整理笔记 背住。面试题。