Vue.js 的 data 属性、方法、计算属性和监听器
在前面的代码中,我们或多或少使用了一些 Vue
的特性,例如 data
属性、方法属性 methods
等,这些都是进行组件配置的重要内容。本节就来详细介绍一下这些配置。
data属性
在 Vue
的组件中,我们必不可少地会用到 data
属性,在 Vue 3
中,data
属性是一个函数,Vue
在创建新组件实例的过程中调用此函数。它应该返回一个对象,然后 Vue
会通过响应性系统将其包裹起来,并以 $data
的形式存储在组件实例中,如示例代码2-3-1所示。
const app = Vue.createApp({
data() {
return { count: 4 }
}
})
const vm = app.mount('#app')
console.log(vm.$data.count) // => 4
console.log(vm.count) // => 4
// 修改 vm.count 的值也会更新 $data.count
vm.count = 5
console.log(vm.$data.count) // => 5
// 反之亦然
vm.$data.count = 6
console.log(vm.count) // => 6
如果在组件初始化时 data
返回的对象中不存在某个 key
,后面再添加,那么这个新增加的 key
所对应的属性 property
是不会被 Vue
的响应性系统自动跟踪的,代码如下:
<div id="app">
<span>{{name}}</span>
<span>{{age}}</span>
</div>
const vm = Vue.createApp({
data() {
return {
name: 'John'
}
}
}).mount("#app")
vm.age = 12
上面的代码中,{{age}}
的值将不会被渲染出来。
方法
在 Vue.js
中,将数据渲染到页面上用得最多的方法莫过于插值表达式 {{}}
。插值表达式中可以使用 文本 或者 JavaScript 表达式 来对数据进行一些处理,代码如下:
<div id="example">
{{ message.split('').reverse().join('') }}
</div>
但是,设计它们的初衷是用于简单运算。在插值表达式中放入太多的程序逻辑会让模板过 “重” 且难以维护。因此,我们可以将这部分程序逻辑单独剥离出来,并放到一个方法中,这样共同的程序逻辑既可以复用,也不会影响模板的代码结构,并且便于维护。
这里的方法和之前讲解的使用 v-on
指令的事件绑定方法在程序逻辑上有所不同,但是在用法上是类似的,同样还是定义在 Vue
组件的 methods
对象内,如示例代码2-3-2所示。
<div id="app">
{{height}}
{{personInfo()}}
</div>
const vmMethods = Vue.createApp({
data() {
return {
name: 'Jack',
age: 23,
height: 175,
country: 'China'
}
},
methods:{
personInfo(){
console.log('methods')
var isFit = false;
// 'this' 指向当前Vue实例
if (this.age > 20 && this.country === 'China') {
isFit = true;
}
return this.name + ' ' + (isFit ? '符合要求' : '不符合要求');
}
}
}).mount("#app")
首先在 methods
中定义了一个 personInfo
方法,将众多的程序逻辑写在其中,然后在模板的插值表达式中调用 {{personInfo()}}
,与使用 data 中的属性不同的是,在插值表达式中使用方法需要在方法名后面加上括号 “()” 以表示调用。
使用方法也支持传参,如示例代码2-3-3所示。
<div id="app">
{{personInfo('Tom')}}
</div>
...
methods:{
personInfo(params){
console.log(params)
}
}
计算属性
前面一节介绍了采用方法的方案来解决在插值表达式中写入过多数据处理逻辑的问题,还有一种方案可以解决这类问题,那就是 计算属性,如示例代码2-3-4所示。
<div id="app">
{{height}}
{{personInfo}}
</div>
const vmComputed = Vue.createApp({
data() {
return {
name: "Jack",
age: 23,
height: 175,
country: "China"
}
},
computed:{
personInfo(){
console.log('computed')
// "this" 指向当前Vue实例,即vm
let isFit = false;
if (this.age > 20 && this.country === "China") {
isFit = true;
}
return this.name + " " + (isFit ? "符合要求" : "不符合要求");
}
}
}).mount("#app")
在上面的代码中,同样实现了将数据处理逻辑剥离的效果,看似把之前的 methods
换成了 computed
,以及将插值表达式的 {{personInfo()}}
换成了 {{personInfo}}
,虽然表面上的结构一样,但是内部却有着不同的机制。
插值中 计算属性 获取不需要括号。 |
计算属性和方法
首先,对于方法(methods
),在之前的代码中,我们在 personInfo
对应的函数中添加一个 console.log("methods")
,然后首先修改 data
中的 name
属性,代码如下:
vmMethods.name = 'Pom';
观察控制台,显示结果如图2-9所示。

由于修改了 vmMethods
的 name
属性,而在 personInfo
方法里面用到了 name
属性,因此 personInfo
方法和 name
就存在依赖,那么 name
属性的修改就导致 personInfo
方法重新执行了一遍,因此可以看到 name
更新成了 pom
,同时打印了一次 console.log("methods")
。随后,我们再次调用 vmMethods.name ='Pom'
,这次没有打印出 console.log("methods")
,原因是 name
的值并没有改变。
同理,对于计算属性(computed
),尝试修改 vmComputed
的 name
属性,代码如下:
vmComputed.name = 'Pom';
观察控制台,显示结果如图2-10所示。

第一次调用 vmComputed.name='Pom';
打印出了 console.log("computed")
。然后又调用了一次 vmComputed.name = 'Pom';
,这次没有打印出 console.log("computed")
。到这里,看似和 methods
方法没有区别。
接下来,我们尝试去改变 height
属性,和 name
属性不同的是,height
属性写在了插值表达式 {{height}}
中,是直接绑定数据进行渲染的,和 personInfo
方法没有依赖。我们看看效果。
修改 vmMethods
的 height
属性:
vmMethods.height = 180;
观察控制台,显示的结果如图2-11所示。

可以看到打印出了 console.log("methods")
,说明执行了 personInfo()
方法,那么再修改一下 vmComputed
的 height
属性:
vmComputed.height = 180;
观察控制台,显示的结果如图2-12所示。

可以看到并没有打印出 console.log("computed")
,说明没有执行 personInfo()
方法。通过上面的一系列操作,可以得到如下结论:
-
如果
personInfo
依赖的数据发生改变,即通过修改data
中的属性改变name
、age
和country
其中之一或多个,导致插值表达式{{personInfo}}
或{{personInfo()}}
更新用户界面,那么personInfo()
方法就会重新执行。反之,如果personInfo
依赖的数据没有改变,personInfo()
方法就不会重新执行,methods
和computed
的表现是一致的。 -
如果
personInfo
依赖的数据并没有发生改变,即通过修改data
中的height
属性,这个改变会导致插值表达式{{height}}
更新用户界面,那么定义在methods
中的personInfo()
方法总是会执行,而定义在computed
中的personInfo()
方法则不会执行。
这就意味着,只要 data
中的属性 name
、age
和 country
没有发生改变,无论何时访问计算属性,都会立即返回之前的计算结果,而不必再次执行对应的函数,这说明计算属性是基于它们的响应式依赖进行缓存的,而方法却没有这种表现。总结一下它们的区别就是:
-
计算属性:只要依赖的数据没发生改变,就可以直接返回缓存中的数据,而不需要每次都重复执行数据操作。
-
方法:只要页面更新用户界面,就会发生重新渲染,
methods
调用对应的方法,执行该函数,无论是不是它所依赖的。
对于计算属性来说,上面定义的 personInfo
所对应的函数其实只是一个 getter
方法,每一个计算属性都包含一个 getter
方法和一个 setter
方法,上面的两个示例都是计算属性的默认用法,只调用了 getter
方法来读取。
在需要时,也可以提供一个 setter
方法,当手动修改计算属性的值时,就会触发 setter
方法,执行一些自定义的操作,如示例代码2-3-5所示。
<div id="app">
{{name}}
<br/>
{{personInfo}}
</div>
const vmComputed = Vue.createApp({
data(){
return {
name: "Jack",
age: 23,
height: 175,
country: "China"
}
},
computed:{
personInfo: {
get(){
console.log("get");
return
"height:"+this.height+",age:"+this.age+",country:"+this.country;
},
set(){
this.height = 165;
this.name = "Pom";
console.log("set");
}
}
}
}).mount("#app")
上面的 set
对应的 function
就代表 setter
方法,get
对应的 function
就代表 getter
方法。运行这段代码,可以看到页面显示如图2-13所示。

并且可以看到控制台上打印了 console.log('get')
,这时在控制台上运行如下代码:
vmComputed.personInfo = 'hello';
可以看到 setter
方法会被调用,同时 height
和 name
的值也被修改了,控制台上显示的结果如图2-14所示。

对应页面的用户界面显示如图2-15所示。

需要说明的是,虽然直接去修改 computed
的 personInfo
的值,但是并没有改变 personInfo
的值,这是因为如果要判断 personInfo
的值是否被改变了,首先要读取 personInfo
的值,而读取 personInfo
的值是由 getter
方法的 return
值控制的,所以一般使用 setter
方法的应用场合大多数是把它当作一个钩子函数来使用,并在其中执行一些业务逻辑。
由此可见,在绝大多数情况下,只会用默认的 getter
方法来读取一个计算属性,在业务中很少用到 setter
方法,因此在声明一个计算属性时,可以直接使用默认的写法,不必同时声明 getter
方法和 setter
方法。
了解了计算属性之后,可以发现,计算属性之所以叫作 “计算” 属性,是因为它是固定属性 data
的对应,同时多了一些对数据的计算和处理操作。
在通常情况下,如果一个值是简单的固定值,无须特殊处理,在 data
中添加之后,在插值表达式中使用即可。但是,如果一个值是不固定的,它可能随着一些固定属性的改变而改变,这时就可以把它设置在计算属性中。一般情况下,同一个属性名,设置了计算属性就无须设置固定属性。反之,设置了固定属性就无须设置计算属性。各位读者在编写代码时要注意这种原则。
在使用计算属性处理数据时,也是可以传递参数的,具体做法是在定义计算属性时,用 return
返回一个函数,如示例代码2-3-6所示。
<div id="app">
{{personInfo('son')}}
</div>
const vmComputed = Vue.createApp({
data() {
return {
name: "Jack",
}
},
computed: {
personInfo() {
return (params) => {
console.log(params);
return this.name + params;
}
}
}
}).mount("#app")
采用 {{personInfo('son')}}
将参数传递进去,看起来就像调用一个方法。
监听器
通过上面对计算属性的 setter
方法的讲解,我们知道 setter
方法提供了一个钩子函数,尽管利用这个钩子函数可以监听到属性的变化,但有时还需要一个自定义的监听器(侦听器),这个监听器有一个监听属性 watch
,如示例代码2-3-7所示。
<div id="app">
{{name}}
</div>
const vmWatch = Vue.createApp({
data() {
return {
name: 'Jack'
}
},
watch:{
name(newV, OldV){
console.log('新值:'+newV+',旧值:'+oldV)
}
}
}).mount("#app")
在上面的代码中定义了一个监听属性 watch
,它所监听的是 data
中定义的 name
属性,修改一下 vmWatch
的 name
属性,代码如下:
vmWatch.name = 'Petter';
观察控制台,显示的结果如图2-16所示。

可以看到,在 watch
中定义的 name
所对应的 function
被执行了,同时打印出了 name
的新旧值。监听属性 watch
的用法很简单,在逻辑上也比较好理解。也可以使用监听器监听父子组件传值时使用 props
传递的值,在后面3.2节会讲解。
这样使用 watch
时有一个特点,就是当值第一次绑定时,不会执行监听函数,只有当值发生改变才会执行。如果需要在最初绑定值的时候也执行函数,则需要用到 immediate
属性。比如当父组件向子组件动态传值时,子组件 props
首次获取到父组件传来的默认值时,也需要执行函数,此时就需要将 immediate
设为 true
,如示例代码2-3-8所示。
const vmWatch = Vue.createApp({
data() {
return {
name: 'Jack'
}
},
watch: {
name: {
handler: function (newV,oldV) {
...
},
immediate: true
}
}
}).mount("#app")
这里把监听的数据写成对象形式,包含 handler
方法和 immediate
,之前编写的函数其实就是在编写这个 handler
方法。immediate
表示在 watch
中首次绑定时,是否执行 handler
,若值为 true
,则表示在 watch
中声明时,就立即执行 handler
方法;若值为 false
,则和一般使用 watch
一样,在数据发生变化的时候才执行 handler
方法。
当需要监听一个复杂对象的改变时,普通的 watch
方法无法监听到对象内部属性的改变,例如监听一个对象,只有这个对象整体发生变化时,才能监听到,如果是对象中的某个属性发生变化或者对象属性的属性发生变化,此时就需要使用 deep
属性来对对象进行深度监听,如示例代码2-3-9所示。
const vmWatch = Vue.createApp({
data() {
return {
name: 'Jack'
}
},
watch:{
name: {
handler: function(newV, oldV) {
...
},
deep: true
}
}
}).mount("#app")
在上面的代码中,尝试修改 this.obj.num
的值,会发现并不会触发 watch
监听的方法,当添加 deep:true
时,watch
监听的方法便会触发。另外,这种直接监听 obj
对象的写法会给 obj
的所有属性都加上这个监听器,当对象属性较多时,每个属性值的变化都会执行 handler
方法。如果只需要监听对象中的一个属性值,则可以进行优化,使用字符串的形式监听对象属性,代码如下:
watch:{
'obj.num': {
handler: function(newNum, oldNum) {
...
},
}
}
此时,就无须设置 deep:true
选项了。在 Vue 3
中,如果需要监听 data
某个数组的变化,分为两种情况:
-
直接重新赋值数组。
-
调用数组的
push()
、pop()
等方法。
const vmWatch = Vue.createApp({
data() {
return {
names: ['Jack','Tom']
}
},
watch:{
names: {
handler: function(newV, oldV) {
console.log('watch')
},
deep: true
}
}
}).mount("#app")
vmWatch.names = ['John']
vmWatch.names.push('John') // 添加了deep才会触发
监听器和计算属性
虽然计算属性 computed
和监听属性 watch
都可以监听属性的变化,而后执行一些逻辑处理,但是它们都有各自适用的场合。例如需求是实时地改变 fullName
,我们采用监听器来实现,如示例代码2-3-10所示。
<div id="app">{{ fullName }}</div>
const vm = Vue.createApp({
data() {
return {
firstName: 'Foo',
lastName: 'Bar',
fullName: 'Foo Bar'
}
},
watch: {
firstName (val) {
this.fullName = val + ' ' + this.lastName
},
lastName (val) {
this.fullName = this.firstName + ' ' + val
}
}
}).mount("#app")
在上面的代码中,使用插值表达式在页面上渲染 fullName
的值,想要实现的效果是:当 firstName
的值或者 lastName
的值中有任何一个改变时,就动态地更新 fullName
的值,于是就利用监听属性 watch
来监听 lastName
和 firstName
。
然后运行下面的代码,试一下是否生效:
vm.firstName = 'Petter';
vm.lastName = 'Jackson';
可以看到页面上的 fullName
动态改变了,表明监听属性 watch
可以满足要求。下面接着使用计算属性 computed
来完成这个需求,如示例代码2-3-11所示。
<div id="app">{{ fullName }}</div>
const vm = Vue.createApp({
data() {
return {
firstName: 'Foo',
lastName: 'Bar',
fullName: 'Foo Bar'
}
},
computed: {
fullName () {
return this.firstName + ' ' + this.lastName
}
}
}).mount("#app")
同样,在运行上面的代码之后,可以看到 fullName
也实时更新了。但是,比较这两段代码可以看到,后面的这段代码更加清晰,使用更加合理一些。
所以,对于计算属性 computed
和监听属性 watch
,它们在什么场合使用,以及使用时需要注意哪些地方,应当遵循以下原则:
-
当只需要监听一个定义在
data
中的属性是否变化时,需要在watch
中设置一个同样的属性key
值,然后在watch
对应的function
方法中去执行响应逻辑,而不需要在computed
中另外定义一个值,然后让这个值依赖于在data
中定义的这个属性,这样反倒绕了一圈,代码逻辑结构并不清晰。 -
如果需要监听一个属性的改变,并且在改变的回调方法中有一些异步的操作或者数据量比较大的操作,这时应当使用监听属性
watch
。而对于简单的同步操作,使用计算属性computed
更加合适。 -
当你需要监听单个
data
属性的变化,并且在变化时执行某些逻辑,使用watch
会更简洁清晰。 -
当你需要基于多个
data
中的值计算一个新值,并且该新值是衍生出来的,并且需要缓存时,使用computed
是最佳选择。
建议各位读者在编写相关代码时遵循这样的原则,切勿随意使用。