组合式和选项式
关于组合式和选项式 API 的区别以及优缺点可以观看。
可以在 Vue3 中使用组合式 API 编写代码,但不推荐。
setup
Vue3 提供一个setup
方法,可以在其中以组合式的风格定义变量和方法。
比如一个选项式的 vue 组件:
<template>
<div class="person">
<h2>姓名:</h2>
<h2>年龄:</h2>
<button @click="showPhone">查看手机号</button>
<button @click="changeName">修改姓名</button>
<button @click="changeAge">修改年龄</button>
</div>
</template>
<script lang="ts">
export default {
name: 'Person',
data() {
return {
name: '张三',
age: 18,
phone: 123213123,
}
},
methods: {
showPhone() {
alert(this.phone)
},
changeAge() {
this.age += 1
},
changeName() {
this.name = '王五'
}
}
}
</script>
使用setup
方法改写:
<script lang="ts">
import { ref } from 'vue'
export default {
name: 'Person',
setup() {
const name = ref('张三')
const age = ref(18)
const phone = 123213123
const showPhone = () => {
alert(phone)
}
const changeName = () => {
name.value = '王五'
}
const changeAge = () => {
age.value += 1
}
return { name, age, phone, showPhone, changeName, changeAge }
}
}
</script>
这里的 name 和 age 需要在页面显示,且随着内容变化页面视图也要改变,因此是响应式数据,所以要使用
ref
方法进行定义。
setup
方法中定义的变量和方法要在模版中使用,需要返回一个对象,并在对象中以属性形式添加。
比较特殊的是,除了通过setup
方法的返回值暴露变量和方法给模版,还可以直接返回一个渲染函数:
<script lang="ts">
import { ref } from 'vue'
export default {
name: 'Person',
setup() {
// ...
return ()=>{
return "Hello"
}
}
}
</script>
此时模版内容不会被渲染,而直接加载setup
返回值中的内容到页面。
setup
方法是一个生命周期钩子,其加载顺序要优先于beforeCreate
:
<script lang="ts">
export default {
name: 'Person',
beforeCreate(){
console.log('beforeCreate')
},
setup() {
console.log('setup')
return ()=>{
return "Hello"
}
}
}
</script>
在这个示例中,先输出setup
。
我们现在已经知道,Vue2 的选项式 API 中,变量和方法定义在data
和methods
中,而 Vue3 的组合式 API 中,变量和方法定义在 setup
中,一般而言我们只会选择其中一种风格进行编码,特殊的是,两者是可以共存的:
<script lang="ts">
import { ref } from 'vue'
export default {
name: 'Person',
data() {
return {
msg: 'hello world'
}
},
methods: {
sayHello() {
console.log('hello')
},
},
setup() {
// ...
}
}
</script>
特别的,可以在 data
或 methods
中访问 setup
定义的变量或方法:
<script lang="ts">
import { ref } from 'vue'
export default {
name: 'Person',
data() {
return {
msg: 'hello world',
anotherName: this.name
}
},
// ...
setup() {
const name = ref('张三')
// ...
return { name, age, phone, showPhone, changeName, changeAge }
}
}
</script>
因为就像前面说的,setup
钩子先加载,所以这里data
是可以通过this
引用获取到setup
中定义的变量的。
反之是不可行的,不仅是因为生命周期钩子加载顺序的问题,setup
方法中压根就没有 this
引用:
setup() {
console.log('setup',this)
// ...
}
这里会输出setup undefined
,也就是说在setup
中 this
引用不可用,从语法上就杜绝了这种可能性。
语法糖
在setup
中编写组合式 API 显得很累赘,因此 Vue 提供一个 setup 语法糖,让代码看上去更优雅。如果用语法糖改写之前的示例:
<script lang="ts">
export default {
name: 'Person123',
}
</script>
<script lang="ts" setup>
import { ref } from 'vue'
const name = ref('张三')
const age = ref(18)
const phone = 123213123
const showPhone = () => {
alert(phone)
}
const changeName = () => {
name.value = '王五'
}
const changeAge = () => {
age.value += 1
}
</script>
这里用有setup
属性的script
标签取代setup
方法,在其中定义变量和方法,并且无需显式返回。
需要注意的是,如果有需要,你依然要使用一个额外的普通script
标签以向外暴露组件名称。且export default
语句不能合并到setup
语法糖中,会报错。
可以借助一个插件解决这个问题。
先安装插件:
npm i vite-plugin-vue-setup-extend -D
修改vite
配置文件vite.config.ts
,添加插件:
import vueSetupExtend from 'vite-plugin-vue-setup-extend'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
vueSetupExtend(),
],
// ...
})
重启项目,在语法糖标签中使用name
属性定义组件名称:
<script lang="ts" name="Person456" setup>
// ...
</script>
可以使用浏览器插件vue devtools
查看子组件名称是否被修改:
响应式数据
在 Vue2 的选项式 API 中,通过data
方法返回的数据都是响应式的:
<script lang="ts">
export default {
name: 'Person',
data() {
return {
name: '张三',
age: 18,
phone: 123213123,
}
},
// ...
}
</script>
其值改变后页面的视图也会随之改变:
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
在 Vue3 的组合式 API 中,需要使用特殊的函数定义响应式数据。
首先,可以用ref
定义基础类型的响应式数据:
<script lang="ts" name="Person456" setup>
import { ref } from 'vue'
const name = ref('张三')
const age = ref(18)
const phone = 123213123
const showPhone = () => {
alert(phone)
}
const changeName = () => {
name.value = '王五'
}
const changeAge = () => {
age.value += 1
}
</script>
在使用这些响应式数据时,需要访问其value
属性,比如xxx.value
,而不是直接使用。
如果通过控制台打印:
console.log(name)
会发现是一个RefImpt
对象,用户关心的值保存在value
属性中。
特别的,在模版中使用ref
定义的响应式数据时,不需要使用xxx.value
:
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
可以使用reactive
定义对象类型的响应式数据:
<script lang="ts" name="Person456" setup>
import { reactive } from 'vue'
const person = reactive({
name: '张三',
age: 18,
phone: '123213123'
})
const showPhone = () => {
alert(person.phone)
}
const changeName = () => {
person.name = '王五'
}
const changeAge = () => {
person.age += 1
}
console.log(person)
</script>
在控制台打印其类型可以看到:
这是一个代理对象。
使用reactive
定义的响应式数据不需要使用value
属性,但需要注意的是,如果你要替换整个相应式数据对象,而不是仅仅修改其中某个属性,不能采用如下做法:
let person = reactive({
name: '张三',
age: 18,
phone: '123213123'
})
person = {
name: '李四',
age: 20,
phone: '55555555'
}
因为这样会导致 person 对象从一个响应式数据变成普通对象,相应的,数据改变后模版中的视图也就不会发生变化。
当然,你也可以用一个新的 reactive
定义的响应式数据替换原有的:
let person = reactive({
name: '张三',
age: 18,
phone: '123213123'
})
person = reactive({
name: '李四',
age: 20,
phone: '55555555'
})
但这并不常见,且可能导致一些新的问题,比如:
<script lang="ts" name="Person456" setup>
import { reactive } from 'vue'
let person = reactive({
name: '张三',
age: 18,
phone: '123213123'
})
const showPhone = () => {
alert(person.phone)
}
const changeName = () => {
person.name = '王五'
}
const changeAge = () => {
person.age += 1
}
const changePerson = () => {
person = reactive({ name: '李四', age: 20, phone: '5555555' })
}
console.log(person)
</script>
这会产生一个奇怪的现象,changeName
和changeAge
在页面加载后都可以正常工作,但如果changePerson
被触发,changeAge
和changeName
就失效了。
为了避免响应式数据被意外改写,通常我们会将其定义为常量(const),因此直接修改属性更为常见:
<script lang="ts" name="Person456" setup>
import { reactive } from 'vue'
const person = reactive({
name: '张三',
age: 18,
phone: '123213123'
})
// ...
const changePerson = () => {
person.name = '李四'
person.age = 20
person.phone = '5555555'
}
</script>
这样做有其局限性,如果一个对象属性过多,或者其要被替换的值来自于一个已有对象(比如通过接口获取到的),这样就很繁琐且不现实。
可以使用Object.assign
简化对目标对象属性的赋值操作:
const changePerson = () => {
Object.assign(person, { name: '王五', age: 20, phone: '5555555' })
}
ref
不仅可以将基本类型包装成响应式数据,也可以将对象类型包装成响应式数据:
const person = ref({
name: '张三',
age: 18,
phone: '123213123'
})
和包装基本类型时类似,其类型为RefImpl
,对象数据保存在value
属性中,是一个代理对象:
同样的,修改属性值的时候需要通过value
属性:
const showPhone = () => {
alert(person.value.phone)
}
const changeName = () => {
person.value.name = '王五'
}
const changeAge = () => {
person.value.age += 1
}
与reactive
不同,替换被包装的对象不需要借助Object.assign
,直接对value
属性赋值即可:
const changePerson = () => {
person.value = { name: '王五', age: 20, phone: '5555555' }
}
总结,对于基本类型,只能使用ref
将其转换为响应式数据,对于对象,可以使用ref
或reactive
,但一般而言,为了避免出现大量的.value
,对于属性较多的对象,比如表单绑定的数据对象(formData
),推荐使用reactive
。
此外,有一个小技巧,如果你觉得使用ref
时频繁需要.value
很不方便,可以借助插件来简化:
勾选上面的插件设置后,在编辑器中使用ref
定义的响应式数据时会自动添加.value
。
ref 属性
通常我们会通过在 HTML 标签行添加 id 属性,并通过原生的document.getElementById
方法获取对应的 DOM 节点:
<button id="btnShowPhone" @click="showPhone">查看手机号</button>
<script lang="ts" name="Person456" setup>
import { ref,onMounted } from 'vue'
const btnChangeName = ref()
onMounted(()=>{
const btnShowPhone = document.getElementById('btnShowPhone')
console.log('btnShowPhone',btnShowPhone)
})
注意,在 Vue 中,
document.getElementById
必须在onMounted
生命周期钩子中调用,这样才能确保 DOM 树已经完成加载。否则获取到的可能是null
。
这样做有一些缺点,比如前端项目过于庞大(根组件加载了很多子组件),就可能出现 id
被重复定义的问题。
Vue 提供了一个更简洁的做法,使用ref
属性定义标签:
<button ref="btnChangeName" @click="changeName">修改姓名</button>
通过ref
方法获取 DOM 节点对应的响应式数据:
<script lang="ts" name="Person456" setup>
import { ref,onMounted } from 'vue'
const btnChangeName = ref()
onMounted(()=>{
console.log('btnChangeName',btnChangeName.value)
})
同样需要注意的是,虽然通过
ref
方法获取响应式数据可以在setup
中定义,但是通过响应式数据的.value
属性获取 DOM 节点的内容同样需要在生命周期钩子onMounted
中执行,否则可能获取到的是undefined
。
toRefs
看一个示例:
<template>
<div class="person">
<h2>姓名:{{ person.name }}</h2>
<h2>年龄:{{ person.age }}</h2>
<button @click="changeName">修改姓名</button>
<button @click="changeAge">修改年龄</button>
</div>
</template>
<script lang="ts" name="Person456" setup>
import { reactive } from 'vue'
const person = reactive({
name: '张三',
age: 18
})
const changeName = () => {
person.name += '~'
}
const changeAge = () => {
person.age += 1
}
</script>
如果对reactive
定义的响应式对象进行结构赋值:
<script lang="ts" name="Person456" setup>
import { reactive } from 'vue'
const person = reactive({
name: '张三',
age: 18
})
let {name,age} = person
const changeName = () => {
name += '~'
}
const changeAge = () => {
age += 1
}
</script>
获取到的name
和age
是普通变量,因此按钮点击后页面视图不会发生变化。
可以通过toRefs
方法将响应式对象中的每个属性都转化为响应式数据,这样再结构赋值后获取到的就是响应式数据,视图会随着数据的改变而改变:
<script lang="ts" name="Person456" setup>
import { reactive,toRefs } from 'vue'
const person = reactive({
name: '张三',
age: 18
})
let {name,age} = toRefs(person)
如果打印console.log(toRefs(person))
,就会发现:
{name: ObjectRefImpl, age: ObjectRefImpl}
此外,比较特别的是,通过这样方式获取到的响应式数据,与原始响应式数据是有关联的,比如:
const changeName = () => {
name.value += '~'
console.log(name.value, person.name)
}
输出:
张三~ 张三~ 张三~~ 张三~~ 张三~~~ 张三~~~
如果你只需要获取响应式对象中的某个属性的响应式数据,可以使用toRef
方法:
import { reactive,toRef } from 'vue'
const person = reactive({
name: '张三',
age: 18
})
let name = toRef(person, 'name')
let age = toRef(person, 'age')
本文的完整示例可以从获取。
The End.
参考资料
文章评论