037_Vue 3 核心知识体系第二部分:高级组件功能

7. 组件通信

7.1 Props

vue

<!-- 父组件 -->
<template>
  <ChildComponent 
    :title="title"
    :user="user"
    :count="count"
    @update-count="updateCount"
  />
</template>

<script setup>
import { ref } from 'vue'

const title = ref('子组件标题')
const user = ref({ name: 'John', age: 30 })
const count = ref(0)

const updateCount = (newCount) => {
  count.value = newCount
}
</script>

<!-- 子组件 -->
<script setup>
// 定义 props
const props = defineProps({
  // 基础类型
  title: String,
  
  // 多种类型
  count: [Number, String],
  
  // 对象类型
  user: {
    type: Object,
    default: () => ({}) // 对象默认值必须是工厂函数
  },
  
  // 自定义验证器
  age: {
    type: Number,
    validator: (value) => {
      return value >= 0 && value <= 150
    }
  }
})

// 在模板中直接使用 props
</script>

7.2 自定义事件 (Emits)

vue

<!-- 子组件 -->
<template>
  <button @click="emitEvent">触发事件</button>
</template>

<script setup>
// 定义 emits(数组形式)
const emit = defineEmits(['update:title', 'custom-event'])

// 定义 emits(对象形式,带验证)
const emit = defineEmits({
  'update:title': (value) => {
    // 验证逻辑
    return typeof value === 'string' && value.length > 0
  },
  'custom-event': null // 不验证
})

const emitEvent = () => {
  // 触发事件
  emit('update:title', '新标题')
  emit('custom-event', { data: '额外数据' })
}
</script>

<!-- 父组件 -->
<template>
  <ChildComponent 
    @update:title="updateTitle"
    @custom-event="handleCustom"
  />
</template>

7.3 v-model 双向绑定

vue

<!-- 自定义 v-model 组件 -->
<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

<!-- 使用(默认 v-model) -->
<CustomInput v-model="message" />

<!-- 等同于 -->
<CustomInput
  :modelValue="message"
  @update:modelValue="newValue => message = newValue"
/>

<!-- 多个 v-model -->
<template>
  <input :value="firstName" @input="$emit('update:firstName', $event.target.value)" />
  <input :value="lastName" @input="$emit('update:lastName', $event.target.value)" />
</template>

<script setup>
defineProps(['firstName', 'lastName'])
defineEmits(['update:firstName', 'update:lastName'])
</script>

<!-- 使用多个 v-model -->
<UserName
  v-model:first-name="firstName"
  v-model:last-name="lastName"
/>

7.4 透传 Attributes

vue

<!-- 子组件 -->
<template>
  <!-- 使用 $attrs 绑定所有透传属性 -->
  <div v-bind="$attrs">
    子组件内容
  </div>
</template>

<script setup>
import { useAttrs } from 'vue'

// 访问透传的 attributes
const attrs = useAttrs()
console.log(attrs.class) // 父组件传入的 class
console.log(attrs.style) // 父组件传入的 style

// 禁用 Attributes 继承
defineOptions({
  inheritAttrs: false
})
</script>

7.5 插槽 (Slots)

vue

<!-- 子组件 -->
<template>
  <div class="container">
    <!-- 默认插槽 -->
    <slot>默认内容</slot>
    
    <!-- 具名插槽 -->
    <slot name="header">默认头部</slot>
    
    <!-- 作用域插槽 -->
    <slot name="footer" :user="user" :data="data"></slot>
    
    <!-- 动态插槽名 -->
    <slot :name="dynamicSlotName"></slot>
  </div>
</template>

<!-- 父组件使用 -->
<template>
  <ChildComponent>
    <!-- 默认插槽内容 -->
    <p>这是默认插槽内容</p>
    
    <!-- 具名插槽 -->
    <template #header>
      <h1>自定义头部</h1>
    </template>
    
    <!-- 作用域插槽 -->
    <template #footer="{ user, data }">
      <p>用户: {{ user.name }}</p>
      <p>数据: {{ data }}</p>
    </template>
    
    <!-- 简写 -->
    <template v-slot:header>简写头部</template>
    
    <!-- 动态插槽 -->
    <template #[dynamicSlotName]>
      动态插槽内容
    </template>
  </ChildComponent>
</template>

7.6 provide / inject

vue

<!-- 祖先组件 -->
<script setup>
import { provide, ref } from 'vue'

const count = ref(0)
const user = ref({ name: 'John' })

// 提供数据
provide('count', count)
provide('user', user)

// 提供方法
provide('increment', () => {
  count.value++
})

// 提供响应式对象
provide('theme', {
  primaryColor: '#409EFF',
  fontSize: '14px'
})
</script>

<!-- 后代组件 -->
<script setup>
import { inject } from 'vue'

// 注入数据
const count = inject('count')
const user = inject('user')
const increment = inject('increment')

// 带默认值
const theme = inject('theme', {
  primaryColor: '#000000',
  fontSize: '16px'
})

// 工厂函数
const config = inject('config', () => ({ apiUrl: '/api' }))

// 注入作为响应式数据
const userRef = inject('user')
userRef.value.name = '新名字' // 会修改祖先组件的数据
</script>

8. 高级组件模式

8.1 异步组件

javascript

// 基本异步组件
const AsyncComponent = defineAsyncComponent(() =>
  import('./MyComponent.vue')
)

// 带选项的异步组件
const AsyncComponentWithOptions = defineAsyncComponent({
  // 加载函数
  loader: () => import('./MyComponent.vue'),
  
  // 加载中显示的组件
  loadingComponent: LoadingComponent,
  
  // 加载失败显示的组件
  errorComponent: ErrorComponent,
  
  // 延迟显示 loading 的时间
  delay: 200,
  
  // 超时时间
  timeout: 3000,
  
  // 是否可挂起
  suspensible: true,
  
  // 加载错误时的回调
  onError(error, retry, fail, attempts) {
    if (error.message.match(/fetch/) && attempts <= 3) {
      retry() // 重试
    } else {
      fail() // 失败
    }
  }
})

8.2 动态组件

vue

<template>
  <!-- 动态组件 -->
  <component :is="currentComponent" />
  
  <!-- 动态组件 with props -->
  <component
    :is="currentComponent"
    :title="title"
    @custom-event="handleEvent"
  />
  
  <!-- 保持动态组件状态 -->
  <KeepAlive>
    <component :is="currentComponent" />
  </KeepAlive>
</template>

<script setup>
import { ref, shallowRef } from 'vue'
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'

const currentComponent = ref('ComponentA')
const components = {
  ComponentA,
  ComponentB
}

// 使用 shallowRef 避免不必要的响应式转换
const currentComponent = shallowRef(ComponentA)
</script>

8.3 递归组件

vue

<!-- TreeItem.vue -->
<template>
  <li>
    <div @click="toggle">
      {{ node.name }}
      <span v-if="hasChildren">[{{ isOpen ? '-' : '+' }}]</span>
    </div>
    <ul v-if="hasChildren && isOpen">
      <TreeItem
        v-for="child in node.children"
        :key="child.id"
        :node="child"
      />
    </ul>
  </li>
</template>

<script setup>
import { ref } from 'vue'

const props = defineProps({
  node: Object
})

const isOpen = ref(false)
const hasChildren = computed(() => 
  props.node.children && props.node.children.length > 0
)

const toggle = () => {
  if (hasChildren.value) {
    isOpen.value = !isOpen.value
  }
}

// 递归组件必须要有 name
defineOptions({
  name: 'TreeItem'
})
</script>

8.4 函数式组件

vue

<!-- 函数式组件 -->
<script>
// 选项式 API
export default {
  functional: true,
  props: ['level'],
  render(h, context) {
    return h(
      'h' + context.props.level,
      context.data,
      context.children
    )
  }
}
</script>

<!-- 组合式 API 函数式组件 -->
<script setup>
import { h } from 'vue'

const FunctionalComponent = (props, { slots, attrs, emit }) => {
  return h('div', [
    h('span', `Count: ${props.count}`),
    slots.default?.(),
    slots.header?.(),
  ])
}

// 定义 props
FunctionalComponent.props = ['count']

// 定义 emits
FunctionalComponent.emits = ['click']
</script>

9. 内置组件

9.1 Transition & TransitionGroup

vue

<template>
  <!-- Transition -->
  <button @click="show = !show">切换</button>
  <Transition name="fade">
    <p v-if="show">你好</p>
  </Transition>
  
  <!-- 自定义类名 -->
  <Transition
    enter-from-class="enter-from"
    enter-active-class="enter-active"
    enter-to-class="enter-to"
    leave-from-class="leave-from"
    leave-active-class="leave-active"
    leave-to-class="leave-to"
  >
    <p v-if="show">自定义动画</p>
  </Transition>
  
  <!-- JavaScript 钩子 -->
  <Transition
    @before-enter="onBeforeEnter"
    @enter="onEnter"
    @after-enter="onAfterEnter"
    @enter-cancelled="onEnterCancelled"
    @before-leave="onBeforeLeave"
    @leave="onLeave"
    @after-leave="onAfterLeave"
    @leave-cancelled="onLeaveCancelled"
  >
    <p v-if="show">JavaScript 动画</p>
  </Transition>
  
  <!-- TransitionGroup -->
  <TransitionGroup name="list" tag="ul">
    <li v-for="item in items" :key="item.id">
      {{ item.text }}
    </li>
  </TransitionGroup>
</template>

<style scoped>
/* 基础过渡 */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

/* 列表过渡 */
.list-move, /* 对移动中的元素应用的过渡 */
.list-enter-active,
.list-leave-active {
  transition: all 0.5s ease;
}

.list-enter-from,
.list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}

/* 确保离开的项目从布局中删除 */
.list-leave-active {
  position: absolute;
}
</style>

9.2 KeepAlive

vue

<template>
  <!-- 基本用法 -->
  <KeepAlive>
    <component :is="currentComponent" />
  </KeepAlive>
  
  <!-- 包含/排除组件 -->
  <KeepAlive :include="['ComponentA', 'ComponentB']" :exclude="['ComponentC']">
    <component :is="currentComponent" />
  </KeepAlive>
  
  <!-- 最大缓存数 -->
  <KeepAlive :max="10">
    <component :is="currentComponent" />
  </KeepAlive>
</template>

<script setup>
import { onActivated, onDeactivated } from 'vue'

onActivated(() => {
  // 组件被激活时调用
  console.log('组件激活')
})

onDeactivated(() => {
  // 组件被停用时调用
  console.log('组件停用')
})
</script>

9.3 Teleport

vue

<template>
  <!-- 传送到 body -->
  <Teleport to="body">
    <div class="modal">
      <p>这是一个模态框</p>
      <button @click="show = false">关闭</button>
    </div>
  </Teleport>
  
  <!-- 传送到指定元素 -->
  <Teleport to="#modal-container">
    <Modal />
  </Teleport>
  
  <!-- 条件传送 -->
  <Teleport :disabled="isMobile">
    <Modal />
  </Teleport>
  
  <!-- 多个 Teleport 到一样目标 -->
  <Teleport to="#modals">
    <ModalA />
  </Teleport>
  <Teleport to="#modals">
    <ModalB />
  </Teleport>
  <!-- 结果:ModalA 在前,ModalB 在后 -->
</template>

9.4 Suspense

vue

<template>
  <!-- 基本用法 -->
  <Suspense>
    <!-- 默认插槽:异步组件 -->
    <template #default>
      <AsyncComponent />
    </template>
    
    <!-- fallback 插槽:加载状态 -->
    <template #fallback>
      <div>加载中...</div>
    </template>
  </Suspense>
  
  <!-- 多个异步组件 -->
  <Suspense>
    <template #default>
      <div>
        <AsyncComponent1 />
        <AsyncComponent2 />
      </div>
    </template>
  </Suspense>
</template>

<script setup>
import { defineAsyncComponent } from 'vue'

const AsyncComponent = defineAsyncComponent(() =>
  import('./AsyncComponent.vue')
)

// 组件中的异步 setup
const AsyncComponentWithAsyncSetup = defineAsyncComponent({
  async setup() {
    const data = await fetchData()
    return { data }
  }
})
</script>

10. 自定义指令

10.1 全局自定义指令

javascript

// main.js
const app = createApp(App)

// 注册全局指令
app.directive('focus', {
  // 指令绑定元素挂载前
  beforeMount(el) {
    el.style.backgroundColor = 'yellow'
  },
  
  // 指令绑定元素挂载后
  mounted(el) {
    el.focus()
  },
  
  // 组件更新前
  beforeUpdate(el) {
    console.log('beforeUpdate')
  },
  
  // 组件更新后
  updated(el) {
    console.log('updated')
  },
  
  // 组件卸载前
  beforeUnmount(el) {
    console.log('beforeUnmount')
  },
  
  // 组件卸载后
  unmounted(el) {
    console.log('unmounted')
  }
})

// 简写形式(只在 mounted 和 updated 时调用)
app.directive('color', (el, binding) => {
  el.style.color = binding.value
})

10.2 局部自定义指令

vue

<script setup>
// 局部自定义指令
const vFocus = {
  mounted: (el) => el.focus()
}

// 对象形式
const vCustom = {
  beforeMount(el, binding, vnode, prevVnode) {
    // el: 绑定元素
    // binding: 绑定对象
    // vnode: 当前 VNode
    // prevVnode: 上一个 VNode
  }
}

// 函数形式
const vColor = (el, binding) => {
  el.style.color = binding.value
}
</script>

<template>
  <!-- 使用指令 -->
  <input v-focus />
  <p v-color="'red'">红色文字</p>
  
  <!-- 动态参数 -->
  <p v-custom:[arg]="value">自定义指令</p>
  
  <!-- 修饰符 -->
  <div v-demo.modifier></div>
</template>

<script setup>
// 访问修饰符
const vDemo = {
  mounted(el, binding) {
    console.log(binding.modifiers) // { modifier: true }
  }
}
</script>

10.3 指令钩子参数

javascript

const vMyDirective = {
  // 元素属性或事件监听器应用前调用
  created(el, binding, vnode, prevVnode) {},
  
  // 元素插入 DOM 前调用
  beforeMount(el, binding, vnode, prevVnode) {},
  
  // 元素插入 DOM 后调用
  mounted(el, binding, vnode, prevVnode) {
    // binding 对象包含:
    // value: 指令的值
    // oldValue: 之前的值
    // arg: 指令参数
    // modifiers: 修饰符对象
    // instance: 组件实例
    // dir: 指令定义对象
  },
  
  // 组件更新前调用
  beforeUpdate(el, binding, vnode, prevVnode) {},
  
  // 组件更新后调用
  updated(el, binding, vnode, prevVnode) {},
  
  // 组件卸载前调用
  beforeUnmount(el, binding, vnode, prevVnode) {},
  
  // 组件卸载后调用
  unmounted(el, binding, vnode, prevVnode) {}
}

10.4 实用指令示例

javascript

// 1. 点击外部关闭指令
const vClickOutside = {
  mounted(el, { value: callback }) {
    el.clickOutsideHandler = (event) => {
      if (!el.contains(event.target)) {
        callback(event)
      }
    }
    document.addEventListener('click', el.clickOutsideHandler)
  },
  unmounted(el) {
    document.removeEventListener('click', el.clickOutsideHandler)
  }
}

// 2. 防抖指令
const vDebounce = {
  mounted(el, { value: callback, arg: delay = 300 }) {
    let timer
    el.addEventListener('input', (event) => {
      clearTimeout(timer)
      timer = setTimeout(() => {
        callback(event)
      }, delay)
    })
  }
}

// 3. 权限指令
const vPermission = {
  mounted(el, { value: permission }) {
    const userPermissions = ['read', 'write'] // 从 store 获取
    if (!userPermissions.includes(permission)) {
      el.parentNode?.removeChild(el)
    }
  }
}

// 4. 复制指令
const vCopy = {
  mounted(el, { value }) {
    el.addEventListener('click', () => {
      navigator.clipboard.writeText(value)
        .then(() => alert('复制成功'))
        .catch(err => console.error('复制失败:', err))
    })
  }
}

这是 Vue 3 核心知识的第二部分,涵盖了组件通信、高级组件模式、内置组件和自定义指令。

© 版权声明

相关文章

暂无评论

none
暂无评论...