1. 计算属性
在上一篇博文中介绍了声明式渲染,我们可以使用 {{ xxx }}
双大括号文本插值的形式进行数据渲染并将数据进行响应式的双向绑定,如上一篇博文的例子:
<div id="app">
<p>{{ message }}p>
<button v-on:click="reverseMessage">反转消息button>
div>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue.js!'
},
methods: {
reverseMessage: function () {
this.message = this.message.split('').reverse().join('');
}}
})
Hello Vue.js!
假如我们点击 反转消息 按钮,
!sj.euV olleH
在这个例子上延伸,假如我们要显示 !sj.euV olleH 呢?可能大家会这样使用模板:
<div id="example">
{{ message.split('').reverse().join('') }}
div>
这个模板便不是简单的声明式逻辑,而是对 message
变量(即 Hello Vue.js! 字符串)执行了 split()
、reverse()
和 join()
三个方法后才能正确地显示我们想要的 !sj.euV olleH 。
然而模板中放入太多地逻辑操作会让模板过重并且难以维护,我们并不能第一时间从表达式中了解到这里要显示的是 message
的反转字符串。如果你想要在多个地方引用 message
反转字符串时,那将变得更难处理。
所以,当我们要对一些变量进行逻辑处理时,我们就要使用计算属性。
1.1 基础例子
当我们知道什么是计算属性后,我们便开始学习如何使用它,以下是一个使用的基础例子:
<div id="example">
<p>message: "{{ message }}"p>
<p>计算属性 reversedMessage: "{{ reversedMessage }}"p>
div>
var vm = new Vue({
el: '#example',
data: {
message: 'Hello Vue.js!'
},
computed: {
// 计算属性的 getter
reversedMessage: function () {
// `this` 指向 vm 实例,`this.message` 即 'Hello Vue.js!' 字符串
return this.message.split('').reverse().join('');
}
}
})
结果:
message: "Hello Vue.js!"
计算属性 reversedMessage: "!sj.euV olleH"
在这个例子中我们声明了一个计算属性 reversedMessage
,它始终取决于 message
的值,所以当我们改变 vm.message
值时:
// 我们在控制台改变 vm.message 的值
console.log(vm.reversedMessage); // => '!sj.euV olleH'
vm.message = 'Good Luck!';
console.log(vm.reversedMessage); // => '!kcuL dooG'
我们可以像绑定普通属性一样在模板中绑定计算属性。Vue 知道 vm.reversedMessage
的值依赖于 vm.message
,因此当 vm.message
发生改变时,所有依赖于它的属性都会更新。而且计算属性的妙处就是我们是以声明的方式创建了这种依赖关系:计算属性的 getter 函数是没有副作用(side effect)的,和普通的属性声明一样比较容易理解。
1.2 计算属性 vs 方法
除了计算属性外,可能大家会想到使用方法实现同样的效果:
<p>计算属性 reversedMessage: "{{ reversedMessage() }}"p>
// 在组件中
methods: {
reversedMessage: function() {
return this.message.split('').reverse().join('');
}
}
我们可以将同一函数定义为一个方法而不是计算属性,两种方式都会得到相同的结果。然而,不同的是计算属性是基于它们响应式依赖进行缓存的。在响应式依赖发生改变时它们才会重新求值。这就意味着只要 message
如果没有发生改变,多次访问 reversedMessage
属性会返回之前的计算结果,不会再次执行函数。
这就意味着下面的计算属性将不会更新,因为 Date.now()
并不是响应式依赖:
computed: {
now: function () {
return Date.now();
}
}
相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。
因此,假设我们有一个逻辑处理比较复杂的计算属性 A,当我们使用计算属性的话,A 会将计算结果进行缓存;如果我们没有缓存,我们将多次执行 A 的 getter,重复计算,尽管结果是一样的,如果我们不希望有缓存,那就可以使用方法代替计算属性。
1.3 计算属性 vs 侦听属性
首先让我们先了解什么是侦听属性:这是一种更通用的方式——来观察和响应 Vue 实例上的数据变动。当你有一些数据需要随着其他数据改变而改变时,我们可能会滥用 watch
—— 特别是使用过 AngularJS 的开发人员。然而,通常更好的方法是使用计算属性而不是命令式的 watch
回调,比如下面这个例子:
<div id="demo">
{{ fullname }}
div>
var vm = new Vue({
el: '#demo',
data: {
firstName: 'Michael',
lastName: 'Jordan',
name: 'Michael Jordan'
},
watch: {
firstName: function(value) {
this.name = value + '' + this.lastName;
},
lastName: function(value) {
this.name = this.firstName + '' + value;
}
}
})
可以看到上面的代码具有较大的重复,当我们拿上面的例子和计算属性进行比较:
var vm = new Vue({
el: '#demo',
data: {
firstName: 'Michael',
lastName: 'Jordan',
},
computed: {
name: function() {
return this.firstName + '' + this.lastName;
}
}
})
同样是 firstName
、lastName
和 name
三个属性,使用计算属性就显得更加简洁易懂了。
1.4 计算属性的 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 = 'Lebron James'
时,setter 会被调用,`
newValuevm.firstName
和
vm.lastName` 也会相应地被更新。
2. 组件化开发
组件系统是 Vue 的一个重要概念,允许我们使用小型、独立和通常可以复用的组件构建大型应用,一个大型应用的应用界面都可以抽象为一个组件树:
通常在 Vue 工程化项目中,会把一个应用分为许多组件,比如我们在下面讲解如何封装一个按钮组件。
2.1 基础例子
// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: `这里是一个按钮标签`
})
组件是可复用的 Vue 实例,且带有一个名字:在这个例子中是 。我们可以在一个通过
new Vue
创建的 Vue 根实例中,把这个组件作为自定义元素来使用:
<div id="components-demo">
<button-counter>button-counter>
div>
new Vue({ el: ' component-demo '})
假如我们点击这个组件按钮,它会自动增加点击的次数 count
。组件与 new Vue
接收相同的选项,例如 data
、computed
、watch
、methods
以及生命周期钩子等。仅有的特例是像 el
这样根示例特有的选项。
2.2 组件的复用
我们可以将组件进行任意次数的复用:
<div id="components-demo">
<button-counter>button-counter>
<button-counter>button-counter>
<button-counter>button-counter>
div>
注意:当我们点击按钮时,每个组件的 count
都是独立的,也就是说,当我点击第一个组件按钮,其他两个组件的 count
属性是不会改变的。因为每使用一次组件,就有一个新的它的实例被创建
2.2.1 data
必须是一个函数
我们看到基础例子中,我们定义这个组件按钮时,data
并不是像我们定义 Vue 实例一样是一个对象,而是一个函数,这又是为什么呢?
由于 JavaScript 语法,如果我们把 data
定义为一个对象,那么当我们对组件进行复用,我们会调用同一个内存空间里的 data
,也就是说:多个组件使用的 data
对象是同一个 data
对象。
那么这会导致什么现象呢?我们还是接着看上一个按钮组件的例子:
假如我们定义组件时,data
是一个对象:
data: {
count: 0
}
当我们点击了 3 个按钮中的任意一个按钮,那么它会出现以下情况:
可以看到三个组件维护的是同一份数据对象,点击一个按钮会影响其他组件实例,这显然与我们想要的是相悖的,因此,一个组件的 data
选项必须是一个函数。
2.3 组件的注册
组件有两种注册类型:全局注册和局部注册。上面个基础例子我们是通过 Vue.component()
全局注册的:
Vue.component('my-conponent', {
// ... 选项 ...
})
全局注册的组件可以用在其被注册之后的任何新创建的 Vue 根实例,也包括其组件树中的所有子组件的模板中。
局部注册我们将在下一篇博文讲解,我们暂时先了解全局注册就足够了。
2.4 通过 Prop 向子组件传递数据
假如我们定义了一个可以复用的新闻组件。但是新闻包括标题、正文等等其他内容,如果我们不向这个新闻组件传递标题、内容或其他数据是无法使用的,这也就是我们为什么要通过 Prop 向子组件传递数据。
Prop 是你可以在组件上注册的一些自定义 attribute。当一个值传递给一个 prop attribute 的时候,它就变成了那个组件实例的一个属性。为了给博文组件传递一个标题,我们可以用一个 props
选项将其包含在该组件可接受的 prop 列表中:
Vue.component('news-article', {
props: [ 'title' ],
template: `这里是标题标签`
})
data
中的值一样使用它。
我们注册了 prop 之后,我们就可以像这样把数据作为一个自定义的 attribute 传递进来:
<news-article title="防护服上的字,句句戳心">news-article>
<news-article title="爱奇艺崩了">news-article>
<news-article title="愿得此身长报国">news-article>
防护服上的字,句句戳心
爱奇艺崩了
愿得此身长报国
2.5 单个根元素
当构建一个新闻组件 时,我们模板上包含的东西远不止一个标题,最起码我们还有正文:
<h3>{{ title }}h3>
<p>{{ content }}p>
然而如果我们在模板中这样写,Vue 会报错,解释道 every component must have a single root element (每个组件必须只有一个根元素)。所以我们可以将模板的内容包裹起来,还是上面的例子:
<div id="news-article">
<h3>{{ title }}h3>
<p>{{ content }}p>
div>
这样就可以修复这个问题了。
以上便是 Vue.js 的计算属性和组件化开发的基础内容,我们将在下一篇博文继续深入地了解组件,包括:
-
组件注册
-
Prop
-
自定义事件
-
...
参考资料:
评论