上一篇《没时间学 Vue (12) —— 组件(二):组件的创建、使用和数据传递》中,我们遇到了 “冻龄” 的问题。
本篇,我们来拆解一下这个问题,并且引入可能的解决方案 —— 计算属性和侦听器。
Vue 官方的介绍在:https://cn.vuejs.org/v2/guide/computed.html。
1、问题的原因
我们在年龄组件 Age.vue 中,是这样计算和显示年龄的 。
貌似年龄只在初始时计算了一次,之后无论生日的数据如何变化都没有更新。
问题有了,先不要着急找解决方案,先来验证一下是不是我们所推测的 “问题” —— 万一不是,那就白忙活了。
我们要怎么验证呢?其实蕴含在我们的推测中,需要把推测按照程序的方式拆解出来。
1)computeAge() 函数只被调用了一次;
2)App.vue 中生日选择控件的数据变更时,年龄组件中的生日数据也会跟着变化。
也就是说,我们只要:
1)增加一个数据,来记录 computeAge() 函数调用次数;
2)把 computeAge() 函数和当前的生日数据显示出来
就可以进行验证了。(等我们之后讲完 “调试” 之后,可以使用更简单的方法进行验证了)
验证的代码大概是这样的:
而验证结果是这样的:(我们的猜测完全正确)
找对问题、找到原因就好办了,接下来我们来解决问题。
2、方法一:侦听器
最容易想到也是最直观的办法是,让年龄跟着生日的变化重新计算 —— 听起来貌似一句废话,这个本来就是我们想要做的效果。
但是这句人能听懂的废话只有拆解成程序能听懂的 “代码”,才能发挥作用。程序想听的大概是这样的:
1)监听 “生日” 的变更事件;
2)“生日” 变更事件里,重新计算 “年龄” 。
为了实现上面的步骤 1),我们需要用到 Vue 提供的 “侦听器”。
侦听器的写法比较特别,需要在 export default 中加入这么一段代码:
watch: {
birthday: function() {
this.age = this.computeAge();
}
}
也就是:
之后,年龄就能跟着生日自动变化了。
上面的代码中,我们只是用到了最基本(也最常用)的侦听处理方式。
比较完整的侦听处理方式的说明在 https://cn.vuejs.org/v2/api/#watch,
而且我们写的侦听处理实际上会被转换成 vm.$watch() 的函数调用( https://cn.vuejs.org/v2/api/#vm-watch)。
完整的处理方式功能强大也比较复杂,先有个印象就行 —— 最好还能记住,监听处理函数是可以带参数的(能知道变更前、变更后的值)。
birthday: function(newValue, oldValue) {
前面那个参数是变更后的值,后面那个参数是变更前的值。
不过要是不用这两个参数的话,就不要写了 —— 不然静态检查的时候会报错,说你画蛇添足、定义了变量却不使用。
3、方法二:计算属性
1)计算属性 —— “计算” 和 “属性” —— 是什么?
先抛开 “计算属性” 这四个字,我们来挖一下使用的上面提到的 “侦听器” 的时候,代码实际上大概是怎么运行的。
1)我们定义了 2 个变量,birthday (生日) 和 age (年龄);
2)当 birthday 发生变化 —— 也就是在设置 birthday 的时候 —— 更新 age 的值。
写成 Java 的话,大概是这样的:(由于没有 “属性” 这个概念,所以我们实际上是折腾的 get/set 函数)
class AgeComponent {
private String birthday; // 生日(可读可写)
private String age; // 年龄(只读)
public String getBirthday() {
return this.birthday;
}
public void setBirthday(String newBirthday) {
this.birthday = newBirthday;
// 更新 this.age
this.age = this.computeAge();
}
public String getAge() {
return this.age;
}
}
要是写成 C# 的话,大概是这样的 —— 没错,C# 是支持 “属性” 的 —— 不过实际上也是 get/set 函数的语法糖。
class AgeComponent {
private String birthday; // “生日” 的成员变量,外部不能直接访问
public String Birthday { // “生日” 的属性
get {
return this.birthday;
}
set {
this.birthday = value;
this.age = this.computeAge();
}
}
private String age; // “年龄” 的成员变量
public String Age { // “年龄” 的属性
get {
return this.age;
}
}
}
哦,不好也就这样,看上去好像没有什么特别的啊?是的 —— 如果上面的场景还比较隐晦的话,我们来看下面这个。
假设现在要做一个跟圆相关的控件,接收的参数是半径,需要输出周长和面积,这个时候会怎么做呢?
Java 地话大概会有以下两种写法:(估计你会比较本能地想到写法二,而不会选择写法一)
换成 C# 的话,则能更直观地看出 “计算” 和 “属性” 是什么意思。
如上图所示,计算属性的特点是:
1)该属性没有对应的成员变量;
2)该属性的值是计算出来的 —— 因此,也不需要监听其他的属性的变更事件、在变更时更新本属性的值。
2)计算属性的写法:
上面拿 Java 和 C# 铺垫了半天,现在终于可以回到 Vue —— 准确的说是 JavaScript —— 上来了。
JavaScript 本身也是支持属性的,用法跟 C# 比较像,只要提供 get 和 set 函数就行了。
比如说,用 JavaScript 来写圆的面积属性的话,大概是这样的:(get/set 的更多内容可以参看:https://zh.javascript.info/property-accessors。)
其实不光 JavaScript 和 C#,Php 和 Python 也支持类似的 get/set 的方式来 “定制” 属性 —— 的读写操作。
再扯远一点儿,虽然 JavaScript 和 Java 的名字很像,语法却大相径庭、完全不是一个妈生的;
如果 C# 学得不错的话,可以用微软出的 TypeScript,跟 C# 比较像,而且可以编译成 JavaScript 。具体可以参照:https://www.tslang.cn/。
应用到 Vue 中的话,要这么来写:
data: function() {
return {
callTimes: 1, // 计算次数
// age: this.computeAge() // 从 data 中挪到 computed 中
}
},
computed: {
age: {
get: function() {
return this.computeAge();
}
}
},
关键的修改如下:(注意结合 JavaScript 的 get/set 属性操作来看)
虽然 JavaScript 的属性支持 get / set 读写两种函数,但是 Vue 计算属性中往往咱们只会指定 get 函数(比如说:会通过半径来计算周长,但是很少通过周长来计算半径)。
此时,计算属性的写法还可以再简化一下,又能少敲几个字符 —— 主要是嵌套层级能少一层,可读性能更好一些。
Vue 计算属性比较完整的资料,可以参照:https://cn.vuejs.org/v2/api/#computed。
当然,还是推荐借此机会也好好看一下 JavaScript 的 get/set:https://zh.javascript.info/property-accessors。
3)计算属性是怎么自动计算的呢?
在惊叹计算属性的简单便利之余,你可能会好奇地问:计算属性是怎么更新的呢?
为什么 birthday 变了之后,age 就会跟着变了呢?谁来自动地调用计算属性的计算操作的呢?
恭喜你,又思考了一个超越 80% 同行的问题。
这个问题很复杂,简单理解的话,就是 Vue 根据 computed 定义、自动创建了一堆侦听器 (vm.$watch) 。
想知道更多的机制的话,就自己搜索吧。https://segmentfault.com/a/1190000016368913 这篇也不错。
4、方法三:纯函数
你可能还会有另一种 “灵感”,就是直接把模板中的 {{age}} 替换成 {{computeAge()}},什么侦听器啊、计算属性啊全部干掉,简单粗暴。
画面效果来看,也实现了年龄跟着生日变化的要求。
但是 —— 敲黑板了哈 —— 注意看上图中的 “计算次数”!
光是初始就要计算 100 多次,生日变更时还要再计算 100 多次,效率是有问题的!
(为什么会调用这么多次呢?这个得深入巴拉 Vue 的底层机制才知道,现在只需要记住这点就行了:不要直接使用函数替代计算属性或者侦听器,没有缓存,要调用很多次,数量有问题)
Vue 官网也专门准备了两个章节,简单做了说明:
Vue 的官网首推计算属性、反对滥用侦听器 —— 主要是从代码的可读性和可维护的角度来考虑的。
实际项目中则两者都会用到,比如说:会监听路由,或者会在监听处理函数中异步做一些处理。
5、思考题
本章的思考题如下:
1)实现计算圆的周长和面积的控件,并在 App.vue 中使用和测试
接收参数:半径,
输出结果:周长
2)实现一个验证码生成器的控件,并在 App.vue 中使用和测试
接收参数:验证码长度 (默认长度为 4 ;验证码长度变化时,需要重新生成验证码)
输出结果:验证码(指定长度的随机生成的数字,需要用到 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Math/random)
控件自带 “重新生成” 按钮,点击后重新生成验证码