背景
最近在开发中需要用到一个倒计时的组件,本着cv工程师的行事原则,自然是百度网罗天下英才的成果,最终找到一个利用插槽实现的倒计时组件,感觉非常nice,之前也一直没有好好了解过vue插槽,固特意借此机会学习一下,提升一下自己cv能力。
vue插槽
vue插槽是vue实现的一种内容分发的API,将slot元素作为分发内容的出口。(这句话来自官方文档)
作用:
-
父组件向子组件传递内容;
-
拓展、复用、定制组件;
分类:
-
默认插槽
-
具名插槽
-
作用域插槽
下面一一看一下吧:
默认插槽
先来个子组件吧:
<template>
<div class="child">
<h1>这是子组件</h1>
<slot></slot>
</div>
</template>
再来个父组件:
<template>
<div class="home">
<h1>这是父组件</h1>
<slotchild>
这是在父组件中的一句话
</slotchild>
</div>
</template>
子组件中的slot就是默认插槽; 在父组件引用子组件,然后在子组件标签中间写上希望传到子组件的东西。当组件渲染的时候,
<slot></slot>
将会被替换为:当组件渲染的时候,<slot></slot>
将会被替换为:这是在父组件中的一句话。如下图:
这就是默认插槽。
具名插槽
顾名思义就是有名字的插槽。有时我们需要多个插槽,例如:
<div class="container">
<header> <!-- 我们希望把页头放这里 --> </header>
<main> <!-- 我们希望把主要内容放这里 --> </main>
<footer> <!-- 我们希望把页脚放这里 --> </footer>
</div>
这个时候使用默认插槽是做不到的。对于这样的情况,<slot>
元素有一个特殊的 attribute:name
。这个 attribute 可以用来定义额外的插槽:也就是具名插槽:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
一个不带
name
的<slot>
插槽会带有隐含的名字“default”。
在向具名插槽提供内容的时候,我们可以在一个 <template>
元素上使用 v-slot
指令,并以 v-slot
的参数的形式提供其名称:
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>
现在 <template>
元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带有 v-slot
的 <template>
中的内容都会被视为默认插槽的内容。
当然默认插槽我们也可以带上他的名字default
<template v-slot:default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
其渲染效果等同于
<div class="container">
<header>
<h1>Here might be a page title</h1>
</header>
<main>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</main>
<footer>
<p>Here's some contact info</p>
</footer>
</div>
注意
v-slot
只能添加在<template>
上
作用域插槽
有时候我们想在父组件访问子组件中才有的数据,这时就要用到作用域插槽了。
例如,设想一个带有如下模板的 <current-user>
组件:
<span>
<slot>{{ user.lastName }}</slot>
</span>
我们可能想换掉备用内容,用名而非姓来显示。如下:
<current-user>
{{ user.firstName }}
</current-user>
然而上述代码不会正常工作,因为只有 <current-user>
组件可以访问到 user
,而我们提供的内容是在父级渲染的。
为了让 user
在父级的插槽内容中可用,我们可以将 user
作为 <slot>
元素的一个 attribute 绑定上去:
<span>
<slot v-bind:user="user">
{{ user.lastName }}
</slot>
</span>
绑定在 <slot>
元素上的 attribute 被称为插槽 prop。现在在父级作用域中,我们可以使用带值的 v-slot
来定义我们提供的插槽 prop 的名字:
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
</current-user>
在这个例子中,我们选择将包含所有插槽 prop 的对象命名为 slotProps
,但你也可以使用任意你喜欢的名字。
好了,抄完了也看完了;下面放个使用例子吧(也是网上扒的)倒计时组件
<template>
<div class="content">
<!-- slot v-bind 将子组件数据传给父组件, 可以选择要显示的内容,例如只显示秒,或者只显示小时,
只需要用插槽,就把倒计时组件,也就是把子组件的值传递给父组件了-->
<!-- <slot v-bind="{
d: days,
h: hours,
m: mins,
s: seconds,
hh: `00${hours}`.slice(-2),
mm: `00${mins}`.slice(-2),
ss: `00${seconds}`.slice(-2)
}"></slot> -->
<slot v-bind:timecutdown="{
d: days,
h: hours,
m: mins,
s: seconds,
hh: `00${hours}`.slice(-2),
mm: `00${mins}`.slice(-2),
ss: `00${seconds}`.slice(-2)
}" name="timecutdown"></slot>
</div>
</template>
<script>
export default {
name: 'BaseCounter',
props: {
// 后台返回的时间戳
time: {
type: Number | String,
default: 0
},
refreshCounter: {
type: Number | String,
default: 0
},
// 到期时间
end: {
type: Number | String,
default: 0
},
// 区分传入的事秒还是毫秒
isMiniSecond: {
type: Boolean,
default: false
}
},
computed: {
// 将获取到的时候进行转化,不管time是毫秒还是秒都转化成秒
// 「+」’号。接口返回的一串数字有时候是字符串的形式,有时候是数字的形式(~不能过分相信后端同学,必须自己做好防范~)。所以通过前面加个‘「+」’号 通通转化为数字。
duration() {
// 处理传入到期时间
if (this.end) {
let end = String(this.end).length >= 13 ? +this.end : +this.end * 1000
end -= Date.now()
return end
}
// 处理入剩余时间
const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time)
return time
}
},
data() {
return {
days: '0',
hours: '00',
mins: '00',
seconds: '00',
timer: null,
curTime: 0 // 当前的时刻,也就是显示在页面上的那个时刻
}
},
methods: {
// 将duration转化成天数,小时,分钟,秒数的方法
durationFormatter(time) {
if (!time) return { ss: 0 };
let t = time;
const ss = t % 60;
t = (t - ss) / 60;
if (t < 1) return { ss };
const mm = t % 60
t = (t - mm) / 60
if (t < 1) return { mm, ss }
const hh = t % 24
t = (t - hh) / 24
if (t < 1) return { hh, mm, ss }
const dd = t
return { dd, hh, mm, ss }
},
// 开始执行倒计时的方法
countDown() {
// 记录下当前时间
this.curTime = Date.now()
this.getTime(this.duration)
},
// 倒计时方法
getTime(time) {
this.timer && clearTimeout(this.timer)
if (time < 0) return
const { dd, hh, mm, ss } = this.durationFormatter(time)
this.days = dd || 0
this.hours = hh || 0
this.mins = mm || 0
this.seconds = ss || 0
this.timer = setTimeout(() => {
/*
出于节能的考虑, 部分浏览器在进入后台时(或者失去焦点时), 「会将 setTimeout 等定时任务暂停 待用户回到浏览器时, 才会重新激活定时任务」
说是暂停, 其实应该说是延迟, 1s 的任务延迟到 2s, 2s 的延迟到 5s, 实际情况因浏览器而异。
原来如此,看来不能每次都只是减1这么简单了(毕竟你把浏览器切到后台之后setTimeout就冷却了,等几秒后切回,然后执行setTimeout,只是减了一秒而已)。
*/
// now 是 setTimeout的回调函数执行的时候的那个时刻。记录当前这个setTimeout的回调函数执行的时间点。
const now = Date.now()
// 当前这个setTimeout的回调函数执行的时刻距离上 页面上的剩余时间上一次变化的时间段」。其实也就是 「当前这个setTimeout的回调函数执行的时刻距离上 一个setTimeout的回调函数执行的时刻时间段。」
// 记录当前这个setTimeout的回调函数执行的时间点距离页面上开始 渲染 剩余时间的 这一段时间。其实此时的diffTime就是=1。
const diffTime = Math.floor((now - this.curTime) / 1000)
// 在手机端页面回退到后台的时候不会计时,对比时间差,大于1s的重置倒计时
const step = diffTime > 1 ? diffTime : 1
// 将curTime的值变成当前这个setTimeout的回调函数执行的时间点。
this.curTime = now
this.getTime(time - step)
}, 1000)
}
},
mounted() {
this.countDown()
},
watch: {
duration() {
this.countDown()
},
refreshCounter() {
this.countDown()
}
}
}
/*
// 原创连接https://mp.weixin.qq.com/s/Edk-0pVDZWOkkfZ2mPiCnw
总结:
1、 为什么要「用setTimeout来模拟setInterval的行为」?
可以看看setInterval有什么缺点:
定时器指定的时间间隔,表示的是何时将定时器的代码添加到消息队列,而不是合适执行代码,所以真正何时执行代码的时间是不能保证的,而是取决于何时被主线程的事件循环取到并执行。
setInterval(fun, n) // 每隔n秒把fun事件推到消息队列中;
setInterval有两个缺点:(1)使用setInterval时,某些间隔会被跳过;(2)可能有多个定时器会连续执行;
可以这么理解:每个setTimeout产生的任务会直接push到任务队列中,而setInterval在每次把任务push到任务队列前,都要进行一下判断看上次的任务是否仍在队列中;因而采用setTimeout来规避上面的缺点。
2、为什么要clearTimeout(this.timer)
假设现在页面显示的是活动一的时间,这时,执行到setTimeout,在「一秒后」就会把setTimeout里的回调函数放到任务队列中,「注意是一秒后哦」!这时,然而,在这一秒的开头,我们点击了活动二按钮,这时候的活动二的时间就会传入倒计时组件中,然后触发countDown(),也就调用this.getTime(this.duration);,然后执行到setTimeout,也会一秒后把回调函数放到任务队列中。
这时,任务队列中就会有两个setTimeout的回调函数了。等待一秒过去,两个回调函数相继执行,我们就会看到页面上的时间一下子背减了2,实际上是很快速地进行了两遍减1的操作。
这就是为什么要添加上this.timer && clearTimeout(this.timer);这一句的原因了。就是要把上一个setTimeout清除掉。
*/
</script>
父组件中使用:
<template>
<div class="home">
<h1>{{ msg }}</h1>
<slotchild :end="countDown" :isMiniSecond="false">
<template v-slot:timecutdown="timeObj">
<span>
{{timeObj.timecutdown.d}}天{{timeObj.timecutdown.hh}}小时{{timeObj.timecutdown.mm}}分钟{{timeObj.timecutdown.ss}}秒
</span>
</template>
</slotchild>
<slotchild v-slot="timeObj" :time="60" :isMiniSecond="false">
<span>
{{timeObj.d}}天{{timeObj.hh}}小时{{timeObj.mm}}分钟{{timeObj.ss}}秒
</span>
</slotchild>
<slotchild v-slot="timeObj" :time="100 * 1000" :isMilliSecond="true">
<span>
{{timeObj.d}}天{{timeObj.hh}}小时{{timeObj.mm}}分钟{{timeObj.ss}}秒
</span>
</slotchild>
</div>
</template>
<script>
import slotchild from "../components/slot/timecountdown.vue"
export default {
name: 'home',
components: {slotchild},
data () {
return {
msg: "父组件",
countDown: "",
}
},
methods: {
setCountDownData() {
this.countDown = (Date.parse(new Date()) + 48*60*60-50000) / 1000;
console.log(this.countDown)
}
},
created() {
this.setCountDownData();
},
}
</script>
<style scoped>
</style>