动态组件

动态组件让你可以在同一个挂载点动态切换不同的组件。这在实现标签页、多步骤表单、动态内容加载等场景非常有用。

基本用法

component 元素

使用 <component> 元素和 is 属性动态切换组件:

<div id="app">
  <button @click="current = 'home'">首页</button>
  <button @click="current = 'posts'">文章</button>
  <button @click="current = 'archive'">归档</button>
  
  <component :is="current"></component>
</div>

<script>
Vue.component('home', {
  template: '<div>首页内容</div>'
})

Vue.component('posts', {
  template: '<div>文章列表</div>'
})

Vue.component('archive', {
  template: '<div>归档内容</div>'
})

new Vue({
  el: '#app',
  data: {
    current: 'home'
  }
})
</script>

组件对象

is 属性可以直接绑定组件对象:

<div id="app">
  <button 
    v-for="tab in tabs" 
    :key="tab.name"
    @click="currentTab = tab.component"
  >
    {{ tab.name }}
  </button>
  
  <component :is="currentTab"></component>
</div>

<script>
new Vue({
  el: '#app',
  data: {
    currentTab: null,
    tabs: [
      { 
        name: '首页', 
        component: {
          template: '<div>首页内容</div>'
        }
      },
      { 
        name: '文章', 
        component: {
          template: '<div>文章列表</div>'
        }
      }
    ]
  },
  created() {
    this.currentTab = this.tabs[0].component
  }
})
</script>

keep-alive

切换组件时,默认会销毁旧组件、创建新组件。使用 <keep-alive> 可以缓存组件状态。

基本用法

<keep-alive>
  <component :is="current"></component>
</keep-alive>

缓存效果

<div id="app">
  <button @click="current = 'counter-a'">计数器 A</button>
  <button @click="current = 'counter-b'">计数器 B</button>
  
  <!-- 不使用 keep-alive:切换后计数器重置 -->
  <component :is="current"></component>
  
  <!-- 使用 keep-alive:切换后计数器保持 -->
  <keep-alive>
    <component :is="current"></component>
  </keep-alive>
</div>

<script>
Vue.component('counter-a', {
  template: '<div>计数器 A: {{ count }} <button @click="count++">+1</button></div>',
  data() {
    return { count: 0 }
  }
})

Vue.component('counter-b', {
  template: '<div>计数器 B: {{ count }} <button @click="count++">+1</button></div>',
  data() {
    return { count: 0 }
  }
})
</script>

include / exclude

指定哪些组件需要缓存:

<!-- 包含指定组件 -->
<keep-alive include="counter-a,counter-b">
  <component :is="current"></component>
</keep-alive>

<!-- 排除指定组件 -->
<keep-alive exclude="counter-c">
  <component :is="current"></component>
</keep-alive>

<!-- 使用正则 -->
<keep-alive :include="/^counter-/">
  <component :is="current"></component>
</keep-alive>

<!-- 使用数组 -->
<keep-alive :include="['counter-a', 'counter-b']">
  <component :is="current"></component>
</keep-alive>

max 限制

限制最多缓存多少组件实例:

<keep-alive :max="10">
  <component :is="current"></component>
</keep-alive>

超出限制后,会销毁最久没有访问的实例。

生命周期钩子

keep-alive 缓存的组件有两个特殊的生命周期钩子:

activated

组件被激活时调用:

Vue.component('my-component', {
  template: '<div>组件内容</div>',
  activated() {
    console.log('组件被激活')
  }
})

deactivated

组件被停用时调用:

Vue.component('my-component', {
  template: '<div>组件内容</div>',
  deactivated() {
    console.log('组件被停用')
  }
})

完整示例

Vue.component('user-list', {
  template: `
    <div>
      <h3>用户列表</h3>
      <ul>
        <li v-for="user in users" :key="user.id">{{ user.name }}</li>
      </ul>
    </div>
  `,
  data() {
    return {
      users: []
    }
  },
  created() {
    console.log('created: 初始化')
    this.fetchUsers()
  },
  activated() {
    console.log('activated: 组件激活')
    // 可以在这里刷新数据
    if (this.users.length > 0) {
      this.fetchUsers()
    }
  },
  deactivated() {
    console.log('deactivated: 组件停用')
  },
  methods: {
    fetchUsers() {
      // 模拟 API 调用
      console.log('获取用户列表')
    }
  }
})

实战示例

标签页

<div id="app">
  <div class="tabs">
    <button 
      v-for="tab in tabs" 
      :key="tab.name"
      :class="{ active: currentTab === tab.name }"
      @click="currentTab = tab.name"
    >
      {{ tab.label }}
    </button>
  </div>
  
  <keep-alive>
    <component :is="currentComponent"></component>
  </keep-alive>
</div>

<script>
Vue.component('tab-home', {
  template: '<div class="tab-pane">首页内容</div>'
})

Vue.component('tab-posts', {
  template: '<div class="tab-pane">文章列表</div>'
})

Vue.component('tab-archive', {
  template: '<div class="tab-pane">归档内容</div>'
})

new Vue({
  el: '#app',
  data: {
    currentTab: 'home',
    tabs: [
      { name: 'home', label: '首页' },
      { name: 'posts', label: '文章' },
      { name: 'archive', label: '归档' }
    ]
  },
  computed: {
    currentComponent() {
      return 'tab-' + this.currentTab
    }
  }
})
</script>

多步骤表单

<div id="app">
  <div class="step-indicator">
    <span 
      v-for="(step, index) in steps" 
      :key="index"
      :class="{ active: currentStep >= index, current: currentStep === index }"
    >
      {{ step.title }}
    </span>
  </div>
  
  <keep-alive>
    <component 
      :is="steps[currentStep].component"
      :data="formData"
      @next="nextStep"
      @prev="prevStep"
    ></component>
  </keep-alive>
  
  <div class="step-actions">
    <button 
      v-if="currentStep > 0"
      @click="prevStep"
    >上一步</button>
    <button 
      v-if="currentStep < steps.length - 1"
      @click="nextStep"
    >下一步</button>
    <button 
      v-if="currentStep === steps.length - 1"
      @click="submit"
    >提交</button>
  </div>
</div>

<script>
Vue.component('step-basic', {
  props: ['data'],
  template: `
    <div class="step-content">
      <h3>基本信息</h3>
      <input v-model="data.name" placeholder="姓名">
      <input v-model="data.email" placeholder="邮箱">
    </div>
  `
})

Vue.component('step-detail', {
  props: ['data'],
  template: `
    <div class="step-content">
      <h3>详细信息</h3>
      <input v-model="data.phone" placeholder="电话">
      <input v-model="data.address" placeholder="地址">
    </div>
  `
})

Vue.component('step-confirm', {
  props: ['data'],
  template: `
    <div class="step-content">
      <h3>确认信息</h3>
      <p>姓名:{{ data.name }}</p>
      <p>邮箱:{{ data.email }}</p>
      <p>电话:{{ data.phone }}</p>
      <p>地址:{{ data.address }}</p>
    </div>
  `
})

new Vue({
  el: '#app',
  data: {
    currentStep: 0,
    formData: {
      name: '',
      email: '',
      phone: '',
      address: ''
    },
    steps: [
      { title: '基本信息', component: 'step-basic' },
      { title: '详细信息', component: 'step-detail' },
      { title: '确认提交', component: 'step-confirm' }
    ]
  },
  methods: {
    nextStep() {
      if (this.currentStep < this.steps.length - 1) {
        this.currentStep++
      }
    },
    prevStep() {
      if (this.currentStep > 0) {
        this.currentStep--
      }
    },
    submit() {
      console.log('提交数据:', this.formData)
    }
  }
})
</script>

动态加载组件

<div id="app">
  <select v-model="selectedComponent">
    <option value="">请选择组件</option>
    <option value="chart-bar">柱状图</option>
    <option value="chart-line">折线图</option>
    <option value="chart-pie">饼图</option>
  </select>
  
  <keep-alive>
    <component 
      v-if="selectedComponent"
      :is="selectedComponent"
      :data="chartData"
    ></component>
  </keep-alive>
</div>

<script>
Vue.component('chart-bar', {
  props: ['data'],
  template: '<div class="chart">柱状图:{{ data }}</div>'
})

Vue.component('chart-line', {
  props: ['data'],
  template: '<div class="chart">折线图:{{ data }}</div>'
})

Vue.component('chart-pie', {
  props: ['data'],
  template: '<div class="chart">饼图:{{ data }}</div>'
})

new Vue({
  el: '#app',
  data: {
    selectedComponent: '',
    chartData: [1, 2, 3, 4, 5]
  }
})
</script>

权限控制

<div id="app">
  <component :is="currentView"></component>
</div>

<script>
new Vue({
  el: '#app',
  data: {
    user: {
      role: 'admin'
    }
  },
  computed: {
    currentView() {
      if (!this.user) {
        return 'login-page'
      }
      
      switch (this.user.role) {
        case 'admin':
          return 'admin-dashboard'
        case 'user':
          return 'user-dashboard'
        default:
          return 'guest-page'
      }
    }
  }
})
</script>

注意事项

keep-alive 要求

keep-alive 要求被包裹的组件必须有 name 属性(用于 include/exclude 匹配):

// ✅ 正确
Vue.component('my-component', {
  name: 'MyComponent',
  template: '...'
})

// ❌ 可能有问题
Vue.component('my-component', {
  template: '...'
})

activated/deactivated vs mounted/destroyed

Vue.component('my-component', {
  created() {
    console.log('created: 只执行一次')
  },
  mounted() {
    console.log('mounted: 只执行一次')
  },
  activated() {
    console.log('activated: 每次激活都执行')
  },
  deactivated() {
    console.log('deactivated: 每次停用都执行')
  }
})

避免内存泄漏

使用 keep-alive 会缓存组件实例,注意避免内存泄漏:

Vue.component('my-component', {
  activated() {
    // 重新添加事件监听
    window.addEventListener('resize', this.handleResize)
  },
  deactivated() {
    // 移除事件监听
    window.removeEventListener('resize', this.handleResize)
  },
  methods: {
    handleResize() {
      // ...
    }
  }
})

小结

动态组件要点:

  1. component 元素:使用 is 属性动态切换组件
  2. keep-alive:缓存组件状态,避免重复创建
  3. 生命周期钩子:使用 activated/deactivated 处理缓存组件
  4. 性能优化:使用 include/exclude/max 控制缓存

至此,Vue2 的组件通信内容已经全部讲解完毕。