文章目录
script setup
<script setup>
是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。当同时使用 SFC 与组合式 API 时该语法是默认推荐。相比于普通的 <script>
语法,它具有更多优势:
- 更少的样板内容,更简洁的代码。
- 能够使用纯 TypeScript 声明 props 和自定义事件。
- 更好的运行时性能 (其模板会被编译成同一作用域内的渲染函数,避免了渲染上下文代理对象)。
- 更好的 IDE 类型推导性能 (减少了语言服务器从代码中抽取类型的工作)。
基本语法
要启用该语法,需要在<script>
代码块上添加 setup attribute:
<script setup>
console.log('hello script setup')
</script>
里面的代码会被编译成组件 setup() 函数的内容。这意味着与普通的 <script>
只在组件被首次引入的时候执行一次不同,<script setup>
中的代码会在每次组件实例被创建的时候执行。
顶层的绑定会被暴露给模板
当使用 <script setup>
的时候,任何在 <script setup>
声明的顶层的绑定 (包括变量,函数声明,以及 import 导入的内容) 都能在模板中直接使用:
<script setup>
// 变量
const msg = 'Hello!'
// 函数
function log() {
console.log(msg)
}
</script>
<template>
<button @click="log">{{ msg }}</button>
</template>
import 导入的内容也会以同样的方式暴露。这意味着我们可以在模板表达式中直接使用导入的 helper 函数,而不需要通过 methods 选项来暴露它:
<script setup>
import { capitalize } from './helpers'
</script>
<template>
<div>{{ capitalize('hello') }}</div>
</template>
响应式
比较常用的ref和reactive在这个语法糖中用法不变:
- ref定义一个基本类型的响应数据
- 操作数据: xxx.value
- 读取数据: 不需要.value,直接:
<div>{{xxx}}</div>
- reactive定义一个复杂类型的响应数据
- 操作数据与读取数据:均不需要.value
这里我们还可以使用ts中的泛型来进行约束:
let person = reactive<Person>({
list:[]
})
interface Ref<T> {
value: T
}
还有toRef、toRefs等函数用法没有什么大变化
监听与计算
computed与watch的使用方法在该语法糖中也没有什么变化,但是我们需要弄清楚一个问题:在vue3中watch、computed、watchEffect使用场景有什么不同?
在vue3中,watch、computed、watchEffect都是用来监听响应式数据的变化,并执行相应的回调函数。但是它们的使用场景和区别有以下几点:
-
computed是用来计算一个派生值,它接受一个getter函数,并返回一个只读的ref对象。computed的回调函数只有在依赖的响应式数据变化时才会重新计算,否则会缓存上一次的结果。computed适合用在需要根据一些数据计算出另一个数据的场景,例如根据姓名拼接全名,或者根据购物车的商品计算总价。
-
watch是用来观察一个或多个响应式数据的变化,并执行相应的回调函数。watch接受一个响应式数据源(可以是ref、reactive、getter函数或者数组)和一个回调函数,返回一个用于停止观察的函数。watch的回调函数会在响应式数据源变化时触发,同时会传入新值和旧值作为参数。watch可以指定一些选项,例如immediate(是否立即执行回调函数)、deep(是否深度观察对象的嵌套属性)等。watch适合用在需要根据数据的变化执行一些副作用的场景,例如发送请求、操作DOM、改变其他状态等。
-
watchEffect是用来自动收集响应式数据的依赖,并执行相应的回调函数。watchEffect接受一个回调函数,返回一个用于停止观察的函数。watchEffect的回调函数会在首次执行时收集依赖,并在任何依赖变化时重新执行。watchEffect不会传入新值和旧值作为参数,也不能指定immediate或deep选项。watchEffect适合用在不需要知道具体哪个数据变化,只需要在数据变化时执行一些逻辑的场景,例如根据数据的变化更新标题、打印日志、触发自定义事件等。
使用组件
<script setup>
范围里的值也能被直接作为自定义组件的标签名使用,也就是说我们import组件之后不需要注册,可以直接使用:
<script setup>
import MyComponent from './MyComponent.vue'
</script>
<template>
<MyComponent />
</template>
这里 MyComponent 应当被理解为像是在引用一个变量。这就有点类似于JSX了。
父子组件传参(变化较大)
父 -> 子 defineProps()
父组件直接使用v-bind在子组件的标签中传递数据:
<Menu :data="data" title="我是标题"></Menu>
<template>
<div class="menu">
菜单区域 {{ title }}
<div>{{ data }}</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
title:string,
data:number[]
}>()
</script>
设置默认值:
type Props = {
title?: string,
data?: number[]
}
withDefaults(defineProps<Props>(), {
title: "张三",
data: () => [1, 2, 3]
})
为什么这里的data的值,要写成函数的返回值的形式?
这里的data要通过函数的形式返回的原因是为了避免多个组件实例共享同一个数组对象,导致数据混乱和污染。
如果我们不使用函数的形式返回data的默认值,而是直接写成data: [1, 2, 3],那么所有使用这个组件的地方都会使用同一个数组对象,如果其中一个组件实例修改了数组的内容,那么其他的组件实例也会受到影响,这会导致数据不一致和难以预测的行为。
因此,为了保证数据的独立性和安全性,我们需要使用函数的形式返回data的默认值,这样每个组件实例都会得到一个新的数组对象,而不会相互干扰。
子 -> 父 defineEmits()
具体过程:
- 在子组件中,使用defineEmits函数来定义一个分发器(emitter),它接受一个字符串数组或者一个对象作为参数,用来指定要分发的事件名称和类型。
- 在子组件中,使用emit方法来触发指定的事件,并传递相应的数据。emit方法的第一个参数是事件名称,后面的参数是要传递的数据。
- 在父组件中,使用@或者v-on指令来监听子组件发射的事件,并定义一个回调函数来处理接收到的数据。回调函数的参数是子组件传递的数据。
父组件:
<template>
<div class="layout">
<Menu @on-click="getList"></Menu>
</div>
</template>
<script setup lang="ts">
import { reactive } from 'vue';
const getList = (list: number[]) => {
console.log(list,'父组件接受子组件');
}
</script>
我们在子组件中去触发这个事件:
<template>
<div class="menu">
<button @click="clickTap">派发给父组件</button>
</div>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
const emit = defineEmits(['on-click'])
//如果用了ts可以这样两种方式
// const emit = defineEmits<{
// (e: "on-click", name: string): void
// }>()
const clickTap = () => {
emit('on-click', '123')
}
</script>
在vue3.3中对这个语法进行了加强:
const emit = defineEmits<{
'on-click':[name:string]
}>()
拓展辨析:
在 Vue 中,$emit()
触发的自定义事件和 DOM 触发的原生事件之间有一些关键区别。让我们详细探讨一下:
-
事件类型:
- DOM 事件:这些事件是浏览器原生支持的,例如点击(
click
)、输入(input
)、鼠标移动(mousemove
)等。 - 自定义事件:这些事件是由 Vue 组件自己定义的,通过
$emit()
方法触发。
- DOM 事件:这些事件是浏览器原生支持的,例如点击(
-
触发方式:
- DOM 事件:由浏览器中的 DOM 元素触发,例如用户点击按钮、输入文本等。
- 自定义事件:由组件内部的逻辑触发,通常在模板中使用
$emit()
触发。
-
事件传播:
- DOM 事件:具有冒泡机制,可以从子元素向父元素传播。
- 自定义事件:没有冒泡机制,只能在直接子组件和父组件之间传递。
-
监听方式:
- DOM 事件:可以使用
v-on
或简写的@
来监听,例如@click
。 - 自定义事件:父组件可以通过
v-on
或@
来监听子组件触发的自定义事件。
- DOM 事件:可以使用
-
用例:
- DOM 事件:适用于与浏览器原生交互,例如点击按钮、输入表单等。
- 自定义事件:适用于组件之间的通信,例如子组件向父组件传递数据或触发某些操作。
总之,DOM 事件和自定义事件在 Vue 中有不同的用途和行为,开发者需要根据具体场景选择合适的事件类型。
子组件暴露给父组件内部属性 defineExpose()
首先在子组件中定义要暴露的属性:
const list = reactive<number[]>([4, 5, 6])
defineExpose({
list
})
然后我们在父组件中通过ref来读:
<Menu ref="refMenu"></Menu>
//这样获取是有代码提示的
<script setup lang="ts">
import MenuCom from '../xxxxxxx.vue'
//注意这儿的typeof里面放的是组件名字(MenuCom)不是ref的名字 ref的名字对应开头的变量名(refMenu)
const refMenu = ref<InstanceType<typeof MenuCom>>()
console.log(refMenu.value.list )
</script>
defineOptions() 【很少用】
vue3中的defineOptions()是一个用于在
实际使用中一般来定义name
使用defineOptions()的一个例子是:
<script setup lang="ts">
import { defineOptions } from 'vue'
// 使用defineOptions()来定义Options API的选项
defineOptions({
name: 'MyComponent', // 组件的名称
data() { // 组件的数据
return {
count: 0
}
},
methods: { // 组件的方法
increment() {
this.count++
}
},
computed: { // 组件的计算属性
doubleCount() {
return this.count * 2
}
},
watch: { // 组件的侦听器
count(newValue, oldValue) {
console.log(`count changed from ${oldValue} to ${newValue}`)
}
},
mounted() { // 组件的生命周期钩子
console.log('component mounted')
}
})
</script>
<template>
<div>
<p>count: {{ count }}</p>
<p>doubleCount: {{ doubleCount }}</p>
<button @click="increment">+1</button>
</div>
</template>
这样,我们就可以在<script setup>
中使用Options API的选项,而不需要使用export default的方式。
Ref属性
使用选项式 API,引用将被注册在组件的 this.$refs
对象里:
<!-- 存储为 this.$refs.p -->
<p ref="p">hello</p>
使用组合式 API,引用将存储在与名字匹配的 ref
里:
<script setup>
import { ref } from 'vue'
const p = ref()
</script>
<template>
<p ref="p">hello</p>
</template>
注意:ref不需要使用单行绑定
如果用于普通 DOM 元素,引用将是元素本身;如果用于子组件,引用将是子组件的实例。
或者 ref
可以接收一个函数值,用于对存储引用位置的完全控制:
<ChildComponent :ref="(el) => child = el" />
关于 ref
注册时机的重要说明:因为 ref
本身是作为渲染函数的结果来创建的,必须等待组件挂载后才能对它进行访问。
this.$refs
也是非响应式的,因此你不应该尝试在模板中使用它来进行数据绑定。