开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第11天,点击查看活动详情
前面我们有篇文章主要介绍了vue2数据响应式原理,如果说那篇文章主要是理论篇,那我们这篇文章是实践版,主要是围绕对象和数组的监测,如果机制不能自动及时更新视图,我们该如何手动更新视图。
监测对象
我们在前面已经了解到,vue监测对象是使用Object.defineProperty()来进行数据劫持的,这样就给数据添加了一个代理,每次对象的属性被修改时就会调用setter, 对象的属性被获取的时候就会调用getter。
下面我们简单地实现一下vue的响应式
let obj = {
name: '广东刘亦菲',
age: 18,
job: {
name: 'coder',
weekdays: 5
}
}
// 创建一个监视的实例对象,用于监视data属性的变化
const reactiveObj = new Observer(obj)
let vm = {}
vm._data = obj = reactiveObj
function Observer (obj) {
// 这个类里面就需要给每个属性都添加响应式
const keys = Object.keys(obj)
keys.forEach(key => {
Object.defineProperty(this, key, {
get () {
console.log(`${this}上的${key}正在被读取`)
return obj[key]
},
set (value) {
console.log(`${this}上的${key}正在被修改`)
obj[key] = value
}
})
})
}
特别说明一下:
为什么Object.defineProperty()第一个参数是this,而不是obj。是因为这个this拿到的是实例化对象,也就是上述代码中的reactiveObj(代理之后的对象)。如果写成obj的话,那么get里面返回值是obj[key],也就是说再次读取obj的属性值,这样的话就会造成一个死循环,最终导致内存溢出。
监测数组
vue里面监听数组的方式其实是对Array.property上的数组的方法进行了封装。换句话说,在vue里面访问数组的这些方法已经不再是访问数组原型上的那些方法了。具体有以下这些方法
- push()
- pop()
- shift()
- unshift()
- sort()
- splice()
- reverse()
<template>
<div class="hello">
<div>姓名:{{obj.name}}</div>
<div>年龄:{{obj.age}}</div>
<ul>
<li v-for="(item, index) in obj.leaders" :key="index">
{{item.name}} - {{item.age}}
</li>
</ul>
<button @click="addLeader">添加领导信息</button>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data () {
return {
obj: {
name: '广东刘亦菲',
age: 18,
leaders: [
{
name: '一个专政的独裁者',
age: 40
},
{
name: '一个温柔的管理者',
age: 35
}
]
}
}
},
methods: {
addLeader () {
let newLeader = {
name: '仙女领导',
age: 30
}
this.obj.leaders.push(newLeader)
this.obj.leaders.pop()
this.obj.leaders.shif()
this.obj.leaders.unshift(newLeader)
this.obj.leaders.splice(0, 1)
this.obj.leaders.sort((a, b) => a - b)
this.obj.leaders.reverse()
this.obj.leaders = [{
name: '仙女领导',
age: 30
}]
this.obj.leaders[0].name = '仙女领导'
}
}
}
</script>
上面除了重写的修改数组的七个方法外,重新给数组赋值,或者给数组的某些项的属性重新设置值都是会触发响应式的。
下面我们来看一下具体原理
主要就是继承Array,然后重写数组上的方法
// 继承Array原型上的所有属性
const extendArr = Object.create(Array.prototype)
const arrMethods = [
'push',
'pop',
'shift',
'unshift',
'sort',
'splice',
'reverse'
]
// 重新包装上述数组的方法
arrMethods.forEach(item => {
const oldItem = Array.prototype[item]
const newItem = function (...args) {
oldItem.apply(this, args)
}
extendArr[item] = newItem
})
只有上述这几种方法才具有响应式
手动更新视图
对于对象类的数据,如果初始化data的时候没有写某个属性,而是通过methods里的方法给他添加属性,不是响应的,视图不会及时更新。 对于数组类的数据,如果使用了上面的那七种方法之外的方法,那么视图也是不会刷新的。比如使用了concat, filter等。 对于这种问题,vue给我们提供了一种方法this.$set()。
1. vue.set()
vue.set()是向响应式对象添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新属性,因为vue无法探测普通的新增属性。
<div>姓名:{{obj.name}}</div>
<div>年龄:{{obj.age}}</div>
<div>工作:{{obj.job}}</div>
<button @click="addJob">添加职位信息</button>
data () {
return {
obj: {
name: '广东刘亦菲',
age: 18
}
}
},
methods: {
addJob () {
this.obj.job = 'coder'
console.log('职位', this.obj.job)
}
}
效果如下:
数据改变了,页面视图没有更新。
1.1 使用全局set
语法:
Vue.set( target, propertyName/index, value )
<script>
import Vue from 'vue'
export default {
name: 'HelloWorld',
data () {
return {
obj: {
name: '广东刘亦菲',
age: 18
}
}
},
methods: {
addJob () {
Vue.set(this.obj, 'job', 'coder')
console.log('职位', this.obj.job)
}
}
}
</script>
效果如下:
1.2 使用组件set
语法格式
vm.$set( target, propertyName/index, value )
<script>
export default {
name: 'HelloWorld',
data () {
return {
obj: {
name: '广东刘亦菲',
age: 18
}
}
},
methods: {
addJob () {
this.$set(this.obj, 'job', 'coder')
console.log('职位', this.obj.job)
}
}
}
</script>
效果如上图
我们在实际开发项目中,主要还是组件化开发,所以使用this.$set()的情况居多。
需要注意的是对象不能是vue实例,或者vue实例的根数据对象。 上述代码示例都是以对象为例的,数组的使用方式也是一样的。
小结
vue监视数据的原理:
- vue会监视data所有对象的所有层级
- 监测对象中的数据: 通过setter实现监视,且要在new Vue时就传入要监测的数据
- 对象中后追加的属性,vue默认不做响应式处理
- 如需给后添加的属性做响应式,请使用:Vue.set( target, propertyName/index, value )或者vm.$set( target, propertyName/index, value )
- 监测数组中的数据: 通过包裹数组更新元素的方法实现,本质上就做了两件事:
- 调用原生对应的方法对数组进行更新
- 重新解析模板,进而更新页面
- 在vue中修改数组中某一项一定要用下面的方法:
- 使用push(), pop(), shift(), unshift(), sort(), splice(), reverse()
- 使用Vue.set()或者vm.set()不能是vue实例,或者vue实例的根数据对象。