Fork me on GitHub

Vue3-组合式API

官网

Vue3官网
前置Vue2

全局实例

app.provide、inject 和 runWithContext

import { inject } from 'vue'

app.provide('id', 1)

const injected = app.runWithContext(() => {
  return inject('id')
})

console.log(injected) // 1

•provide/inject 底层基于组件树的原型链传递数据;
•app.provide 提供了全局级别的依赖注入;
•runWithContext 是为了在非组件函数里“临时模拟” Vue 上下文环境,让 inject 正常工作;
image

app.config

app.config 是 Vue 应用实例上的全局配置对象。
✅ 整个应用下的所有组件、指令、插件都会共享这些配置;
✅ 注意组件内不能直接通过 this.config 访问,它属于应用实例级别;
✅ 正确用法是通过全局属性或者上下文间接获取。

app.config.globalProperties

//app.config定义
const app = createApp(App)
//通过 globalProperties 暴露给组件,全局属性命名推荐$开头
app.config.globalProperties.$apiBaseUrl = 'https://api.example.com'

//使用全局配置
//方法1: this.$xxx
console.log(this.$apiBaseUrl) // 在 options API 里
//方法2: proxy.$xxx
import { getCurrentInstance } from 'vue'
const { proxy } = getCurrentInstance()
console.log(proxy.$apiBaseUrl) // 在 setup 里

app.config.errorHandler 和 app.config.warnHandler

image

错误捕获:errorHandler

const app = createApp(App)

app.config.errorHandler = (err, instance, info) => {
  console.error('全局错误:', err)
  console.log('出错组件实例:', instance)
  console.log('错误信息:', info)
}

触发场景

<template>
  <button @click="throwError">报错</button>
</template>

<script setup>
function throwError() {
  throw new Error('按钮被点击,抛出异常!')
}
</script>

//全局错误:Error: 按钮被点击,抛出异常!
//出错组件实例: Proxy({...})
//错误信息:click event

警告捕获:warnHandler

app.config.warnHandler = (msg, instance, trace) => {
  console.warn('Vue 警告:', msg)
  console.log('出警组件实例:', instance)
  console.log('组件追踪信息:', trace)
}

//因为 notDefinedVar 未定义,Vue 会警告,warnHandler 捕获到。
<template>
  <div>{{ notDefinedVar }}</div>
</template>

app.config.performance性能追踪

开启或关闭性能追踪信息输出,方便开发者用浏览器性能面板分析 Vue 组件的渲染、更新时间。

const app = createApp(App)
app.config.performance = true  // 开启性能追踪
app.mount('#app')

app.config.compilerOptions编译配置

配置运行时编译器

const app = createApp(App)
//告诉 Vue:my- 开头的标签是外部自定义元素,Vue 不解析、不报错;
app.config.compilerOptions = {
  isCustomElement: tag => tag.startsWith('my-')
}
//是否移除模板中的 HTML 注释,true保留
app.config.compilerOptions.comments = true

app.config.optionMergeStrategies属性合并

自定义 Vue 组件选项(比如 data、methods、props、自定义配置项)在继承、混入、全局配置、组件扩展时的合并逻辑。

const app = createApp(App)
app.config.optionMergeStrategies.myOption = (parent, child) => {
  return (parent || '') + ' | ' + (child || '')
}

const mixin = {
  myOption: '全局的'
}

const comp = {
  mixins: [mixin],
  myOption: '局部的'
}
console.log(comp.myOption)  // 输出:'全局的 | 局部的'

app.config.idPrefix自动生成ID前缀

const app = createApp(App)
app.config.idPrefix = 'my-app-'

<div id="my-app-0"></div>
<div id="my-app-1"></div>

app.config.throwUnhandledErrorInProduction强制在生产模式下抛出未处理的错误

const app = createApp(App)

app.config.throwUnhandledErrorInProduction = true

setup()

setup() 是组件的初始化入口函数,在组件实例创建之前执行,用来声明响应式数据、函数、计算属性、生命周期等逻辑。
✅ 组合式 API 的入口;
✅ 定义响应式数据(ref、reactive);
✅ 定义方法、函数;
✅ 定义计算属性(computed);
✅ 使用生命周期钩子(onMounted、onUnmounted 等);
✅ 执行依赖注入(inject、provide);
✅ 最终返回的内容,模板和组件其他部分可以直接用。

<script>
<template>
  <div>
    <p>计数:{{ count }}</p>
    <button @click="increment">增加</button>
  </div>
</template>

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

const count = ref(0)

function increment() {
  count.value++
}
</script>

这里 setup() 被 <script setup>语法糖隐藏了,本质上等同于:

export default {
  setup() {
    const count = ref(0)

    function increment() {
      count.value++
    }

    return { count, increment }
  }
}

setup() 返回值

•返回的对象,属性和方法都会暴露给模板和其他逻辑;
•不返回或返回空对象,模板里啥都用不了;
<script setup> 语法糖会自动把定义的变量暴露出去。

注意事项

✅ setup() 里没有 this,组件实例还没完全生成;
✅ 可以使用 ref、reactive、computed、生命周期函数等组合式 API;
✅ 适合逻辑复用、代码组织更清晰的场景;
✅ 搭配<script setup> 语法糖,写法更简洁。

setup()访问 Props

setup 函数的第一个参数是组件的 props; 在 setup(props) 的参数里,Vue 自动把父组件传进来的 props 注入进来。
父组件:

<template>
  <Child title="你好秋先生" />
</template>

<script setup>
import Child from './Child.vue'
</script>

子组件:

<script setup>
defineProps({
  title: String
})

const props = defineProps()

console.log(props.title)  // 访问 props
</script>

或者不写 <script setup>,标准 setup() 写法:

export default {
  props: {
    title: String
  },
  setup(props) {
    console.log(props.title)
  }
}

📦 script setup 语法糖总结

•defineProps() 代替了传统 props 定义;
•直接调用 defineProps() 拿到 props 对象;
•模板里直接用,不用返回。

⚡ 注意细节

✅ props 是响应式的,但它是只读的;
✅ 可以监听 props 变化,但不能直接改:
✅ 要想基于 props 派生出可变的数据.

props.title = '新标题'  // ❌ 报错,props 是只读的
// 派生出可变的数据
const newTitle = ref(props.title)
//监听
watch(() => props.title, (newVal) => {
  console.log('父组件传入的 title 变了:', newVal)
})

setup 上下文(Context)

export default {
  setup(props, context) {
    // 透传 Attributes(非响应式的对象,等价于 $attrs)
    console.log(context.attrs)

    // 插槽(非响应式的对象,等价于 $slots)
    console.log(context.slots)

    // 触发事件(函数,等价于 $emit)
    console.log(context.emit)

    // 暴露公共属性(函数)
    console.log(context.expose)
  }
}

image

setup暴露公共属性

export default {
  setup(props, { expose }) {
    // 让组件实例处于 “关闭状态”
    // 即不向父组件暴露任何东西
    expose()

    const publicCount = ref(0)
    const privateCount = ref(0)
    // 有选择地暴露局部状态
    expose({ count: publicCount })
  }
}

<script setup>语法糖使用defineExpose函数

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

// 内部状态
const publicCount = ref(0)
const privateCount = ref(0)

// 只暴露 publicCount,privateCount 完全私有
defineExpose({
  count: publicCount
})
</script>

setup函数渲染

子组件:

import { h, ref } from 'vue'

export default {
  setup(props, { expose }) {
    const count = ref(0)
    const increment = () => ++count.value

    expose({
      increment
    })

    return () => h('div', count.value)
  }
}

父组件:

<Child ref="childRef" />
<button @click="childRef.value.increment()">加一</button>

h('div', count.value) 实际等同于:

<template>
  <div>{{ count }}</div>
</template>

只不过是用 JS 的方式写模板结构。

响应式 API:核心

<template>
  <div>
    <h2>ref: {{ count }}</h2>
    <h2>computed: {{ doubleCount }}</h2>
    <h2>reactive: {{ state.name }} - {{ state.age }}</h2>
    <h2>readonly: {{ readonlyState.name }}</h2>

    <button @click="increment">增加 count</button>
    <button @click="changeName">改名</button>
  </div>
</template>

<script setup>
import { ref, computed, reactive, readonly, watchEffect, watchPostEffect, watchSyncEffect, watch, onMounted, onUnmounted } from 'vue'

/** 1. ref:基础响应式变量 */
const count = ref(0)

/** 2. computed:计算属性 */
const doubleCount = computed(() => count.value * 2)

/** 3. reactive:响应式对象 */
const state = reactive({
  name: '秋先生',
  age: 25
})

/** 4. readonly:只读包装,无法修改 */
const readonlyState = readonly(state)

/** 5. watchEffect:立即执行,依赖变化自动重新执行 */
watchEffect(() => {
  console.log('watchEffect 监听 count:', count.value)
})

/** 6. watchPostEffect:DOM 更新后执行 */
watchPostEffect(() => {
  console.log('watchPostEffect DOM 更新完,count:', count.value)
})

/** 7. watchSyncEffect:同步执行,DOM 更新前触发 */
watchSyncEffect(() => {
  console.log('watchSyncEffect 同步监听 count:', count.value)
})

/** 8. watch:精确监听指定数据 */
watch(() => state.name, (newVal, oldVal, onCleanup) => {
  console.log(`state.name 变化:${oldVal} -> ${newVal}`)

  /** 9. onWatcherCleanup:清理逻辑示例 */
  onCleanup(() => {
    console.log('上一次监听清理了')
  })
})

/** 按钮操作 */
function increment() {
  count.value++
}

function changeName() {
  state.name = '秋老师'
}
</script>

✅ ref 适合基础类型响应式;
✅ computed 计算属性,自动依赖追踪;
✅ reactive 响应式对象,直接操作;
✅ readonly 包装只读对象,防止误改;
✅ watchEffect 自动依赖追踪,立即执行;
✅ watchPostEffect 视图更新后执行;
✅ watchSyncEffect 同步立即执行;
✅ watch 精确监听 + onCleanup 释放资源。

响应式: 工具

<script setup>
import { ref, reactive, isRef, unref, toRef, toRefs, isProxy, isReactive, isReadonly, readonly, toValue } from 'vue'

// 1. ref 创建响应式基本数据
const count = ref(10)
console.log('count是ref吗?', isRef(count))  // true

// 2. reactive 创建响应式对象
const state = reactive({
  name: '秋先生',
  age: 30
})
console.log('state是Proxy吗?', isProxy(state))       // true
console.log('state是reactive吗?', isReactive(state)) // true

// 3. readonly 创建只读对象
const readOnlyState = readonly(state)
console.log('readOnlyState是只读吗?', isReadonly(readOnlyState)) // true

// 4. unref 取出ref的值,若不是ref直接返回原值
console.log('unref(count):', unref(count)) // 10
console.log('unref(100):', unref(100))     // 100

// 5. toRef 单独把对象里的属性“转ref”
const nameRef = toRef(state, 'name')
console.log('nameRef是ref吗?', isRef(nameRef)) // true

// 修改 nameRef,相当于改 state.name
nameRef.value = 'Vue大师'
console.log('state.name:', state.name)  // Vue大师

// 6. toRefs 把整个对象每个属性都转成ref
const stateRefs = toRefs(state)
console.log('stateRefs:', stateRefs)
/*
{
  name: Ref<...>,
  age: Ref<...>
}
*/
stateRefs.age.value = 99
console.log('state.age:', state.age)  // 99

// 7. toValue 取出ref值,非ref原样返回,类似 unref
console.log('toValue(count):', toValue(count)) // 10
console.log('toValue("abc"):', toValue("abc")) // abc

</script>

响应式 API:进阶

<script setup>
import { 
  ref, shallowRef, triggerRef, customRef, 
  shallowReactive, shallowReadonly, toRaw, markRaw, 
  effectScope, getCurrentScope, onScopeDispose 
} from 'vue'

// -------------------- 1. shallowRef --------------------
const shallow = shallowRef({ name: '秋先生' })
console.log('shallowRef:', shallow.value)

// 改属性不触发视图更新(浅层)
shallow.value.name = 'Vue大师'
// 必须整个对象替换,才触发更新
shallow.value = { name: '新名字' }

// -------------------- 2. triggerRef --------------------
const manual = shallowRef(0)
function forceUpdate() {
  manual.value++  // 或手动触发
  triggerRef(manual)
}
console.log('手动触发 shallowRef 更新:', manual.value)

// -------------------- 3. customRef --------------------
const debounced = customRef((track, trigger) => {
  let value = ''
  let timer
  return {
    get() {
      track() // 依赖追踪
      return value
    },
    set(newVal) {
      clearTimeout(timer)
      timer = setTimeout(() => {
        value = newVal
        trigger() // 手动通知变化
      }, 500)
    }
  }
})

debounced.value = '输入防抖示例'
console.log('debounced:', debounced.value)

// -------------------- 4. shallowReactive --------------------
const shallowObj = shallowReactive({ deep: { count: 0 } })
shallowObj.deep.count++  // 不追踪内部对象

// -------------------- 5. shallowReadonly --------------------
const readonlyObj = shallowReadonly({ msg: '你好' })
// readonlyObj.msg = '修改' // 报错
readonlyObj.msg.newProp = '不追踪内部'

// -------------------- 6. toRaw --------------------
const reactiveObj = shallowReactive({ foo: 1 })
const raw = toRaw(reactiveObj)
console.log('原始对象:', raw)

// -------------------- 7. markRaw --------------------
const pure = markRaw({ secret: '不参与响应式' })
const obj = shallowReactive({ pure })
console.log('pure是否响应式:', toRaw(obj.pure) === obj.pure)  // true

// -------------------- 8. effectScope --------------------
const scope = effectScope()
scope.run(() => {
  const count = ref(0)
  console.log('effectScope内部count:', count.value)
})
scope.stop()  // 统一停止作用域内副作用

// -------------------- 9. getCurrentScope & onScopeDispose --------------------
effectScope().run(() => {
  if (getCurrentScope()) {
    onScopeDispose(() => {
      console.log('作用域销毁,清理资源')
    })
  }
})

</script>

image

生命周期钩子

挂前 onBeforeMount,挂后 onMounted;
更新前 onBeforeUpdate,更新后 onUpdated;
卸载前 onBeforeUnmount,卸载后 onUnmounted;
错误捕获 onErrorCaptured,渲染追踪 onRenderTracked;
渲染触发 onRenderTriggered,激活休眠 onActivated、onDeactivated;
image

依赖注入

<template>
  <div>
    <h1>👑 祖先组件</h1>
    <ParentComponent />
  </div>
</template>
<script setup>
import { provide } from 'vue'
import ParentComponent from './ParentComponent.vue'

// provide:向下传递数据
provide('msg', '这是从祖先传来的数据')
provide('count', 520)
</script>


<!-- ParentComponent.vue -->
<template>
  <div style="padding-left: 20px;">
    <h2>🧑 父组件</h2>
    <ChildComponent />
  </div>
</template>
<script setup>
import ChildComponent from './ChildComponent.vue'
</script>


<!-- ChildComponent.vue -->
<template>
  <div style="padding-left: 40px;">
    <h3>👶 子组件</h3>
    <p>收到的 msg:{{ msg }}</p>
    <p>收到的 count:{{ count }}</p>
    <p v-if="!hasContext">⚠️ 不在有效注入上下文中</p>
  </div>
</template>
<script setup>
import { inject, hasInjectionContext } from 'vue'

// 判断是否有注入上下文
const hasContext = hasInjectionContext()

// inject:获取祖先提供的数据
const msg = inject('msg', '默认值')
const count = inject('count', 0)
</script>

image

辅助函数

<template>
  <div v-bind="attrs">
    <slot></slot>

    <input :id="id" v-model="modelValue" />

    <div ref="el">我是 DOM 元素</div>
  </div>
</template>

<script setup>
import { useAttrs, useSlots, useModel, useTemplateRef, useId } from 'vue'

// 获取未被 props 接收的其他属性
const attrs = useAttrs()

// 获取<template>标签里的插槽内容(具体的内容由父组件传递给子组件)
const slots = useSlots()
console.log('默认插槽是否存在:', !!slots.default)

// 组合式的 v-model
const modelValue = useModel()

// 获取模板里 ref="xxx" 标记的 DOM 或组件
const el = useTemplateRef('el')

// 生成唯一 ID
const id = useId()
</script>
posted @ 2025-07-04 21:00  秋夜雨巷  阅读(43)  评论(0)    收藏  举报