Vue3 Composition API

一、setup 配置详解

1.1 Vue2 中的 attrs 和 slots 回顾

在 Vue2 中,父组件通过标签属性向子组件传递数据时,通常需要在子组件中使用 props 接收。但即使不声明接收,这些属性也会存在于子组件实例的 $attrs 中。

示例对比:

javascript

复制代码
// Vue2 子组件
export default {
  props: ['receivedProp'], // 声明接收的属性
  created() {
    console.log(this.$attrs) // 未声明接收的属性会出现在这里
    console.log(this.$slots) // 父组件传递的插槽内容
  }
}

1.2 Vue3 setup 的两个关键点

执行时机
  • setupbeforeCreate 之前执行

  • 此时组件实例尚未创建,thisundefined

参数解析

setup 接收两个参数:

  1. props:包含组件外部传递且内部声明接收的属性

  2. context:上下文对象,包含:

    • attrs:未在 props 中声明的属性(相当于 Vue2 的 $attrs

    • slots:接收的插槽内容(相当于 Vue2 的 $slots

    • emit:触发自定义事件的函数(相当于 Vue2 的 $emit

完整示例:

javascript

复制代码
import { defineComponent } from 'vue'

export default defineComponent({
  props: ['title'],
  emits: ['change'], // 必须声明自定义事件
  setup(props, { attrs, slots, emit }) {
    console.log(props.title) // 访问声明的 prop
    console.log(attrs.customAttr) // 访问未声明的属性
    
    const handleClick = () => {
      emit('change', 'new value') // 触发事件
    }
    
    return {
      handleClick
    }
  }
})

二、响应式进阶:计算属性与监视

2.1 computed 函数

Vue3 的 computed 用法与 Vue2 类似,但更灵活:

javascript

复制代码
import { ref, computed } from 'vue'

setup() {
  const firstName = ref('张')
  const lastName = ref('三')
  
  // 只读计算属性
  const fullName = computed(() => `${firstName.value} ${lastName.value}`)
  
  // 可写计算属性
  const editableName = computed({
    get: () => `${firstName.value} ${lastName.value}`,
    set: (newValue) => {
      const [first, last] = newValue.split(' ')
      firstName.value = first
      lastName.value = last
    }
  })
  
  return { fullName, editableName }
}

2.2 watch 函数详解

Vue3 的 watch 功能强大但有一些注意事项:

javascript

复制代码
import { ref, reactive, watch } from 'vue'

setup() {
  const count = ref(0)
  const state = reactive({
    user: {
      name: 'Alice',
      age: 25
    }
  })
  
  // 情况1:监视 ref
  watch(count, (newVal, oldVal) => {
    console.log(`count变化: ${oldVal} -> ${newVal}`)
  })
  
  // 情况2:监视多个 ref
  watch([count, anotherRef], ([newCount, newAnother], [oldCount, oldAnother]) => {
    // 处理变化
  })
  
  // 情况3:监视 reactive 对象(注意 oldValue 问题)
  watch(
    () => state.user,
    (newUser, oldUser) => {
      // oldUser 可能与 newUser 相同!
    },
    { deep: true } // 虽然默认开启,但显式声明更清晰
  )
  
  // 情况4:监视 reactive 对象的特定属性
  watch(
    () => state.user.age,
    (newAge, oldAge) => {
      // 这里能正确获取 oldAge
    }
  )
}

2.3 watchEffect 智能监听

watchEffect 自动追踪回调中的响应式依赖:

javascript

复制代码
import { ref, watchEffect } from 'vue'

setup() {
  const count = ref(0)
  const message = ref('')
  
  watchEffect(() => {
    console.log(`count: ${count.value}, message: ${message.value}`)
    // 会自动追踪 count 和 message 的变化
  })
  
  // 实际应用:自动取消之前的请求
  const searchQuery = ref('')
  watchEffect(async (onCleanup) => {
    const query = searchQuery.value
    const controller = new AbortController()
    
    onCleanup(() => controller.abort()) // 清除副作用
    
    if (query) {
      const results = await fetchResults(query, {
        signal: controller.signal
      })
      // 处理结果
    }
  })
}

三、生命周期全解析

3.1 Vue2 与 Vue3 生命周期对比

Vue2 生命周期 Vue3 生命周期 (Options API) Vue3 Composition API
beforeCreate beforeCreate setup()
created created setup()
beforeMount beforeMount onBeforeMount
mounted mounted onMounted
beforeUpdate beforeUpdate onBeforeUpdate
updated updated onUpdated
beforeDestroy beforeUnmount onBeforeUnmount
destroyed unmounted onUnmounted

3.2 组合式 API 生命周期使用

javascript

复制代码
import { onMounted, onUnmounted } from 'vue'

setup() {
  // 鼠标位置跟踪示例
  const x = ref(0)
  const y = ref(0)
  
  const updatePosition = (e) => {
    x.value = e.pageX
    y.value = e.pageY
  }
  
  onMounted(() => {
    window.addEventListener('mousemove', updatePosition)
  })
  
  onUnmounted(() => {
    window.removeEventListener('mousemove', updatePosition)
  })
  
  return { x, y }
}

四、自定义 Hook 实践

自定义 Hook 是 Vue3 代码复用的利器:

4.1 鼠标位置跟踪 Hook

javascript

复制代码
// hooks/useMousePosition.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useMousePosition() {
  const x = ref(0)
  const y = ref(0)
  
  const updatePosition = (e) => {
    x.value = e.pageX
    y.value = e.pageY
  }
  
  onMounted(() => {
    window.addEventListener('mousemove', updatePosition)
  })
  
  onUnmounted(() => {
    window.removeEventListener('mousemove', updatePosition)
  })
  
  return { x, y }
}

// 在组件中使用
import { useMousePosition } from './hooks/useMousePosition'

setup() {
  const { x, y } = useMousePosition()
  return { x, y }
}

4.2 数据请求 Hook

javascript

复制代码
// hooks/useFetch.js
import { ref, isRef, unref, watchEffect } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)
  
  const fetchData = async () => {
    loading.value = true
    try {
      const response = await fetch(unref(url))
      data.value = await response.json()
      error.value = null
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }
  
  if (isRef(url)) {
    watchEffect(fetchData)
  } else {
    fetchData()
  }
  
  return { data, error, loading, retry: fetchData }
}

五、toRef 与 toRefs 深度解析

5.1 toRef 使用场景

javascript

复制代码
import { reactive, toRef } from 'vue'

setup() {
  const state = reactive({
    name: 'Alice',
    age: 25
  })
  
  // 保持响应式连接
  const nameRef = toRef(state, 'name')
  
  setTimeout(() => {
    state.name = 'Bob' // nameRef 也会更新
  }, 1000)
  
  return {
    nameRef // 可以在模板中直接使用
  }
}

5.2 toRefs 解构响应式对象

javascript

复制代码
import { reactive, toRefs } from 'vue'

setup() {
  const state = reactive({
    name: 'Alice',
    age: 25,
    address: {
      city: 'Beijing'
    }
  })
  
  // 解构后仍保持响应式
  const { name, age } = toRefs(state)
  
  // 嵌套对象需要单独处理
  const { city } = toRefs(state.address)
  
  return {
    name,
    age,
    city
  }
}

六、Vue3 新组件实战

6.1 Fragment 片段组件

Vue3 不再需要根标签:

vue

复制代码
<template>
  <header>...</header>
  <main>...</main>
  <footer>...</footer>
</template>

6.2 Teleport 传送门

将组件渲染到 DOM 中的其他位置:

vue

复制代码
<template>
  <button @click="showModal = true">打开弹窗</button>
  
  <Teleport to="body">
    <div v-if="showModal" class="modal">
      <div class="modal-content">
        <h2>标题</h2>
        <p>内容...</p>
        <button @click="showModal = false">关闭</button>
      </div>
    </div>
  </Teleport>
</template>

<script>
import { ref } from 'vue'
export default {
  setup() {
    const showModal = ref(false)
    return { showModal }
  }
}
</script>

6.3 Suspense 异步组件

优雅处理异步组件加载状态:

vue

复制代码
<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div class="loading-spinner">
        加载中...
      </div>
    </template>
  </Suspense>
</template>

<script>
import { defineAsyncComponent } from 'vue'

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

export default {
  components: {
    AsyncComponent
  }
}
</script>

七、Composition API 优势总结

  1. 更好的代码组织:相关功能代码集中在一起

  2. 更好的逻辑复用:通过自定义 Hook 实现

  3. 更好的类型推断:对 TypeScript 支持更友好

  4. 更小的生产包:Tree-shaking 友好

对比示例:

javascript

复制代码
// Options API 方式
export default {
  data() {
    return {
      count: 0,
      searchQuery: ''
    }
  },
  computed: {
    filteredList() {
      // 基于 searchQuery 过滤列表
    }
  },
  methods: {
    increment() {
      this.count++
    }
  },
  mounted() {
    // 初始化代码
  }
}

// Composition API 方式
import { ref, computed, onMounted } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const searchQuery = ref('')
    
    const increment = () => {
      count.value++
    }
    
    const filteredList = computed(() => {
      // 过滤逻辑
    })
    
    onMounted(() => {
      // 初始化代码
    })
    
    return {
      count,
      searchQuery,
      increment,
      filteredList
    }
  }
}

结语

Vue3 的 Composition API 为开发者提供了更灵活、更强大的代码组织方式。通过本文的详细解析和丰富示例,相信你已经掌握了其核心概念和使用技巧。在实际开发中,建议从简单功能开始逐步尝试组合式 API,体验其带来的开发效率提升和代码可维护性优势。

相关推荐
江城开朗的豌豆2 分钟前
useLayoutEffect:你以为它和useEffect是"亲兄弟"?其实差别大了!
前端·javascript·react.js
江城开朗的豌豆7 分钟前
聊聊useEffect:谁说副作用不能“优雅”?
前端·javascript·react.js
!执行9 分钟前
开发electron时候Chromium 报 Not allowed to load local resource → 空白页。
前端·javascript·electron
top_designer13 分钟前
告别“复制粘贴”式换肤:我用Adobe XD组件变体与CC库,构建多品牌设计系统架构
前端·ui·adobe·系统架构·ux·设计师·adobe xd
赛博切图仔27 分钟前
面试手写 Promise:链式 + 静态方法全实现
前端·javascript·面试
掘金安东尼33 分钟前
互联网不再由 URL 为核心入口
前端·人工智能·github
Moment36 分钟前
面试官:用户访问到一个不存在的路由,如何重定向到404 Not Found的页面 ❓❓❓
前端·javascript·面试
前端小巷子38 分钟前
深入 Vue3 computed
前端·vue.js·面试
未来的旋律~1 小时前
Web程序设计
前端
全宝1 小时前
实现一个有意思的眼球跟随卡片
前端·javascript·css