vue2之插槽

175 阅读3分钟

背景

最近在开发中需要用到一个倒计时的组件,本着cv工程师的行事原则,自然是百度网罗天下英才的成果,最终找到一个利用插槽实现的倒计时组件,感觉非常nice,之前也一直没有好好了解过vue插槽,固特意借此机会学习一下,提升一下自己cv能力。

vue插槽

vue插槽是vue实现的一种内容分发的API,将slot元素作为分发内容的出口。(这句话来自官方文档)

作用:

  1. 父组件向子组件传递内容;

  2. 拓展、复用、定制组件;

分类:

  1. 默认插槽

  2. 具名插槽

  3. 作用域插槽

下面一一看一下吧:

默认插槽

先来个子组件吧:


<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> 将会被替换为:这是在父组件中的一句话。如下图:

image.png

这就是默认插槽。

具名插槽

顾名思义就是有名字的插槽。有时我们需要多个插槽,例如:


<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,但你也可以使用任意你喜欢的名字。

好了,抄完了也看完了;下面放个使用例子吧(也是网上扒的)倒计时组件

参考链接:blog.csdn.net/baiqiangdou…


<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>