【Vue 3 】——setup、ref、watch
Vue 3 核心知识点全面解析
一、Vue 3 简介
Vue 3 是下一代前端框架 Vue.js 的重大版本更新,于 2020 年 9 月正式发布。它带来了许多令人兴奋的新特性和改进,旨在解决 Vue 2 在大型项目中的规模限制,并提供更好的性能。
Vue 3 的核心亮点:
- 更强的性能:重写了虚拟 DOM,优化了编译过程,使得打包体积更小,运行速度更快。
- 更好的 TypeScript 支持:Vue 3 的代码库完全使用 TypeScript 重写,提供了完美的类型推断。
- Composition API:引入了一套新的、更具弹性的 API,用于组织和复用代码逻辑,这是 Vue 3 最重要的特性之一。
- 新的内置组件:如
<Fragment>
,<Teleport>
,<Suspense>
等,用于处理更复杂的 UI 场景。 - 按需引入:支持更好的 Tree-Shaking,未使用的功能不会被打包到最终产物中。
二、创建 Vue 3 工程
官方推荐使用 Vite
或 vue-cli
来创建项目。Vite 提供了更快的启动和热更新速度。
使用 Vite 创建项目:
# npm
npm create vite@latest my-vue-app -- --template vue# yarn
yarn create vite my-vue-app --template vue# pnpm
pnpm create vite my-vue-app --template vue
然后进入项目目录,安装依赖并运行:
cd my-vue-app
npm install
npm run dev
使用 Vue CLI 创建项目:
npm install -g @vue/cli
vue create my-vue-app
# 在提示中选择 Vue 3 预设
cd my-vue-app
npm run serve
三、编写 App 组件
创建项目后,主入口文件 main.js
会使用新的 createApp
工厂函数来初始化应用:
// main.js
import { createApp } from 'vue'
import App from './App.vue'const app = createApp(App)
app.mount('#app')
根组件 App.vue
是一个典型的单文件组件 (SFC):
<!-- App.vue -->
<template><div><h1>{{ title }}</h1><MyComponent /></div>
</template><script>
import MyComponent from './components/MyComponent.vue'export default {name: 'App',components: {MyComponent},data() {return {title: 'Welcome to My Vue 3 App!'}}
}
</script><style>
h1 {color: #42b883;
}
</style>
四、一个简单的效果
让我们实现一个简单的计数器组件,感受 Vue 3 的响应式特性。
<template><div><p>Count is: {{ count }}</p><button @click="increment">Increment</button></div>
</template><script>
import { ref } from 'vue'export default {setup() {const count = ref(0)const increment = () => {count.value++}return {count,increment}}
}
</script>
vscode同时修改多列:shift+alt拖动选择列,然后输入可以在多行输入相同内容
五、Options API 与 Composition API
这是 Vue 3 中两种组织组件代码的方式。
- Options API (Vue 2 风格):通过不同的选项属性 (
data
,methods
,computed
,watch
,生命周期
等) 来组织代码。逻辑关注点分散在各个选项中。
export default {data() {return { count: 0 }},methods: {increment() { this.count++ }},mounted() {console.log('Component is mounted!')}}
- Composition API:使用导入的函数来定义组件逻辑。它允许我们根据逻辑关注点而不是选项类型来组织代码,使得代码更易于阅读和维护,尤其是大型组件。
import { ref, onMounted } from 'vue'export default {setup() {const count = ref(0)function increment() { count.value++ }onMounted(() => {console.log('Component is mounted!')})return { count, increment }}}
Composition API 的优势:
- 更好的逻辑复用能力 (自定义 Composable 函数,类似 React Hooks)
- 更灵活的代码组织
- 更好的 TypeScript 支持
六、setup
概述
setup
是 Composition API 的入口和舞台。它是一个组件选项,在组件被创建之前、props
被解析之后执行。
1、数据
2、方法
return相当于把数据交出去
注意:
- 执行时机:在
beforeCreate
生命周期之前执行,只执行一次。 this
的用法:在setup
内部,this
不是该活跃实例的引用,因为setup
是在组件被完全初始化之前调用的。所有对数据和函数的访问都直接通过响应式引用和上下文对象。- 参数:它接收两个参数:
props
:响应式的 props 对象。context
:一个普通 JavaScript 对象,暴露了三个组件属性:attrs
,slots
,emit
。
export default {props: {title: String},setup(props, context) {console.log(props.title) // 访问 propsconsole.log(context.attrs) // 等价于 this.$attrsconsole.log(context.slots) // 等价于 this.$slotsconsole.log(context.emit) // 等价于 this.$emit// 在这里声明响应式数据、方法、计算属性、生命周期钩子等// 最后必须返回一个对象,该对象上的属性可以在模板中使用return { ... }}
}
七、setup
的返回值:return
setup
必须返回一个对象。这个对象包含所有你想要在模板中使用的属性(响应式数据、方法等)。这个对象会被合并到组件的实例上,模板可以直接访问它们。
setup() {const message = ref('Hello World!')const count = ref(0)function changeMessage() {message.value = 'Vue 3 is awesome!'}// 返回的对象可以在模板中使用return {message,count,changeMessage}
}
八、setup
与 Options API
setup
和传统的 Options API (data
, methods
等) 可以共存。在 setup
中返回的属性会被暴露到 this
上,并且可以被 Options API 访问。
data可以用setup的数据
但setup不可以用data的数据
最好不要混合使用
export default {data() {return {dataCount: 1}},setup() {const setupCount = ref(100)return { setupCount }},mounted() {console.log(this.dataCount) // 1 (来自 Options API)console.log(this.setupCount) // 100 (来自 setup)}
}
然而,通常建议在同一个组件中只选择一种风格,以避免不必要的混淆。
九、setup
的语法糖 <script setup>
为了简化 setup
的使用,Vue 3 提供了 <script setup>
语法糖。这是编译时语法糖,代码更简洁。
传统写法 vs <script setup>
:
<!-- 传统写法 -->
<script>
import { ref } from 'vue'export default {setup() {const count = ref(0)return { count }}
}
</script>
<!-- 使用 <script setup> -->
<script setup>
import { ref } from 'vue'const count = ref(0)
// 任何顶层绑定 (变量、函数导入) 都可以直接在模板中使用
</script>
拓展:配置组件名字
一、多写一个script标签
二、下载一个插件
然后配置vite.config.ts
再设置name属性
在 <script setup>
中:
- 不需要手动return模板需要使用的数据和方法。
- 引入的组件可以直接在模板中使用,无需注册。
- 使用
defineProps
和defineEmits
来声明props
和emits
。 - 使用
useSlots
和useAttrs
来获取slots
和attrs
。
<script setup>
import { ref } from 'vue'
import MyComponent from './MyComponent.vue'// 定义 props 和 emits
const props = defineProps({title: String
})
const emit = defineEmits(['change'])const count = ref(0)function increment() {count.value++emit('change', count.value)
}
</script><template><h1>{{ title }}</h1><MyComponent /><button @click="increment">{{ count }}</button>
</template>
十、ref
创建:基本类型的响应式数据
ref
函数用于创建一个响应式的引用。它接收一个内部值(可以是基本类型或对象),返回一个响应式的、可更改的 ref 对象。这个对象只有一个 .value
属性,指向其内部值。
主要用于处理基本类型数据 (String, Number, Boolean, etc.)。
import { ref } from 'vue'const count = ref(0) // 创建一个值为 0 的响应式 ref
console.log(count.value) // 0count.value++ // 在 JavaScript 中需要通过 .value 来访问和修改
console.log(count.value) // 1
在模板中(<template>),Vue 会自动解包,无需使用 .value
:
在<script>里ref包裹的数据要.value
<template><div>{{ count }}</div> <!-- 直接使用,不需要 .value -->
</template>
十一、reactive
创建:对象类型的响应式数据
reactive
函数用于创建一个响应式的对象。它接收一个普通对象,返回该普通对象的响应式代理。
主要用于处理对象或数组等引用类型数据。
import { reactive } from 'vue'const state = reactive({count: 0,user: {name: 'Alice',age: 30},hobbies: ['reading', 'music']
})// 访问和修改
console.log(state.count) // 0 (直接访问属性,不需要 .value)
state.count++
state.user.name = 'Bob'
state.hobbies.push('coding')
小技巧:
选中按括号可以直接包裹
十二、ref
创建:对象类型的响应式数据
ref
也可以用来创建对象类型的响应式数据。当你使用 ref(null)
或 ref({ ... })
时,内部会调用 reactive
方法来使值具有响应性。
ref 里的对象 其实还是用reactive 实现的
使用ref定义需要.value
注意:reactive
返回的是原始对象的 Proxy,它们是不相等的: console.log(state === originalObject) // false
import { ref } from 'vue'const state = ref({count: 0,user: {name: 'Alice'}
})// 在 JS 中访问和修改需要使用 .value
console.log(state.value.count) // 0
state.value.count = 1
state.value.user.name = 'Bob'
在模板中同样会自动解包:
<template><div>{{ state.count }}</div> <!-- 自动解包,无需 .value -->
</template>
十三、ref
对比 reactive
特性 | ref | reactive |
---|---|---|
数据类型 | 基本类型、对象类型 | 仅对象类型 (Array, Object, Map, Set) |
JS 中访问 | 需要 .value | 直接访问 |
模板中访问 | 自动解包,无需 .value | 直接访问 |
重新赋值 | 可以用 .value 整体替换 | 不能直接整体替换,会失去响应性 |
TypeScript | 需用 Ref<T> 类型 | 直接用接口类型即可 |
适用场景 | 基本类型数据、可能需要整体替换的引用数据 | 不需要整体替换的复杂对象、JSON 数据 |
如何选择?
- 推荐:优先使用
ref
,因为它更统一,可以处理所有类型。 - 当你确定一个对象永远不会被整体替换,只是想深度响应式地更新其属性时,可以使用
reactive
。
拓展:区别中的第一个
ref使用插件
在设置中√上就可以了
他就会自动补全value
区别中的第二个
不能重新分配对象,因为他就不是原来的那个对象了
解决方法
对于ref是可以直接修改的,因为.value就是响应式数据
十四、toRefs
与 toRef
不加toRefs,数据不会改变
正确写法
当你想解构一个响应式对象而不丢失其响应性时,这两个工具函数非常有用。
toRefs
: 将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。
import { reactive, toRefs } from 'vue'const state = reactive({count: 0,name: 'Vue'})// 直接解构会失去响应性!// let { count, name } = state// 使用 toRefs 解构,保持响应性let { count, name } = toRefs(state)// 现在 count 和 name 都是 ref,需要 .valuecount.value++ // state.count 也会变成 1
toRef
: 为源响应式对象上的某个属性创建一个 ref。这个 ref 是响应式的,并且会保持和源属性的同步。
import { reactive, toRef } from 'vue'const state = reactive({count: 0})const countRef = toRef(state, 'count')// countRef 是一个 ref,修改它的 .value 会更新 state.countcountRef.value++ console.log(state.count) // 1
toref是单独解构,torefs是全部解构
解构之后原来的reactive就会变成ref对象,就需要.value
<script setup>
中解构 props 的经典用法:
<script setup>
import { toRefs } from 'vue'const props = defineProps({title: String,user: Object
})// 解构 props,保持响应式连接
const { title, user } = toRefs(props)// 或者只解构其中一个
const titleRef = toRef(props, 'title')
</script>
十五、computed
计算属性
基本使用方法
2、computed是有缓存的,但是方法函数没有
全名打印了多次,但1只打印了一次,只有发生改变是才会重新计算
computed
函数用于创建计算属性,它接收一个 getter 函数,返回一个只读的响应式 ref 对象。
import { ref, computed } from 'vue'const count = ref(0)
const doubleCount = computed(() => count.value * 2)console.log(doubleCount.value) // 0
count.value++
console.log(doubleCount.value) // 2
可写的计算属性: 传入一个带有 get
和 set
函数的对象。
const firstName = ref('John')
const lastName = ref('Doe')const fullName = computed({get() {return `${firstName.value} ${lastName.value}`},set(newValue) {[firstName.value, lastName.value] = newValue.split(' ')}
})fullName.value = 'Jane Smith' // 会触发 setter
console.log(firstName.value) // 'Jane'
console.log(lastName.value) // 'Smith'
十六、watch
监视
watch
函数用于在响应式数据变化时执行副作用操作。
情况一:监视单个 ref
不用.value,因为他监视的是这个响应式数据,不是值
import { ref, watch } from 'vue'const count = ref(0)watch(count, (newValue, oldValue) => {console.log(`count changed from ${oldValue} to ${newValue}`)
})
结束监视就去调他的返回值
情况二:监视ref定义的对象数据类型
实际开发中一班不管旧数据
拓展:
情况三:监视 reactive
对象
直接监视整个 reactive
对象会默认开启深度监视,且 newValue
和 oldValue
会是同一个对象(Proxy)。
const state = reactive({ count: 0, user: { name: 'A' } })watch(state, (newValue, oldValue) => {// 深度监视已开启,state.user.name 变化也会触发console.log('state changed:', newValue)console.log(newValue === oldValue) // true (都是 Proxy)
})
情况四:监视 reactive
对象的某个属性
如果需要监视嵌套属性,需要使用 getter 函数。
错误写法:
正确写法
const state = reactive({ count: 0, user: { name: 'A' } })// 监视一个嵌套属性
watch(() => state.user.name,(newName, oldName) => {console.log(`Name changed: ${oldName} -> ${newName}`)}
)// 监视多个嵌套属性
watch([() => state.count, () => state.user.name],([newCount, newName], [oldCount, oldName]) => {console.log('Count or Name changed')}
)
情况五:立即执行与深度监视
watch
的第三个参数是一个配置对象:
immediate: true
:在侦听器创建时立即触发回调。deep: true
:强制深度遍历源,即使它是 ref 或 getter 返回的对象。
const state = reactive({ user: { name: 'Unknown' } })watch(() => state.user,(newUser) => {console.log('User changed:', newUser)},{ immediate: true, deep: true } // 注意:这里监视的是 state.user 这个对象引用。// 如果不加 deep: true,修改 state.user.name 不会触发回调,因为对象引用没变。
)
十七、watchEffect
watchEffect
会立即执行传入的函数,同时会自动追踪该函数依赖的所有响应式属性,并在它们发生变化时重新运行该函数。
- 无需明确指定监视源,自动收集依赖。
- 立即执行。
- 没有
newValue
和oldValue
。
import { ref, watchEffect } from 'vue'const count = ref(0)
const name = ref('Alice')watchEffect(() => {// 这个函数用到了 count 和 name,它们都是依赖console.log(`Effect triggered: Count is ${count.value}, Name is ${name.value}`)
})
// 立即打印: "Effect triggered: Count is 0, Name is Alice"count.value++
// 重新执行,打印: "Effect triggered: Count is 1, Name is Alice"
停止侦听器: watch
和 watchEffect
都会返回一个停止函数,调用它可以手动停止侦听。
const stop = watchEffect(() => { ... })// 当不再需要此侦听器时:
stop()
副作用清除: watchEffect
的回调函数可以接收一个 onCleanup
函数作为参数,用于注册清理回调。
watchEffect((onCleanup) => {const timer = setTimeout(() => {// 做一些异步操作}, 1000)// 在下次重新执行前,或停止侦听时,会先调用这个函数onCleanup(() => {clearTimeout(timer)})
})
watch
vs watchEffect
:
watch
:需要明确指定监视源,可以访问变化前后的值,惰性(除非设置immediate
)。watchEffect
:自动收集依赖,无法获取旧值,立即执行。常用于处理不需要知道前后值的副作用。
总结
Vue 3 的 Composition API 通过 ref
, reactive
, computed
, watch
, watchEffect
等函数,为我们提供了更强大、更灵活的方式来组织和复用组件逻辑。结合 <script setup>
语法糖,代码变得非常简洁和直观。理解这些核心概念是掌握 Vue 3 开发的关键。建议在实际项目中多多练习,以加深理解和运用。