小春日和の秘密基地

Vue框架的Tabs组件封装

watch_later2020年08月24日
menu_book总字数:1.1k
access_alarm预计阅读时间:15分钟
local_offerVue.jslocal_offer前端技术

图片来源:pixiv:ハロウィンチマメ 作者:とけとう

前言

在使用Vue开发的过程中,想必大家都使用过一些UI框架的Tab组件,这些Tab组件一般都由两部分组成:TabContainerTabItem,在使用时呈以下结构:

<TabContainer v-model="activeTab" @change="handlerForWasActiveTabChanged">
  <TabItem label="水果" :value="1">
    <div>苹果</div>
    <div>橙子</div>
  </TabItem>

  <TabItem label="蔬菜" :value="2">
    <div>白菜</div>
    <div>蘑菇</div>
  </TabItem>
</TabContainer>

最后生成一组标签,并在点击对应标签时显示对应TabItem中的内容。

那么问题来了,这种写法最后生成Tabs是如何实现的呢?

实现

首先来实现TabItem组件,这个组件非常简单。

可以理解为这个组件为接下来要实现的TabContainer组件提供数据。

<template>
  <!-- 判断父组件(TabContainer)中的activeTab,决定是否显示 -->
  <div v-if="$parent.activeTab === value">
    <slot></slot>   <!-- 声明插槽接收item标签的内容 -->
  </div>
</template>

<script>
export default {
  // 声明要使用的数据,这部分数据会在tabContainer组件中被使用
  props: [
    'label',  // 标签名
    'value'   // 标签值
  ],   

  data() {
    return {

    }
  },

  methods: {

  }
}
</script>

再来实现关键的TabContainer组件,这个组件包含生成实际内容的逻辑。

<template>
  <div>
    <span 
      v-for="(item, index) in tabs" 
      :key="index" 
      @click="activeTab = item.value"
    >{{ item.label }}</span>

    <slot />  <!-- 接收tabItem的内容 -->
  </div>
</template>

<script>
export default {
  props: ['value'],

  data (){
    return {
      children: []  // $children不支持响应式,必须声明一个数组对其进行接管
    }
  },

  // 必须在mounted中,在created中$children为空数组
  mounted() {
    // 这一步有两个意义:一是触发更新,二是接管$children,如果使用$children作为数据源,会发现即使手动更新,$children的数据依然会慢上一拍(拿到的总是上一次的数据)
    this.children = this.$children   
  },

  computed: {
    // 将子组件中的label和value取出
    tabs (){
      return this.children.map(item => ({ label: item.label, value: item.value }))
    },

    activeTab: {
      get (){ return this.value },
      set (val){ this.$emit('input', val) }
    }
  },

  methods: {

  }
}
</script>

到这里就已经实现在前言中描述的用法了。但是,细心的朋友可能已经发现了,上面的代码在动态生成TabItem时会导致无法更新,因为官方在API文档中对于$children属性的说明中也已经提到了:

需要注意 $children 并不保证顺序,也不是响应式的。

上面的代码tabs只会在mounted中更新一次,由对this.children的赋值引起,之后就不会再更新了,因为$children本身不是响应式的。同时我还尝试了使用$slots获取,虽然文档上没有提及,但经过测试$slots也不是响应式的,最后只好在updated钩子中进行脏检查更新,如果有更好的解决办法,也请各路大神不吝赐教。

以下是脏检查版的TabContainer

<template>
  <div>
    <span 
        v-for="(item, index) in tabsData()" 
        :key="index" 
        @click="activeTab = item.value"
    >{{ item.label }}</span>

    <slot />
  </div>
</template>
<script>
export default {
  props: ['value'],

  data (){
    return {
      childrenCache: this.$children
    }
  },

  mounted() {
    this.$forceUpdate()
  },

  updated() {
    if (this.needUpdateTabs()) {
      this.childrenCache = this.$children
      this.$forceUpdate()
    }
  },

  computed: {
    activeTab: {
      get (){ return this.value },
      set (val){ this.$emit('input', val) }
    }
  },

  methods: {
    // 生成渲染tabs时要用到的数据
    tabsData() {
      if (this.childrenCache === undefined) return []
      return this.childrenCache.map(item => ({ label: item.label, value: item.value }))
    },

    // 检查是否需要更新视图
    needUpdateTabs() {
      if (this.childrenCache.length !== this.$children.length) return true
      return this.childrenCache.some((item, index) => item !== this.$children[index])
    }
  }
}
</script>

版权声明:本文为原创文章,版权归 小春日和 所有

文章链接:https://koharubiyori.github.io/Vue/Vue框架的Tabs组件封装/

所有原创文章采用 署名-非商业性使用 4.0 国际 (CC BY-NC 4.0)

您可以自由转载和修改,但必须保证在显著位置注明文章来源,且不能用于商业目的。

north