计算属性与侦听器
在模板中放入太多的逻辑会让模板过重且难以维护,例如:
<div id="app">
{{msg.split('').reverse().join('')}}
</div>
在这个地方,模板不再是简单的声明式逻辑。你必须看一段时间才能意识到,这里是想要显示变量 msg
的翻转字符串。当你想要在模板中的多处包含此翻转字符串时,就会更加难以处理。
所以,对于任何复杂逻辑,你都应当使用计算属性。
基础例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<style></style>
</head>
<body>
<div id="app">
{{reverseMsg}}
</div>
<script>
const app = new Vue({
el: "#app",
data: {
msg: "你好",
},
computed: {
// 计算属性的 getter
reverseMsg:function(){
// `this` 指向 app 实例
return this.msg.split('').reverse().join('')
}
},
});
</script>
</body>
</html>
这里声明了一个计算属性reverseMsg
,我们提供的函数用作propertyapp.reverseMsg
的getter函数
console.log(app.reverseMsg) // => 'olleH'
app.msg = 'Goodbye'
console.log(app.reverseMsg) // => 'eybdooG'
app.reverseMsg
的值取决于app.msg
的值。可以像绑定普通property一样在模板中绑定计算属性。Vue知道app.reverseMsg
依赖于app.msg
,所以当后者更新,前者也会更新。
计算属性缓存 vs 方法
可以注意到通过表达式中调用方法可以达到同样的效果
<p>Reversed message: "{{ reverseMsg() }}"</p>
// 在组件中
methods: {
reverseMsg: function () {
return this.msg.split('').reverse().join('')
}
}
两种方式的最终结果是完全相同的。然而,不同的是计算属性是基于它们的响应式依赖进行缓存的。意味着只要msg
还未改变,那么多次访问计算属性会立即返回之前计算的结果。
这也同样意味着下面的计算属性将不再更新,因为 Date.now()
不是响应式依赖:
computed: {
now: function () {
return Date.now()
}
}
计算属性 vs 侦听属性
Vue 提供了一种更通用的方式来观察和响应 Vue 实例上的数据变动:侦听属性。当你有一些数据需要随着其它数据变动而变动时,你很容易滥用 watch
——特别是如果你之前使用过 AngularJS。然而,通常更好的做法是使用计算属性而不是命令式的 watch
回调。细想一下这个例子:
<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
}
}
})
上面代码是命令式且重复的。将它与计算属性的版本进行比较:
var vm = new Vue({
el: '#demo',
data: {
firstName: 'Foo',
lastName: 'Bar'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})
计算属性的setter
计算属性默认只有 getter,不过在需要时你也可以提供一个 setter:
// ...
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.firstName
和 vm.lastName
也会相应地被更新。
侦听器
Vue通过watch
选项提供了一个通用的方法来响应数据的变化,当需要在数据变化时执行异步或开销较大的操作时,这个方法最有用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<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>
<style></style>
</head>
<body>
<div id="watch-example">
<p>
Ask a yes/no question:
<input v-model="question" />
</p>
<p>{{ answer }}</p>
</div>
<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>
</body>
</html>
在这个示例中,使用 watch
选项允许我们执行异步操作 (访问一个 API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
Class与Style绑定
绑定 HTML Class
操作元素的 class 列表和内联样式是数据绑定的一个常见需求。因为它们都是 attribute,所以我们可以用 v-bind
处理它们:只需要通过表达式计算出字符串结果即可。不过,字符串拼接麻烦且易错。因此,在将 v-bind
用于 class
和 style
时,Vue.js 做了专门的增强。表达式结果的类型除了字符串之外,还可以是对象或数组。
对象语法
可以传给v-bind:class
一个对象,动态地切换class:
<div v-bind:class="{ active: isActive }"></div>
上面的语法表示active
这个class存在与否将取决于数据propertyisActive
的真值。
可以在对象中传入更多字段来动态切换多个class。此外,v-bind:class
指令也可以与普通的class attribute共存。当有如下模板:
<div class="static" v-bind:class="{ active: isActive, 'text-danger': hasError }"></div>
和如下data:
data: {
isActive: true,
hasError: false
}
结果渲染为:
<div class="static active"></div>
当 isActive
或者 hasError
变化时,class 列表将相应地更新。例如,如果 hasError
的值为 true
,class 列表将变为 "static active text-danger"
。
绑定的数据对象不必内联定义在模板里:
<div v-bind:class="classObject"></div>
data: {
classObject: {
active: true,
'text-danger': false
}
}
渲染的结果和上面一样。我们可以在这里绑定一个返回对象的计算属性。这是一个常用且强大的模式:
<div v-bind:class="classObject"></div>
data: {
isActive: true,
error: null
},
computed: {
classObject: function () {
return {
active: this.isActive && !this.error,
'text-danger': this.error && this.error.type === 'fatal'
}
}
}
数组语法
我们可以把一个数组传给v-bind:class
,以应用一个列表:
<div v-bind:class="[activeClass, errorClass]"></div>
data: {
activeClass: 'active',
errorClass: 'text-danger'
}
渲染为:
<div class="active text-danger"></div>
如果你也想根据条件切换列表中的 class,可以用三元表达式:
<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>
这样写将始终添加 errorClass
,但是只有在 isActive
是 truthy时才添加 activeClass
。
不过,当有多个条件 class 时这样写有些繁琐。所以在数组语法中也可以使用对象语法:
<div v-bind:class="[{ active: isActive }, errorClass]"></div>
用在组件上
当一个自定义组件上使用class
property时,这些class将被添加到该组件的根元素上面,这个元素上已经存在的class不会被覆盖。
例如,如果你声明了这个组件:
Vue.component('my-component', {
template: '<p class="foo bar">Hi</p>'
})
在使用它的时候添加一些class:
<my-component class="baz boo"></my-component>
HTML将被渲染为:
<p class="foo bar baz boo">Hi</p>
对于带数据绑定 class 也同样适用:
<my-component v-bind:class="{ active: isActive }"></my-component>
当 isActive
为 truthy[1] 时,HTML 将被渲染成为:
<p class="foo bar active">Hi</p>
绑定内联样式
对象语法
v-bind:style
的对象语法像CSS,但其实是一个JavaScript对象。CSS property名可以用驼峰式或短横线分隔 (kebab-case,记得用引号括起来) 来命名:
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
data: {
activeColor: 'red',
fontSize: 30
}
直接绑定到一个样式对象通常更好,这会让模板更清晰:
<div v-bind:style="styleObject"></div>
data: {
styleObject: {
color: 'red',
fontSize: '13px'
}
}
同样的,对象语法常常结合返回对象的计算属性使用。
数组语法
v-bind:style
的数组语法可以将多个样式对象应用到同一个元素上:
<div v-bind:style="[baseStyles, overridingStyles]"></div>
自动添加前缀
当 v-bind:style
使用需要添加浏览器引擎前缀的 CSS property 时,如 transform
,Vue.js 会自动侦测并添加相应的前缀。
多重值
从 2.3.0 起你可以为 style
绑定中的 property 提供一个包含多个值的数组,常用于提供多个带前缀的值,例如:
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
这样写只会渲染数组中最后一个被浏览器支持的值。在本例中,如果浏览器支持不带浏览器前缀的 flexbox,那么就只会渲染 display: flex
。
条件渲染
v-if
v-if
指令用于条件性地渲染一块内容,这块内容只会在指令的表达式返回 truthy 值的时候被渲染。
<h1 v-if="awesome">Vue is awesome!</h1>
也可以用 v-else
添加一个“else 块”:
<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>
在<template>
元素上使用v-if
条件渲染分组:
因为 v-if
是一个指令,所以必须将它添加到一个元素上。但是如果想切换多个元素呢?此时可以把一个 <template>
元素当做不可见的包裹元素,并在上面使用 v-if
。最终的渲染结果将不包含 <template>
元素。
<template v-if="ok">
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</template>
v-else
你可以使用 v-else
指令来表示 v-if
的“else 块”:
<div v-if="Math.random() > 0.5">
Now you see me
</div>
<div v-else>
Now you don't
</div>
v-else
元素必须紧跟在带 v-if
或者 v-else-if
的元素的后面,否则它将不会被识别。
v-else-if
v-else-if
,顾名思义,充当 v-if
的“else-if 块”,可以连续使用:
<div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else>
Not A/B/C
</div>
类似于 v-else
,v-else-if
也必须紧跟在带 v-if
或者 v-else-if
的元素之后。
用key管理可复用的元素
Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。这么做除了使 Vue 变得非常快之外,还有其它一些好处。例如,如果你允许用户在不同的登录方式之间切换:
<template v-if="loginType === 'username'">
<label>Username</label>
<input placeholder="Enter your username">
</template>
<template v-else>
<label>Email</label>
<input placeholder="Enter your email address">
</template>
那么在上面的代码中切换 loginType
将不会清除用户已经输入的内容。因为两个模板使用了相同的元素,<input>
不会被替换掉——仅仅是替换了它的 placeholder
。
这样也不总是符合实际需求,所以 Vue 为你提供了一种方式来表达“这两个元素是完全独立的,不要复用它们”。只需添加一个具有唯一值的 key
attribute 即可:
<template v-if="loginType === 'username'">
<label>Username</label>
<input placeholder="Enter your username" key="username-input">
</template>
<template v-else>
<label>Email</label>
<input placeholder="Enter your email address" key="email-input">
</template>
注意,<label>
元素仍然会被高效地复用,因为它们没有添加 key
attribute。
v-show
另一个用于根据条件展示元素的选项是v-show
指令。用法大致一样:
<h1 v-show="ok">Hello!</h1>
不同的是带有 v-show
的元素始终会被渲染并保留在 DOM 中。v-show
只是简单地切换元素的 CSS property display
。