1.分析过程
- 问题:做一个定时器,设定3分钟开始,倒计时到0时,发出警告alert
- 分析:
- (1)data存储的数据是不依赖其他数据的,那么什么数据是独立的呢?
- (2)倒计时的过程中,为什么每秒减一呢?
- (3)时间是不是在往前流动呢?
- (4)如果时间不动,那么倒计时是不是就停止了呢?
- (5)综上所述,根本原因就是时间的流动,所以data里面应该放一个时间戳
- (6)除了时间戳,我们还应该放倒计时的初始值(设定值),因为在这个例子中,我们的设定值是非响应的,但是在实际的例子中,一定会让用户自己设定倒计时的事件的,所以我们在此例子中先把它放在data里面
2.利用computed+watch+data实现倒计时
<template>
<div>
<div>{{ countdownHour }}:{{ countdownMinute }}:{{ countdownSecond }}</div>
</div>
</template>
<script>
export default {
data() {
//初始值设置了2个NaN,因为这两个变量都是数字类型的
//NaN是数字类型的,但是它表示不是数字,因为我们还没设置他们的值
return {
// now是实时的时间,也就是流动的时间
now: NaN,
// time是设定的倒计时,如3分钟,换算成和timestamp一样的单位,就是3 * 60e3 = 180000
// 需要自己定义倒计时时间
time:6000,
};
},
created() {
// 先定义一个函数update,即每次更新的函数
// update做了一件事,就是会设置一个定时器,并在1秒钟后递归调用自身,并且将时间更新
//第一种方法:用setTimeout
// const update = () => {
// setTimeout(() => {
// // 因为递归调用自身,所以又设置了另外一个延迟1秒的操作
// update();
// // 当然,这样写有个问题,就是第1秒的时候是now还是NaN
// // 因为要1秒后才会更新this.now
// // 所以在第一秒可以手动设置this.now = Date.now()
// this.now = Date.now();
// }, 1e3);
// };
// update();
//第二种方法:用requeatAnimationFrame
const update = () => {
// 也是循环调用自身,这次会在一开始就更新this.now
this.now = Date.now();
// requestAnimationFrame接受一个参数,这个参数是一个函数
// 因为在update里调用requestAnimationFrame(update),相当于设定下次屏幕刷新前执行一下update
// 那么下次执行update的时候,又会设置下一帧要调用update
//更新的事件由API把握
//requestAnimationFrame在下次屏幕刷新前,所以不同的显示器,刷新率不一样,调用的次数也不同,但是,能保证,每次重绘的时候总是最新的时间
requestAnimationFrame(update);
};
update();
//这里面不管是用了setTimeout还是requestAnimationFrame,都使用了箭头函数,因为this.now中的this需要指向组件实例
},
computed: {
// 结束时间
finishTime() {
//这里并没有用this.now,因为this.now记录的是实时时间
//这里的是设定的时间+设定的倒计时毫秒数
return Date.now() + this.time;
//this.time的变化,触发这个结束的时间,只要this.time不更新,结束时间就不会重新计算,所以结束时间一直固定
},
//剩余的毫秒数
countdown() {
//倒计时不会小于0,也就是说countdown最小值是0
return Math.max(0, this.finishTime - this.now);
},
// 时
countdownHour() {
//padStart是用于补位
return String.prototype.padStart.call(
(this.countdown / 3.6e6) | 0,
2,
"0"
);
},
// 分
countdownMinute() {
return String.prototype.padStart.call(
((this.countdown % 3.6e6) / 6e4) | 0,
2,
"0"
);
},
// 秒
countdownSecond() {
return String.prototype.padStart.call(
((this.countdown % 6e4) / 1e3) | 0,
2,
"0"
);
},
},
// 监听时间到了:
watch: {
countdown(countdown) {
if (0 === countdown) {
alert("时间到了");
}
},
},
};
</script>
3.利用computed+watch+data+filter实现倒计时
<template>
<!-- 2控制的是显示2位,就是过滤器里面的length -->
<div>{{ countdown | display(2) }}</div>
</template>
<script>
export default {
data() {
return {
now: NaN,
time: 12000,
};
},
filters: {
//定义display过滤器,第一个value是输入值,length是过滤器的选项
display(value, length) {
const hour = (value / 3.6e6) | 0,
minute = ((value % 3.6e6) / 6e4) | 0,
second = ((value % 6e4) / 1e3) | 0;
//用length控制补0后的长度
const format = (number) =>
String.prototype.padStart.call(number, length, "0");
return `${format(hour)}:${format(minute)}:${format(second)}`;
},
},
created() {
const update = () => {
this.now = Date.now();
requestAnimationFrame(update);
};
update();
},
computed: {
// 结束时间
finishTime() {
return Date.now() + this.time;
},
//剩余的毫秒数
countdown() {
return Math.max(0, this.finishTime - this.now);
},
},
watch: {
countdown(countdown) {
if (0 === countdown) {
alert("时间到了");
}
},
},
};
</script>
4.在3的基础上,可以继续串联filter进行过滤,把:变成-
<template>
<!-- 2控制的是显示2位,就是过滤器里面的length -->
<div>{{ countdown | display(2)|transform(":", "-") }}</div>
</template>
<script>
export default {
data() {
return {
now: NaN,
time: 12000,
};
},
filters: {
//定义display过滤器,第一个value是输入值,length是过滤器的选项
display(value, length) {
const hour = (value / 3.6e6) | 0,
minute = ((value % 3.6e6) / 6e4) | 0,
second = ((value % 6e4) / 1e3) | 0;
//用length控制补0后的长度
const format = (number) =>
String.prototype.padStart.call(number, length, "0");
return `${format(hour)}:${format(minute)}:${format(second)}`;
},
//pattern是要替换的部分,char是要替换成什么
transform(value, pattern, char) {
return value.replace(new RegExp(pattern, "g"), char);
},
},
created() {
const update = () => {
this.now = Date.now();
requestAnimationFrame(update);
};
update();
},
computed: {
// 结束时间
finishTime() {
return Date.now() + this.time;
},
//剩余的毫秒数
countdown() {
return Math.max(0, this.finishTime - this.now);
},
},
watch: {
countdown(countdown) {
if (0 === countdown) {
alert("时间到了");
}
},
},
};
</script>
5.使用computed和filter的差异(上面2和3方法的差异)
- (5.1) 从上述写法我们可以看到,countdown这个属性是一直在变的,因为now一直在变,而且它变得非常快,不需要1秒那么长才变一次,在1秒内会变动多次,但实际上,我们现实的最大精度到秒就可以了
- (5.2)this.now同步Date.now,所以其实countdown的频繁更新不会体现到视图上,因为countdown在1秒内的变化,视图上其实是没有任何变化的
- (5.3)filter:
- (1)因为template里直接用了countdown,所以只要countdown一变,就会马上重新渲染,这里的重新渲染是指重新生成虚拟DOM
- (2)就是会调用display过滤器,所以如果在display里加了console的话,会发现,这个过滤器被调用得非常频繁
- (3)最终视图没更新,是因为经过过滤器之后,1秒的变化其实没有造成任何变化,过滤器抹平了差异
- (4)最终,虚拟DOM经过对比后发现,不需要重新渲染真实的DOM,才没更新
- (5.4)computed:
- (1)countdown一直在变,所以会造成时分秒这3个属性不断地被计算
- (2)但是时分秒计算后,适合分就变得不那么快了,秒也是这样,只有1秒才变一次
- (3)所以这个时候不会频繁的进行虚拟DOM的重新渲染,只有当变化最快的,也就是秒变化时,才会重新渲染一次
- (4)和filter对比,其更新频率下降了很多,因为减少了虚拟DOM的渲染次数,就减少了虚拟DOM的比较次数,虚拟DOM的新旧比较diff操作也是非常耗资源的
- (5.5)所以在比较虚拟DOM渲染的次数上,更新频率上,使用computed更好一些
- (5.6)既然这样,那么为什么filter比computed好呢?
- 问题在于,我们要显示的是秒,但是变动频繁的是毫秒,是因为这个原因才导致的,如果我们这时候要显示毫秒精度的倒计时,那么肯定filter更好,因为不需要computed这些属性做缓存了
- 总结:
- (1)如果要给数据做变形,如格式化,那么就应该用filter,不要在造出额外的数据了,filter纯粹是为了显示而存在的
- (2)如果要对数据做加工,加工后的数据不是用来显示,而是可能会用在某些地方(可能有些条件,能用或者用不到),那么这是要用computed
6.上述的例子中,有没有办法既用filter又让filter比computed有用呢?
- 看第5点的分析可知,是因为单位不统一造成的,所以我们可以把now改成单位为秒的数值,相应的,其他有关的计算都要改一下
- 而在update这个函数中,我们每次赋值的时候只赋值单位为秒的当前时间,换句话说,2次赋值可能是同一个值,这个没关系,因为set劫持器会对数据比较,如果相同,根本不会触发更新
- 通过这种方式,让filter又重新比computed好用了
<template>
<!-- 2控制的是显示2位,就是过滤器里面的length -->
<div>{{ countdown | display(2) }}</div>
</template>
<script>
export default {
data() {
return {
now: NaN,
time: 60,
};
},
filters: {
display(value, length) {
const hour = (value / 3.6e3) | 0,
minute = ((value % 3.6e3) / 6e1) | 0,
second = ((value % 6e1) / 1e0) | 0;
const format = (number) =>
String.prototype.padStart.call(number, length, "0");
// console.log(`${format(hour)}:${format(minute)}:${format(second)}`)
return `${format(hour)}:${format(minute)}:${format(second)}`;
},
},
created() {
const update = () => {
this.now = (Date.now() / 1e3 | 0);
requestAnimationFrame(update);
};
update();
},
computed: {
// 结束时间
finishTime() {
return (Date.now()/1e3 | 0) + this.time;
},
//剩余的毫秒数
countdown() {
return Math.max(0, this.finishTime - this.now);
},
},
watch: {
countdown(countdown) {
if (0 === countdown) {
alert("时间到了");
}
},
},
};
</script>