文章目录
一、响应式基础
1.1. 声明响应式状态
选用响应式API时,会用 data 选项来声明组件的响应式状态。此选项的值应为返回一个对象的函数。Vue将在创建新组件实例的时候调用此函数,并将函数返回的对象用响应式系统进行包装。此对象的所有顶层属性都会被代理到组件实例(即方法和生命周期钩子中的this)上。(若你在method选项的方法中不加 this. 去访问这些属性,你会发现得不到想要的结果)
// js
export default{
data(){
return { //返回对象的return函数,会被响应式系统包装
count: 1
}
}
},
// mounted是生命周期钩子
mounted() {
// this 指向当前组件实例
console.log(this.count) // => 1
// 数据属性也可以被更改
this.count = 2
}
这些实例上的属性仅在实例首次创建时被添加,因此你需要确保它们都出现在 data 函数返回的对象上。若所需的值还未准备好,在必要时也可以使用 null 、undefined 或者 其他一些值占位。
虽然也可以不在 data 上定义,直接向组件实例添加新属性,但这个属性将无法触发响应式更新。
总之,在data的return函数中添加属性是为了它能具有响应式功能。
Vue在组件实例上暴露的内置API使用 $ 作为前缀。它同时也为内部属性保留 _ 前缀。因此,你应该避免在顶层 data 上使用任何以这些字符作前缀的属性。
🔺1.1.1. 响应式代理 vs. 原始值
在 Vue 3 中,数据是基于 JavaScript Proxy(代理) 实现响应式的。使用过 Vue 2的用户要注意下面这样的边界情况:
export default {
data() {
return {
someObject: {}
}
},
mounted() {
const newObject = {}
this.someObject = newObject
console.log(newObject === this.someObject) //false
}
}
当你在赋值后再访问 this.someObject,此值已经是原来的 newObject 的一个响应式代理。月Vue 2不同的是,这里原始的 newObject 不会变为响应式:请确保始终通过 this 来访问响应式状态。
2. 声明方法
要为组件添加方法,我们需要用到 methods 选项。它应该是一个包含所有方法的对象:
// js
export default{
data(){
return {
count: 0
}
},
methods: {
increment(){
this.count++
}
},
mounted(){
// 在其他方法或生命周期中也可以调用方法
this.increment()
}
}
Vue 自动为 methods 中的方法绑定了永远指向组件实例的 this。这确保了方法在作为事件监听器或回调函数时始终保持正确的 this。你不应该在定义 methods 时使用箭头函数,因为箭头函数没有自己的 this 上下文。
// js
export default{
methods: {
increment: ()=>{
// 反例:无法访问此处的 this
}
}
}
和组件实例上的其他属性一样,方法也可以在模板上被访问。在模板中它们常常被用作事件监听器:
<button @click="increment">{{ count }}</button>
increment 方法会在 <button> 被点击时调用。
2.1. DOM更新时机
当你更改响应式状态后,DOM会自动更新。然而,你得注意DOM的更新并不是同步的。相反,Vue将缓冲它们直到更新周期的“下个时机”以确保无论你进行了多少次状态更改,每个组件都只更新一次。
若要等待一个状态改变后的DOM更新完成,你可以使用 nextTick() 这个全局API:
import { nextTick } from 'vue'
export default {
methods: {
increment() {
this.count++
nextTick(() => {
// 访问更新后的DOM
})
}
}
}
2.2. 深层响应性 ✔
在Vue中,状态都是默认深层响应式。这意味着即使在更改深层次的对象或数组,你的改动也能被检测到。(所以深层响应式就是改变顶层元素的内部元素,也会触发响应式)
// js
export default {
data() {
return {
obj: {
nested: { count: 0 },
arr: ['foo', 'bar']
}
}
},
methods: {
mutateDeeply() {
// 以下都会按照期望工作
this.obj.nested.count++
this.obj.arr.push('baz')
}
}
}
你也可以直接创建一个浅层响应式对象。它们仅在顶层具有响应性,一般仅在某些特殊场景中需要。
2.3. 有状态方法 ❓
在某些情况下,我们可能需要动态地创建一个方法函数,比如创建一个预置防抖的事件处理器:
// js
import { debounce } from 'lodash-es'
export default {
methods: {
// 使用 lodash 的防抖函数
click: debounce(function() {
}, 500)
}
}
不过这种方法对于被重用的组件来说是有问题的,因为这个预置防抖的函数是 有状态的 :它在运行时维护着一个内部状态。如果多个组件实例都共享这同一个预置防抖的函数,那么它们之间将互相影响。
从上下文来看,有状态指的是函数内部有需要维护的变量,函数运行可能会改变变量的值。
要保持每个组件实例的防抖函数都彼此独立,我们可以改为在 created 生命周期钩子中创建这个预置防抖的函数:
export default {
created() {
// 每个实例都有了自己的预置防抖的处理函数
this.debouncedClick = _.debounce(this.click, 500)
},
unmounted() {
// 最好是在组件卸载时
// 清除掉防抖计时器
this.debouncedClick.cancel()
},
methods: {
click() {
// ... 对点击的响应 ...
}
}
}
二、计算属性
2.1. 基础示例
模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变的臃肿,难以维护。比如说,我们有这样一个包含嵌套数组的对象:
export default {
data() {
return {
author: {
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
}
}
}
}
我们想根据 author 是否已有一些书籍来展示不同的信息:
<p>Has published books:</p>
<span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span>
这里的模板看起来有些复杂。我们得看一会儿才能明白它的计算依赖于 author.books 。更重要的是,如果在模板中需要不止一次这样的计算,我们可不想将这样的代码在模板里重复好多遍。
因此我们推荐使用 计算属性 来描述依赖响应式状态的复杂逻辑。这是重构后的示例:
export default {
data() {
return {
author: {
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
}
}
},
computed: {
// 一个计算属性的 getter
publishedBooksMessage() {
// this 指向当前组件实例
return this.author.books.length > 0 ? 'Yes' : 'No'
}
}
}
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
我们在这里定义了一个计算属性 publishedBooksMessage。
更改此应用的 data 中 books 数组的值后,可以看到 publishedBooksMessage 也会随之改变。
在模板中使用计算属性的方式和一般的属性并无二致。Vue会检测到 this.publishedBooksMessage 依赖于 this.author.books ,所以当 this.author.books 改变时,任何依赖于 this.publishedBooksMessage 的绑定都将同时更新。
2.2. 计算属性缓存 vs 方法
你可能注意到我们在表达式中像这样调用一个函数也会获得和计算属性相同的结果:
<p>{{ calculateBooksMessage() }}</p>
// 组件中
methos: {
calculateBooksMessage() {
return this.author.books.length > 0 ? 'Yes' : 'No'
}
}
若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。这意味着只要 author.books 不改变,无论多少次访问 publishedBooksMessage 都会立即返回先前的计算结果,而不用重复执行 getter 函数。
这也解释了为什么下面的计算属性永远不会更新,因为 Date.now() 并不是一个响应式依赖:
computed: {
now() {
return Date.now()
}
}
相比之下,方法调用总是会在重渲染发生时再次执行函数。
为什么需要缓存呢?想象一下我们有一个非常耗性能的计算属性 list ,需要循环一个巨大的数组并做许多计算逻辑,并且可能也要其他计算属性依赖于 list 。没有缓存的话,我们会重复执行非常多次 list 的 getter,然而这实际上没有必要!如果你确定不需要缓存,那么也可以使用方法调用。
2.3. 可写计算属性
计算属性默认是只读的。当你尝试修改一个计算属性时,你会受到一个运行时警告。只在某些特殊场景中你可能才需要“可写”的属性,你可以通过同事提供 getter 和 setter 来创建:
export default {
data() {
return {
firstName: 'John',
lastName: 'Doe'
}
},
computed: {
fullName: {
// getter
get() {
return this.firstName + ' ' + this.lastName
},
// setter
set(newValue) {
// 注意:这里使用的是解构赋值语法
[this.firstName, this.lastName] = newValue.split(' ')
}
}
}
}
现在当你再运行 this.fullName = ‘John Doe’ 时,setter 会被调用而 this.firstName 和 this.lastName 会随之更新。
2.4. 最佳实践
2.4.1. Getter不应有副作用
计算属性的 getter 应只做计算而没有任何其他的副作用,这一点非常重要。举例来说,不要在 getter 中做异步请求或者更改DOM!一个计算属性的声明中描述的是如何根据其他值派生一个值。因此 getter 的职责应该仅为计算和返回该值。在之后的指引中我们会讨论如何使用监听器根据其他响应式状态的变更来创建副作用。
2.4.2. 避免直接修改计算属性值
从计算属性返回的值是派生状态。可以把它看作是一个“临时快照”,每当源状态发生变化时,就会创建一个新的快照。更改快照是没有意义的,因为计算属性的返回值应该被视为只读的,并且永远不应该被更改——应该更新它所依赖的源状态以触发新的计算。