Skip to content

计算属性

模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护。例如:

javascript
<div id="example">
  {{ message.split('').reverse().join('') }}
</div>

在这个地方,模板不再是简单的声明式逻辑。你必须看一段时间才能意识到,这里是想要显示变量 message 的翻转字符串。当你想要在模板中的多处包含此翻转字符串时,就会更加难以处理。所以,对于任何复杂逻辑,你都应当使用计算属性。

javascript
* 相当于React 当中的 useMemo 方法。
* 计算属性的本质是一个函数,但是在模板中要作为(函数的结果)属性来使用。
* 可以使用实例中的属性进行复杂的逻辑运算,并将结果进行缓存。
* 如果影响结果的实例属性发生改变,那么计算属性函数会重新计算结果,如果未改变会从缓存中提取结果。
* 在什么时候用:当逻辑复杂,结果受多个数据状态影响且需要缓存时使用。
* 计算属性的值会被缓存,只有实例中相关依赖值改变时,才重新计算,性能好但不适合做异步请求。
* 计算属性是声明式的描述一个值依赖了其他值,依赖的值改变后重新计算结果更新DOM。属性监听的是定义的变量,当定义的值发生变化时,执行相对应的函数。

基本使用

使用插值表达式

javascript
<div id="example">
  {{ message.split('').reverse().join('') }}
</div>

使用计算属性

javascript
<div id="example">
  <p>Original message: "{{ message }}"</p>
  <p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
javascript
var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // 计算属性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 实例 
      return this.message.split('').reverse().join('')
    }
  }
})

这里我们声明了一个计算属性 reversedMessage。我们提供的函数将用作 property vm.reversedMessage 的 getter 函数:
console.log(vm.reversedMessage) // => 'olleH'
vm.message = 'Goodbye'
console.log(vm.reversedMessage) // => 'eybdooG'

你可以打开浏览器的控制台,自行修改例子中的 vm。vm.reversedMessage 的值始终取决于 vm.message 的值。

你可以像绑定普通 property 一样在模板中绑定计算属性。Vue 知道 vm.reversedMessage 依赖于 vm.message,因此当 vm.message 发生改变时,所有依赖 vm.reversedMessage 的绑定也会更新。而且最妙的是我们已经以声明的方式创建了这种依赖关系:计算属性的 getter 函数是没有副作用 (side effect) 的,这使它更易于测试和理解。

计算属性缓存 vs 方法

你可能已经注意到我们可以通过在表达式中调用方法来达到同样的效果

javascript
<p>Reversed message: "{{ reversedMessage() }}"</p>

// 在组件中
methods: {
  reversedMessage: function () {
    // 如果函数设置为箭头函数,那么this指向的是window,这里指向的是vm实例
    return this.message.split('').reverse().join('')
  }
}

我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要 message 还没有发生改变,多次访问 reversedMessage 计算属性会立即返回之前的计算结果,而不必再次执行函数。

这也同样意味着下面的计算属性将不再更新,因为 Date.now() 不是响应式依赖:

javascript
computed: {
  now: function () {
    return Date.now()
  }
}

相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。

我们为什么需要缓存?假设我们有一个性能开销比较大的计算属性 A,它需要遍历一个巨大的数组并做大量的计算。然后我们可能有其他的计算属性依赖于 A。如果没有缓存,我们将不可避免的多次执行 A 的 getter!如果你不希望有缓存,请用方法来替代。

计算属性的 setter

计算属性默认只有 getter(读取计算属性),不过在需要时(修改计算属性)你也可以提供一个 setter:

javascript
// ...
computed: {
  fullName: {
    // getter
    get: function () {
      return this.firstName + ' ' + this.lastName
    },
    // setter
    set: function (newValue) {
      var names = newValue.split(' ')
      this.firstName = names[0]
      this.lastName = names[names.length - 1]
    }
  }
}
// ...

现在再运行 vm.fullName = 'John Doe' 时,setter 会被调用,vm.firstNamevm.lastName 也会相应地被更新。

侦听器watch

虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch 选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。

侦听String

侦听数据类型是一个String

javascript
<div id="watch-example">
  <p>
    Ask a yes/no question:
    <input v-model="question">
  </p>
  <p>{{ answer }}</p>
</div>
javascript
<!-- 因为 AJAX 库和通用工具的生态已经相当丰富,Vue 核心代码没有重复 -->
<!-- 提供这些功能以保持精简。这也可以让你自由选择自己更熟悉的工具。 -->
<script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js"></script>
<script>
var watchExampleVM = new Vue({
  el: '#watch-example',
  data: {
    question: '',
    answer: 'I cannot give you an answer until you ask a question!'
  },
  watch: {
    // 如果 `question` 发生改变,这个函数就会运行
    question: function (newQuestion, oldQuestion) {
      this.answer = 'Waiting for you to stop typing...'
      this.debouncedGetAnswer()
    }
  },
  created: function () {
    // `_.debounce` 是一个通过 Lodash 限制操作频率的函数。
    // 在这个例子中,我们希望限制访问 yesno.wtf/api 的频率
    // AJAX 请求直到用户输入完毕才会发出。想要了解更多关于
    // `_.debounce` 函数 (及其近亲 `_.throttle`) 的知识,
    // 请参考:https://lodash.com/docs#debounce
    this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)
  },
  methods: {
    getAnswer: function () {
      if (this.question.indexOf('?') === -1) {
        this.answer = 'Questions usually contain a question mark. ;-)'
        return
      }
      this.answer = 'Thinking...'
      var vm = this
      axios.get('https://yesno.wtf/api')
        .then(function (response) {
          vm.answer = _.capitalize(response.data.answer)
        })
        .catch(function (error) {
          vm.answer = 'Error! Could not reach the API. ' + error
        })
    }
  }
})
</script>

在这个示例中,使用 watch 选项允许我们执行异步操作 (访问一个 API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

除了 watch 选项之外,您还可以使用命令式的 vm.$watch API

示例2

javascript
data: {
    
    str:"I am ironMan",
    obj:{
         userName:"",
         age:
    },
    arr:[]
},
method:{
    changeNum() {
		this.num++;
		this.a = this.num + '!'
	},
}
watch:{
    // 写法1,不指定其他选项
    str(newValue,oldValue){
        console.log("num侦听函数变量",newValue,oldValue);
        console.log('this的指向',this); // 指向Vue实例
    },
    // 写法2,指定其他选项
    // handler 句柄函数:作用是要干什么
    str:{
        handler(newValue,oldValue){
            console.log("str",newValue,oldValue)
        },
        immediate:true
    }
}
html
<input type="text" v-model="str">

侦听Number

当num值发生改变,会执行该函数。

javascript
参数:接收两个参数:newValue(修改后的值),oldValue(修改前的值)
函数中的this指向的是Vue实例
javascript
data: {
    num: 445,
    str:"",
    obj:{
         userName:"",
         age:
    },
    arr:[]
},
method:{
    changeNum() {
		this.num++;
		this.a = this.num + '!'
	},
}
watch:{
    num(newValue,oldValue){
        console.log("num侦听函数变量",newValue,oldValue);
        console.log('this的指向',this); // 指向Vue实例
    }
}
html
<button @click="changeNum">{{num}}</button>
<button @click="num++;a=num+'!'">{{num}}</button>

侦听Array

Array数组的引用地址发生改变 watch才能侦听到,或使用实例全局方法vm.$set()设置原型链上的数据

javascript
data: {
    num: 445,
    str:"",
    obj:{
         userName:"",
         age:
    },
    arr:[1,2,3,4]
},
methods:{
    changeArr(){
        // 添加一个元素,可以侦听到,响应式
        this.arr.push(this.arr.length+1);
        // 更改某一个元素:不支持响应式,也不支持侦听(包括深度侦听也不支持)。
        this.arr[3] = 100;
        // 解决方案一:通过更改数组的引用地址
        this.arr = this.arr.map((item,index)=>{
            if(index===3) item = 100;
            return item;
        })
        // 解决方案二:通过实例中的$set方法。
        this.$set(this.arr,3,100);
    }
}

watch:{
    // 写法1 函数形式不指定选项
    arr(newValue,oldValue){
		console.log("arr",newValue,oldValue);
	}
    // 写法2 对象形式指定选项
	arr:{
		handler(newValue,oldValue){
			console.log("arr",newValue,oldValue);
		},
		deep:true
	}
}
html
<button @click="changeArr">更改</button>
<p>{{arr}}</p>

侦听Function

侦听的数据类型是函数

javascript
data: {
    num: 445,
    str:"",
    obj:{
         userName:"",
         age:
    },
    arr:[1,2,3,4]
},
methods:{
    changeObj(){
        // 修改方式1 重新设置对象的属性
		this.obj.userName="lisi";
		this.obj.age = 100;
        // 修改方式2 重新给对象的属性赋值
		this.obj = {
			userName:"lisi",
			age:100
		}
	}
},
watch:{
    firstName(){
		this.fullName = this.firstName+this.lastName;
	},
	lastName(){
		this.fullName = this.firstName+this.lastName;
	}
},
computed:{
	fullName2(){
		return this.firstName+this.lastName;
	}
}

侦听Object

侦听的数据类型是Onject时

html
<input type="text" v-model="obj.userName">
<input type="text" v-model.number="obj.age">
<button @click="changeObj">更改obj</button>
<p>{{obj}}</p>
javascript
data: {
    num: 445,
    str:"",
    obj:{
         userName:"",
         age:
    },
    arr:[1,2,3,4]
},
methods:{
    changeObj(){
        // 修改方式1 重新设置对象的属性
		this.obj.userName="lisi";
		this.obj.age = 100;
        // 修改方式2 重新给对象的属性赋值
		this.obj = {
			userName:"lisi",
			age:100
		}
	}
}

watch:{
    // 如果对象的引用地址未发生更改,仅更改对象的属性值,写法1 无法进行侦听
    // 写法1 函数形式不指定 选项
    obj(newValue,oldValue){
		console.log("obj",newValue,oldValue);
	}
    
    // 写法2 对象形式指定 deep选项
	obj:{
		handler(newValue,oldValue){
			console.log("arr",newValue,oldValue);
		},
		deep:true
	}
    // 写法3 侦听对象的具体某个属性
    "obj.userName"(newValue,oldValue){
		console.log("obj.userName",newValue,oldValue);
	},
}

实例方法vm.$watch

javascript
vm.$watch( expOrFn, callback, [options] )
参数:
{string | Function} expOrFn
{Function | Object} callback
{Object} [options]
{boolean} deep
{boolean} immediate
返回值:{Function} unwatch

观察 Vue 实例上的一个表达式或者一个函数计算结果的变化。回调函数得到的参数为新值和旧值。表达式只接受简单的键路径。对于更复杂的表达式,用一个函数取代。

注意:在变更 (不是替换) 对象或数组时,旧值将与新值相同,因为它们的引用指向同一个对象/数组。Vue 不会保留变更之前值的副本。

javascript
// 键路径
vm.$watch('a.b.c', function (newVal, oldVal) {
  // 做点什么
})

// 函数
vm.$watch(
  function () {
    // 表达式 `this.a + this.b` 每次得出一个不同的结果时
    // 处理函数都会被调用。
    // 这就像监听一个未被定义的计算属性
    return this.a + this.b
  },
  function (newVal, oldVal) {
    // 做点什么
  }
)

vm.$watch 返回一个取消观察函数,用来停止触发回调:

javascript
var unwatch = vm.$watch('a', cb)
// 之后取消观察
unwatch()

选项:deep

为了发现对象内部值的变化,可以在选项参数中指定 deep: true。注意监听数组的变更不需要这么做。

javascript
vm.$watch('someObject', callback, {
  deep: true
})
vm.someObject.nestedValue = 123
// callback is fired

选项:immediate

在选项参数中指定 immediate: true 将立即以表达式的当前值触发回调:

默认值false,只有据发生改变后才会执行

javascript
vm.$watch('a', callback, {
  immediate: true // 代表立即执行,且数据发生改变会再次执行
    
})
// 立即以 `a` 的当前值触发回调

注意在带有 immediate 选项时,你不能在第一次回调时取消侦听给定的 property。

javascript
// 这会导致报错
var unwatch = vm.$watch(
  'value',
  function () {
    doSomething()
    unwatch()
  },
  { immediate: true }
)

如果你仍然希望在回调内部调用一个取消侦听的函数,你应该先检查其函数的可用性:

javascript
var unwatch = vm.$watch(
  'value',
  function () {
    doSomething()
    if (unwatch) {
      unwatch()
    }
  },
  { immediate: true }
)

计算属性 vs 侦听属性

javascript
侦听与计算属性的区别:
1- 侦听是通过watch声明,计算属性是通过computed声明
2- 侦听不需要返回值,计算属性要有返回值。
3- 侦听默认不会立即调用。计算属性在模板中调用才会执行
4- 侦听没有缓存,计算属性有缓存。
5- 侦听的应用:侦听某一数据发生改变后的一个行为。计算属性:受多个属性的变化而变化。
6- 侦听支持异步,计算属性不支持异步。

Vue 提供了一种更通用的方式来观察和响应 Vue 实例上的数据变动:侦听属性。当你有一些数据需要随着其它数据变动而变动时,你很容易滥用 watch——特别是如果你之前使用过 AngularJS。然而,通常更好的做法是使用计算属性而不是命令式的 watch 回调。细想一下这个例子:

javascript
<div id="demo">{{ fullName }}</div>

var vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar',
    fullName: 'Foo Bar'
  },
  watch: {
    firstName: function (val) {
      this.fullName = val + ' ' + this.lastName
    },
    lastName: function (val) {
      this.fullName = this.firstName + ' ' + val
    }
  }
})

上面代码是命令式且重复的。将它与计算属性的版本进行比较:

javascript
var vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar'
  },
  computed: {
    fullName: function () {
      return this.firstName + ' ' + this.lastName
    }
  }
})