组合式 API 是 Vue 3 中引入的一种新的 API 设计模式,它可以使开发者更好地组织和复用组件逻辑。传统的选项式 API 通过将相关的选项属性和生命周期钩子分散在不同的选项中来定义组件逻辑,而组合式 API 允许开发者通过组合逻辑功能来组织组件。
组合式 API 的核心概念是 "Composition"(组合),即将组件逻辑划分为一组独立的功能块,每个功能块都可以包含状态、计算属性、方法等。这些功能块可以在需要时通过其他组件进行组合和复用。
使用组合式 API,开发者可以更灵活地组织组件代码,将关注点分离,并使得代码更容易测试和复用。它还提供了更细粒度的控制,使开发者可以更深入地探索组件的内部实现。同时,组合式 API 也与 TypeScript 更好地集成,提供了更好的类型推断和代码补全,使得开发者在开发过程中更加高效。
总结来说,组合式 API 是 Vue 3 中引入的一种新的 API 设计模式,通过组合逻辑功能来组织和复用组件逻辑,使得组件代码更加灵活、可测试和可复用。
setup
-
setup() 钩子是在组件中使用组合式 API 的入口通常只在以下情况下使用:
-
需要在非单文件组件中使用组合式 API 时。
-
需要在基于选项式 API 的组件中集成基于组合式 API 的代码时。
-
它将接收两个参数:props 和 context 。
props
- 表示父组件给子组件传值,props 是响应式的,当传入新的 props 时,自动更新
- 因为 props 是响应式的,不能使用 ES6 解构,会消除prop的响应特性,此时需要借用 toRefs 解构;
javascript
import { defineComponent, toRefs } from 'vue';
export default defineComponent({
name: 'Button',
porps: {
data: Array
},
setup(props){
const { data } = toRefs(props);
}
})
- 使用组件时,经常会遇到可选参时,有些地方需要传递某个值,有些时候不需要,该如何处理呢?
- 如果 name 是一个可选参数,则传入 props 中可能没有 name 。在这种情况下 toRefs 将不会为 name 创建一个 ref ,需要使用 toRef 代替它。
javascript
import { defineComponent, toRef } from 'vue';
export default defineComponent({
name: 'Button',
setup(props){
const name = toRef(props, 'name');
}
})
context
- context 是一个 Setup 上下文对象 ,其中包含了 attrs, slots, emit, expose。
javascript
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)
}
}
- attrs 主要接收没在 props 里定义,但父组件又传过来的属性
javascript
import { defineComponent } from 'vue';
export default defineComponent({
name: 'Icon',
props: {
name: String
},
setup(props, { attrs }) {
return () => {
// style 没有在props中定义 但是父组件却传过来了
const { style } = attrs;
return (
<span
style={{ ...style }}
class={['iconfont', props.name]}
></span>
);
};
},
});
- slots 插槽
javascript
// 子组件
import { defineComponent } from "vue";
export default defineComponent({
name: "Child",
setup(props, { slots }) {
return () => {
// 父组件通过 v-slots 属性去定义插槽
return (
<>
{slots.header?.()}
</>
);
};
},
})
// 父组件
import { defineComponent } from 'vue'
import Child from './Child'
export default defineComponent({
name: "Father",
setup(props) {
return () => {
// 父组件通过 v-slots 属性去定义插槽
return (
<Child
v-slots={{
header: () => (
<>这是header插槽</>
)
}}
></Child>
);
};
},
})
- emit 组件通信
javascript
import { defineComponent } from "vue";
export default defineComponent({
name: "Child",
setup(props, { emit }) {
return () => {
return (
<button onClick={()=>{
// 触发父组件绑定的事件
emit('onClick')
}}>按钮</button>
);
};
},
})
- expose 暴露组件内部方法和属性
javascript
import { defineComponent, ref } from "vue";
export default defineComponent({
name: "Child",
setup(props, { expose }) {
const text = ref('')
const setText = ()=>{}
// 暴露当前组件内部的方法属性给父组件使用
expose({
setText,
text,
})
return () => {
return (
<button>按钮</button>
);
};
},
})
.vue文件中使用
- 对于结合单文件组件使用的组合式 API,推荐通过
xml
<template>
<button @click="log">{{ msg }}</button>
</template>
<script setup>
import { ref } from 'vue'
const msg = ref('hello world')
const log = () => {}
</script>
---------------------------------
// 如果不在 script标签上写 setup那么就需要显示的return 出去变量和方法 template里才可以使用
<script>
import { ref } from 'vue'
export default {
setup() {
const msg = ref('hello world')
const log = () => {}
return { msg, log };
},
};
</script>
.tsx文件中使用
- 很多组件库都使用了tsx的方式开发,主要因为其灵活性比较高 。
javascript
import { defineComponent, ref } from 'vue';
export default defineComponent({
name: 'Button',
setup(){
const count = ref(0)
const click = () => {
++count.value
}
return () => {
return (
<button onClick={click}>{count}</button>
)
}
}
})
创建响应式数据
ref
- 此对象只有一个指向其内部值的属性 .value
- 创建一个响应式数据,一般来说用于创建简单类型的响应式对象,比如String、Number...基础类型
- 也可以定义引用数据类型但是使用起来不是很方便
scss
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
reactive
- 创建一个对象的响应式代理,响应式转换是"深层"的:它会影响到所有嵌套的属性
- ref与reactive都是对数据深度监听,不管是简单类型还是复杂的对象,只要发生改变时都出触发视图更新
ini
const obj = reactive({ count: 0 })
obj.count++
shallowRef
- 对于一些结构特别复杂的对象且不需要深度响应数据的用此API 性能会好很多。
- 只监听.value属性的值的变化,对象内部的某一个属性改变时并不会触发更新,只有当更改value为对象重新赋值时才会触发更新
ini
const foo = shallowRef({
c: 1,
})
const change = () => {
foo.value.c = 2 // 视图不更新
foo.value={a:1} // 视图更新
}
shallowReactive
- 只监听对象的第一层属性,对嵌套的对象不做响应式处理
ini
const state = shallowReactive({
foo: 1,
nested: {
bar: 2
}
})
const change = () => {
state.foo = 2 // 视图更新
state.nested={count:2}// 视图更新
state.nested.bar =3 // 视图不更新
}
triggerRef
- 强制触发依赖于一个浅层 ref 的副作用,这通常在对浅引用的内部值进行深度变更后使用。
php
const shallow = shallowRef({
greet: 'Hello, world'
})
shallow.value.greet = 'Hello, universe' // 视图不会更新
triggerRef(shallow) // 手动更新相关副作用,更新视图
readonly
- 接收一个普通对象或者经过reactive、ref处理过的响应式对象,使其变为只读对象,对其中的任何数据都不能进行更改
objectivec
const original = reactive({ count: 0 })
const copy = readonly(original)
original.count++ // 仍然可以更改响应式对象
copy.count++ // 被readonly包裹后再更改会报警告
shallowReadonly
- 对象的第一层属性被设置成只读,嵌套的属仍然可以被更改
php
const state = shallowReadonly({
foo: 1,
nested: {
bar: 2
}
})
state.foo++ //警告,不可被更改
state.nested.bar++ // 嵌套的属性仍然可以被更改
toRaw
- toRaw() 可以返回由 reactive()、readonly()、shallowReactive() 或者 shallowReadonly() 创建的代理对应的原始对象。
- 这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。
arduino
const foo = {}
const reactiveFoo = reactive(foo)
console.log(toRaw(reactiveFoo) === foo) // true
markRaw
- 标记一个对象,使其永远不会变成响应式proxy,比如挂载一个第三方类库,那么这个类库不需要创建响应式,比如为一个响应式对象额外添加了一个属性用来展示列表,那么这个列表仅作展示使用,就不需要创建响应式,以避免性能的浪费
less
const foo = markRaw({})
console.log(isReactive(reactive(foo))) // 不是响应式的
自定义Ref
customRef
- 创建一个自定义的 ref,显式声明对其依赖追踪和更新触发的控制方式
- customRef() 预期接收一个工厂函数作为参数,这个工厂函数接受 track 和 trigger 两个函数作为参数,并返回一个带有 get 和 set 方法的对象。
- 一般来说,track() 应该在 get() 方法中调用,而 trigger() 应该在 set() 中调用。然而事实上,你对何时调用、是否应该调用他们有完全的控制权。
- 创建一个防抖 ref,即只在最近一次 set 调用后的一段固定间隔后再调用:
javascript
import { customRef } from 'vue'
export function useDebouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => {
return {
get() {
track() // 通知vue追踪数据的变化
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger() // 通知Vue去重新解析模板
}, delay)
}
}
})
}
// 在组件中使用
<script setup>
import { useDebouncedRef } from './debouncedRef'
const text = useDebouncedRef('hello')
</script>
<template>
<input v-model="text" />
</template>
监听
watch
- 侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。
- 第一个参数 source侦听器的源
- 第二个参数是在发生变化时要调用的回调函数。
- 第三个可选的参数是一个对象
scss
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
// 处理监听得值变更后的逻辑...
}
)
watchEffect
-
立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行
-
watchEffect 它与 watch 的区别主要有以下几点:
-
不需要手动传入依赖,每次初始化时会执行一次回调函数来自动获取依赖
-
无法获取到原值,只能得到变化后的值
scss
const count = ref(0)
watchEffect(() => {
// 自动监听其内部使用的响应式依赖更新
console.log(count.value)
})
// -> 输出 0
count.value++
// -> 输出 1
watchPostEffect
- watchEffect() 使用 flush: 'post' 选项时的别名。
watchSyncEffect
- watchEffect() 使用 flush: 'sync' 选项时的别名。
computed
- computed 表示计算属性,通常用于处理数据,简化书写在模板中的复杂逻辑。使用 computed 可以将数据处理成我们想要的格式,无需在模板中使用复杂冗长的计算表达式。
xml
<p>结果:{{sum}}</p>
<script setup>
import { ref, computed } from "vue"
const a = ref(1)
const b = ref(2)
// 自动跟踪内部响应式数据变化而变化
let sum = computed(()=>{
return a.value + b.value
})
</script>
依赖注入
- provide 和 inject 主要为高阶插件/组件库提供用例。
- 这对选项是一起使用的。以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。
- 通俗的说就是:组件得引入层次过多,我们的子孙组件想要获取祖先组件得资源,那么怎么办呢,总不能一直取父级往上吧,而且这样代码结构容易混乱。这个就是这对选项要干的事情。
- 一句话介绍:provide可以向所有子孙组件提供数据以及提供修改数据的方法,子孙组件用inject使用数据
provide / inject
- provide:是一个对象,或者是一个返回对象的函数。里面呢就包含要给子孙后代的东西,也就是属性和属性值。
- inject:一个字符串数组,或者是一个对象。属性值可以是一个对象,包含from和default默认值
- 顶级组件
xml
<template>
<div>
<son />
</div>
</template>
<script setup>
import son from "./son.vue";
import { provide, ref } from "vue";
const input = ref('s')
// 像其子孙组件注入依赖
provide("input", input);
</script>
- 子组件
xml
<template>
<div>
<grandson />
</div>
</template>
<script setup>
import grandson from "./grandson.vue";
</script>
- 孙子组件
xml
<template>
<div>我是孙子</div>
</template>
<script setup>
import { inject, watch } from "vue";
// inject()只能放在setup()生命周期里运行,不能放在别的周期里运行,也不能放在事件周期里运行。
const input = inject("input");
console.log(input);
// 监听
watch(input, () => {
console.log('变了');
});
</script>
生命周期
onBeforeMount
- 在挂载开始之前被调用:相关的 render 函数首次被调用。
onMounted
- 在组件挂载完成后执行。
- 这个钩子通常用于执行需要访问组件所渲染的 DOM 树相关的副作用
onBeforeUpdate
- 在组件即将因为响应式状态变更而更新其 DOM 树之前调用。
- 这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器
onUpdated
- 在组件因为响应式状态变更而更新其 DOM 树之后调用
- 父组件的更新钩子将在其子组件的更新钩子之后调用。
onBeforeUnmount
- 在卸载组件实例之前调用。在这个阶段,实例仍然是完全正常的。
onUnmounted
- 在组件实例被卸载之后调用
onActivated
- 注册一个回调函数,若组件实例是 缓存树的一部分,当组件被插入到 DOM 中时调用。
onDeactivated
- 注册一个回调函数,若组件实例是 缓存树的一部分,当组件从 DOM 中被移除时调用。
onErrorCaptured
- 当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false 以阻止该错误继续向上传播。
Pinia
Vue Router
Vite
ESlint 代码规范
- Vue 团队维护着 eslint-plugin-vue 项目,它是一个 ESLint 插件,会提供 SFC 相关规则的定。
VSCode插件
- Volar VSCode 插件为 Vue SFC 提供了开箱即用的格式化功能。