直接跳到内容

Composition API

为什么需要 Composition API?

Options API 的局限性

在 Vue 2 中,我们使用 Options API 来组织组件代码,将数据、方法、计算属性等分别放在不同的选项中。这种方式在简单组件中表现良好,但随着组件复杂度增加,问题逐渐显现:

javascript
// Options API 的"碎片化"问题示例
export default {
  data() {
    return {
      // 功能A相关的数据
      listA: [],
      filterA: '',

      // 功能B相关的数据
      listB: [],
      filterB: '',

      // 功能C相关的数据
      loading: false,
      error: null,
    }
  },
  computed: {
    // 功能A的计算属性
    filteredListA() {
      /* ... */
    },

    // 功能B的计算属性
    filteredListB() {
      /* ... */
    },

    // 功能C的计算属性
    hasError() {
      /* ... */
    },
  },
  methods: {
    // 功能A的方法
    fetchDataA() {
      /* ... */
    },
    updateFilterA() {
      /* ... */
    },

    // 功能B的方法
    fetchDataB() {
      /* ... */
    },
    updateFilterB() {
      /* ... */
    },

    // 功能C的方法
    handleError() {
      /* ... */
    },
  },
  mounted() {
    // 功能A的初始化
    this.fetchDataA()

    // 功能B的初始化
    this.fetchDataB()
  },
}

这种“碎片化”的组织方式导致相关逻辑被分散在不同选项中,理解和维护复杂组件变得困难。

Composition API 的优势

Composition API 通过基于逻辑功能而非选项类型来组织代码,解决了上述问题:

javascript
import { ref, computed, onMounted } from 'vue'

export default {
  setup() {
    // 功能A:数据列表管理
    const { listA, filterA, filteredListA, fetchDataA } = useFeatureA()

    // 功能B:另一个数据列表管理
    const { listB, filterB, filteredListB, fetchDataB } = useFeatureB()

    // 功能C:状态管理
    const { loading, error, hasError, handleError } = useFeatureC()

    onMounted(() => {
      fetchDataA()
      fetchDataB()
    })

    return {
      // 功能A
      listA,
      filterA,
      filteredListA,
      fetchDataA,
      // 功能B
      listB,
      filterB,
      filteredListB,
      fetchDataB,
      // 功能C
      loading,
      error,
      hasError,
      handleError,
    }
  },
}

核心概念:setup() 函数与响应式引用

setup() 函数详解

setup() 是 Composition API 的入口点,它在组件实例创建之前执行:

javascript
export default {
  props: {
    title: String,
    count: Number,
  },
  setup(props, context) {
    // props:包含组件接收的所有 prop,是响应式的
    console.log(props.title)

    // context:包含三个属性
    const { attrs, slots, emit } = context

    // attrs:非响应式的 attribute
    console.log(attrs.class)

    // slots:插槽内容
    console.log(slots.default)

    // emit:触发事件
    const handleClick = () => {
      emit('update:count', props.count + 1)
    }

    return {
      handleClick,
    }
  },
}

响应式数据定义

ref - 用于基本类型

javascript
import { ref } from 'vue'

export default {
  setup() {
    // 基本类型使用 ref
    const count = ref(0)
    const name = ref('Vue')

    // 访问值时需要使用 .value
    const increment = () => {
      count.value++
    }

    // 在模板中自动解包,无需 .value
    return {
      count,
      name,
      increment,
    }
  },
}

reactive - 用于对象类型

javascript
import { reactive, toRefs } from 'vue'

export default {
  setup() {
    // 对象类型使用 reactive
    const state = reactive({
      count: 0,
      name: 'Vue',
      todos: [],
    })

    const addTodo = (text) => {
      state.todos.push({ text, done: false })
    }

    // 使用 toRefs 保持响应式解构
    return {
      ...toRefs(state),
      addTodo,
    }
  },
}

生命周期钩子在 setup 中的使用

生命周期渲染流程示意图

组件初始化

beforeCreate    ← Options API 特有

created         ← Options API 特有

setup()         ← Composition API 入口

beforeMount

DOM 渲染

mounted

数据变化触发更新

beforeUpdate

虚拟 DOM 重新渲染和打补丁

updated

组件卸载前

beforeUnmount

组件卸载

unmounted

生命周期对比表

Options APIComposition API执行时机
beforeCreate无直接对应setup() 之前执行
created无直接对应setup() 之前执行
beforeMountonBeforeMount挂载开始之前调用
mountedonMounted组件挂载后调用
beforeUpdateonBeforeUpdate数据变化,DOM 更新前
updatedonUpdated数据变化,DOM 更新后
beforeUnmountonBeforeUnmount组件卸载前调用
unmountedonUnmounted组件卸载后调用
errorCapturedonErrorCaptured捕获后代组件错误时
renderTrackedonRenderTracked渲染依赖被追踪时
renderTriggeredonRenderTriggered渲染依赖被触发时
activatedonActivatedkeep-alive 组件激活时
deactivatedonDeactivatedkeep-alive 组件停用时

setup 中使用生命周期

javascript
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  ref,
} from 'vue'

export default {
  setup() {
    const count = ref(0)
    const message = ref('Hello Vue 3')

    onBeforeMount(() => {
      console.log('组件即将挂载')
    })

    onMounted(() => {
      console.log('组件已挂载,可以访问 DOM')
      // 在这里进行数据初始化
      fetchInitialData()
    })

    onBeforeUpdate(() => {
      console.log('数据即将更新,DOM 还未更新')
    })

    onUpdated(() => {
      console.log('数据已更新,DOM 也已更新')
    })

    onBeforeUnmount(() => {
      console.log('组件即将卸载')
      // 清理定时器、取消订阅等
      clearInterval(timer)
    })

    onUnmounted(() => {
      console.log('组件已卸载')
    })

    const fetchInitialData = async () => {
      // 获取初始数据
    }

    return {
      count,
      message,
    }
  },
}

逻辑复用利器:自定义 Hook

什么是自定义 Hook?

自定义 Hook 是一个 JavaScript 函数,它使用 Composition API 来封装和复用有状态的逻辑。约定上,自定义 Hook 以“use”开头。

封装 useMouse Hook

javascript
// useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  const update = (event) => {
    x.value = event.pageX
    y.value = event.pageY
  }

  onMounted(() => {
    window.addEventListener('mousemove', update)
  })

  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })

  return { x, y }
}

使用自定义 Hook

javascript
// 在组件中使用
import { useMouse } from './useMouse'

export default {
  setup() {
    const { x, y } = useMouse()

    return {
      x,
      y,
    }
  },
}

更复杂的示例:useFetch

javascript
// useFetch.js
import { ref, onUnmounted } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)

  let abortController = null

  const fetchData = async () => {
    loading.value = true
    error.value = null

    // 如果已有请求在进行,取消它
    if (abortController) {
      abortController.abort()
    }

    abortController = new AbortController()

    try {
      const response = await fetch(url, {
        signal: abortController.signal,
      })

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }

      data.value = await response.json()
    } catch (err) {
      if (err.name !== 'AbortError') {
        error.value = err.message
      }
    } finally {
      loading.value = false
      abortController = null
    }
  }

  onUnmounted(() => {
    if (abortController) {
      abortController.abort()
    }
  })

  return {
    data,
    loading,
    error,
    fetchData,
  }
}

在多个组件中复用

javascript
// UserList.vue
import { useFetch } from '../composables/useFetch'

export default {
  setup() {
    const { data: users, loading, error, fetchData } = useFetch('/api/users')

    return {
      users,
      loading,
      error,
      refresh: fetchData
    }
  }
}

// PostList.vue
import { useFetch } from '../composables/useFetch'

export default {
  setup() {
    const { data: posts, loading, error, fetchData } = useFetch('/api/posts')

    return {
      posts,
      loading,
      error,
      refresh: fetchData
    }
  }
}
Composition API已经加载完毕