如果发现 有问题 或者觉得 有常用但是没写到 的地方,欢迎留言讨论,我会及时改正与添加
关于Composition Api(组合式API)
Vue3.x 带来了一个全新特性——
Composition API(组合式API),理解是为了实现函数的聚合逻辑,逻辑复用,而产生的。
回顾Option Api
Option Api的缺陷
- 随着业务复杂度越来越高,代码量会不断的加大;
- 由于相关业务的代码需要遵循option的配置写到特定的区域;
- 如果没有非常好的约束,会导致后续维护非常的复杂,代码可复用性也不高。
Composition Api
Composition Api让相关功能的代码更加有序的组织在一起,聚合逻辑!
按官网分类包括:
- setup
- 生命周期钩子:除了
beforeCreate跟created不在setup里之外,其他都是前面加个on,在setup里调用,例:onMounted - Provide / Inject
- getCurrentInstance(详见官网): 支持访问内部组件实例,适合高阶使用场景,典型的比如在库中。。
Vue3.x 新增
composition api,但也向下兼容了option api,可以混用, 但是从理念上来说,更加推荐setup的方式,来写我们的组件。
原因如下:Vue3的存在,本身是为了解决Vue2的问题的,Vue2的问题就是在于,聚合性不足,会导致代码越来越臃肿!setup的方式,能够让data、方法逻辑、依赖关系等聚合在一块,更方便维护。
Setup
- 在执行
setup函数的时候,还没有执行beforeCreated,所以在setup函数中,无法使用data和methods的变量和方法,setup中this指向undefined
- setup参数:
{ Data } props,{ attrs, emit, slots, expose } context
emit: 就是 vue2.x 的this.$emit....用来触发父组件的方法attrs: 就是 vue2.x 的this.$attrs..就是组件本身挂载的属性(除 class 和 style 除外的非 props 属性集合)slots: 就是 vue2.x 的this.$slots..记录插槽的信息(带有dom的属性)expose(3.2+) 类似于 setup 中的return{}. 在使用模板渲染函数后,return 被占用 就需要使用 expose 将属性暴露出去(是否好用?)
若要对传递给
setup()的参数进行 类型推导,你需要使用 defineComponent详见官网 或者 Vue 中的 defineComponent
export default defineComponent ({
// 需要先声明 props,才能在setup中取值
props: {
title: String
},
setup( props, { attrs, emit, slots, expose }) {
// Attribute (非响应式对象,等同于 $attrs)
console.log(attrs)
// 插槽 (非响应式对象,等同于 $slots)
console.log(slots)
// 触发事件 (方法,等同于 $emit)
console.log(emit)
// 暴露公共 property (函数)
console.log(expose)
const reset = () => {
// 某些逻辑
}
expose({
reset
})
console.log(props.title)
return { }
}
})
逻辑聚合,关注点分离(共识)
应该分两层意思:
第一层意思,Vue3的setup,本身就把相关的数据,处理逻辑放到一起,这就是一种关注点的聚合,更方便我们看业务代码。
第二层意思,就是当setup变得更大的时候,我们可以在setup内部,提取相关的一块业务,做到第二层的关注点分离。
import { defineComponent, ref, computed } from 'vue'
import useUserInfo from './useUserInfo.ts'
export default defineComponent({
name: 'Gift',
setup() {
const counter = ref(0)
const onClick = () => {
router.push({ name: "AddGift" })
}
// 在此示例中,把 userInfo 的相关业务分离出去。也就是下面的 useUserInfo.ts
const { userInfo } = useUserInfo()
return {
counter,
onClick,
userInfo
}
}
})
useUserInfo.ts( hooks )
import { getUserInfo } from "@/api/gift"
import { ref, onMounted } from "vue"
export default function useUserInfo() {
const userInfo = ref([])
const fetchUserInfo = async () => {
let res = await getUserInfo()
userInfo.value = res.data
}
onMounted(fetchUserInfo)
return {
userInfo
}
}
这种方式避免了将功能逻辑都堆叠在setup的问题,我们可以将独立的功能写成单独的函数
<script setup>
script setup已在vue3.2的版本上发布
// 隐性setup的script
<script setup>
import {reactive} from "vue";
const states = reactive({
name: 'xiaoming',
age: 18,
fun(){
console.log('xxxxx~')
}
})
// 也可以单独直接定义
const { name,age,fun } = states
</script>
// 优点: 更少的模板代码,简洁
// 缺点: 所有定义的变量都会暴露出去(是否必要?); 没有这两个`name` `inheritAttrs`
关于 <script setup> 介绍,可以看这篇 知乎:现在Vue3的script setup体验如何?
视频介绍的不错,
但是更新一点,Vue3.2 版本中<script setup>结束实验特性,已经是 稳定特性(传送门)
不过我们应当避免两种混用在一个文件里:
// bad
<script setup>
</script>
// 显性setup的script
<script>
export default {
setup(){
return{
//此时,这里暴露出的对象,无法被temp读取
}
}
}
</script>
生命周期函数
在
setup中使用生命周期钩子,除了BeforeDestroy变成了onBeforeUnmount;destroyed变成了onUnmounted之外,其他都是前面加个on,例:onMounted
生命周期钩子中使用 async/await
setup() {
const users = ref([])
onBeforeMount(async () => {
const res = await loadData()
users.value = res.data
console.log(res)
})
return {
users,
}
},
引申出使用 async/await,
setup() {
async function handleSubmit() {
try {
// this parse may fail
const values = JSON.parse(await validate())
setModalProps({ confirmLoading: true })
// TODO custom api
console.log(values)
} catch (err) {
console.log(err)
}
}
return {
handleSubmit
}
}
使用 async/await,也可以利用 Suspense 新增 包裹你的组件, 是否谨慎使用/避免使用
<template>
<suspense>
<router-view></router-view>
</suspense>
</template>
export default {
async setup() {
// 在 `setup` 内部使用 `await` 需要小心
// 因为大多数组合式 API 函数只会在,第一个 `await` 之前执行
const data = await loadData()
// 它隐性地包裹在一个 Promise 内
// 因为函数是 `async` 的
return {
// ...
}
}
}
关于Reactivity API(响应式API)
Reactive && Ref
ref我们用来将基本数据类型定义为响应式数据,其本质是基于Object.defineProperty()重新定义属性的方式来实现
reactive用来将引用类型定义为响应式数据,其本质是基于Proxy实现对象代理
<template>
<p> {{ name }} </p>
</template>
<script >
import { reactive, toRefs } from "vue";
export default {
setup(){
const obj = reactive({
name: 'xiaoming',
gender: '男',
age: 18
})
return{
// 直接解构相当于 ...obj ==> name:obj.name
...toRefs(obj)
}
}
}
</script >
如果直接解构,name 是 string,失去了响应性
使用 toRefs 详见官网 可以看到name是一个响应式字符串。可以在不失去响应性的情况下解构
toRefs会将我们一个响应式的对象转变为一个普通对象,然后将这个普通对象里的每一个属性变为一个响应式的数据
引申
既然如此,我们就可以将处理 同一块业务逻辑 的变量定义在一起,甚至是方法
<script >
import { reactive, toRefs } from "vue";
export default {
setup(props){
// 处理相同业务逻辑
const state = reactive({
isMoving: false,
distance: '',
startTime: 0,
recordList: [],
wrapper: {},
// 甚至可以
xxCallBack(){
// do ...
},
})
// 可以这么做,取决于你是否愿意
const login = reactive({
param: {
username: '123',
password: '123456',
},
rules: {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
},
login(){
this.param.username = 'inline'
this.param.password = '123456'
console.log('成功登录!')
}
})
return{
...toRefs(state),
// 避免这里一堆变量
...toRefs(login)
}
}
}
</script>
Readonly
readonly 接受一个对象(响应式或纯对象) 或
ref并返回原始对象的 只读代理。只读代理是 深层的:任何被访问的嵌套 property 也是只读的。
<script >
import { readonly, reactive } from "vue";
export default {
setup(){
const original = reactive({ count: 0 })
const copy = readonly(original)
// 变更副本将失败并导致警告
copy.count++ // 警告!
return{ }
}
}
</script >
Computed & watch
Computed
Vue 3.x中的 computed 也支持getter和setter
import { reactive, ref, toRefs, computed } from 'vue'
export default {
setup () {
const state = reactive({
count: 0,
double: computed(() => {
return state.count * 2
})
})
const num = ref(0)
const addCount = function () {
state.count++
}
const addNum = function () {
num.value++
}
// only getter
const totalCount = computed(() => state.count + num.value)
// getter & setter
const doubleCount = computed({
get () {
return state.count * 2
},
set (newVal) {
state.count = newVal / 2
}
})
return {
...toRefs(state),
num,
totalCount,
doubleCount,
addCount,
addNum
}
}
}
Watch
watch(source, callback, {Object}[options])
3.x和2.x的watch一样,也支持immediate和deep选项,但3.x不再支持'obj.key1.key2'的"点分隔"写法;
3.x中watch支持监听单个属性,也支持监听多个属性
默认情况下,watch 是惰性的, 那什么情况下不是惰性的, 可以立即执行回调函数呢? 给第三个参数中设置immediate: true即可注册后会立即调用
import { reactive, ref, toRefs, computed, watch } from 'vue'
export default {
setup () {
const state = reactive({
count: 0,
double: computed(() => {
return state.count * 2
}),
midObj: {
innerObj: {
size: 0
}
}
})
const num = ref(0)
const totalCount = computed(() => state.count + num.value)
const addCount = function () {
state.count++
}
const addNum = function () {
num.value++
}
// 监听单个属性
// 侦听器数据源可以是一个具有返回值的 getter 函数,也可以直接是一个ref
watch(() => totalCount.value, (newVal, oldVal) => {
console.log(`count + num = ${newVal}`)
})
// 监听单个属性, immediate
watch(() => totalCount.value, (newVal, oldVal) => {
console.log(`count + num = ${newVal}, immediate=true`)
}, {
immediate: true
})
// 监听单个属性, deep
watch(() => state.midObj, (newVal, oldVal) => {
console.log(`state.midObj = ${JSON.stringify(newVal)}, deep=true`)
}, {
deep: true
})
setTimeout(() => {
state.midObj.innerObj.size = 1
}, 2000)
// 监听多个属性
watch([num, () => totalCount.value], ([numVal, totalVal], [oldNumVal, OldTotalVal]) => {
console.log(`num is ${numVal}, count + num = ${totalVal}`)
})
return {
...toRefs(state),
num,
totalCount,
addCount,
addNum
}
}
}
watch demo 传送门:https://sfc.vuejs.org/
Stop 停止监听
在组件中创建的watch监听,会在组件被销毁时自动停止。如果在组件销毁之前我们想要停止掉某个监听, 可以调用watch()函数的返回值,
const stopTotalCount = watch(() => totalCount.value, (newVal, oldVal) => {
console.log(`count + num = ${newVal}`)
})
setTimeout(()=>{
// 停止监听
stopTotalCount()
}, 5000)
WatchEffect
立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
import { defineComponent, ref, reactive, toRefs, watchEffect } from "vue"
export default defineComponent({
setup() {
const state = reactive({ nickname: "xiaoming", age: 20 })
let year = ref(0)
const numInterval = setInterval(() =>{
state.age++
year.value++
},1000)
watchEffect(() => {
console.log(state.age)
console.log(year.value)
})
return {
...toRefs(state)
}
},
})
demo 传送门:https://sfc.vuejs.org/ : 执行结果首先打印一次state和year值;然后每隔一秒,打印state和year值。
从上面的代码可以看出,
并没有像watch一样需要先传入依赖,watchEffect会自动收集依赖, 只要指定一个回调函数。在组件初始化时, 会先执行一次来收集依赖, 然后当收集到的依赖中数据发生变化时, 就会再次执行回调函数。
所以总结对比如下:
- watchEffect 不需要手动传入依赖
- watchEffect 会先执行一次用来自动收集依赖
- watchEffect 无法获取到变化前的值,只能获取变化后的值(无oldVal)
响应式丢失
有一些操作,会导致我们丢失对象的响应式,我们应当避免
- 从
setup()的根范围props中获取一个值将导致该值失去响应性。
export default {
props: {
rowData: Object,
},
setup: (props) => {
//bad
const formData = props.rowData
return { formData }
}
}
- setup中
解构props
import {toRefs} from "vue"
export default {
props:{
a: string,
b: string,
c: string,
},
setup: (props) => {
// bad 这里 a b c 都会失去响应性
const { a, b, c } = props
// good 需要将响应式对象转换为一组 ref
const { a, b, c } = toRefs(props)
}
}
- 响应式状态解构
import { reactive,toRefs } from 'vue'
const book = reactive({
title: 'Vue 3 Guide',
price: 'free'
})
// bad 使用解构的 price,title 的响应性都会丢失
let { price, title } = book
// good
let { price, title } = toRefs(book)
title.value = 'Vue 3 Detailed Guide'
console.log(book.title) // 'Vue 3 Detailed Guide'
- 使用
let定义后不能直接重新赋值reactive对象,会导致响应式的代理被覆盖。
export default {
name: 'App',
setup() {
let obj = reactive({a:1})
// 这样重新赋值后会,obj会变成普通对象,失去响应式;
setTimeout(() => {
obj = {a:2,b:3}
}, 1000)
return {
obj
}
}
}
// 应当,单独赋值
const obj = reactive({a:1})
setTimeout(() => {
obj.a = 2;
obj.b = 3;
}, 1000)
// 或,使用`Object.assign`
export default {
name: 'App',
setup() {
const obj = reactive({a:1})
setTimeout(() => {
Object.assign(obj,{a:2, b:3})
}, 1000)
return {
obj
}
}
}
组件间数据传递
Emits选项
vue3新增emits选项,使用 emits 记录每个组件所触发的所有事件,移除了 .native 修饰符。
任何未在 emits 中声明的事件监听器都会被算入组件的 $attrs 并将默认绑定到组件的根节点上。
对于向其父组件透传原生事件的组件来说,这会导致有两个事件被触发:
<template>
<button v-on:click="$emit('click', $event)">OK</button>
</template>
<script>
export default {
emits: [] // 不声明事件
}
</script>
当一个父级组件拥有 click 事件的监听器时:
// vue 2.x 我们通常使用 .native 来处理这种情况
<my-button v-on:click="handleClick"></my-button>
该事件现在会被触发两次:
- 一次来自
$emit()。 - 另一次来自应用在根元素上的原生事件监听器。
需要将组件发送的自定义事件定义在
emits选项中:
<template>
<button v-on:click="$emit('click', $event)">OK</button>
</template>
<script>
export default {
emits: ['click'] // 声明事件
}
</script>
但是,我们定义 props 跟 emit 对比,应该避免下面这样定义props: 官方风格指南
// 只有在原型开发时,这么做才能被接受
props: ['status']
子组件接收props
<script>
import { defineComponent } from "vue";
export default defineComponent({
props: {
name: String,
},
setup(props){
console.log(props);
}
})
</script>
/***或者***/
<script setup>
import { defineProps } from "vue";
let props=defineProps({
name:String
})
</script>
/***或者***/
<script setup>
//这个api在下面的几种方法中都适用,但不建议用
import { getCurrentInstance } from "vue";
let instance=getCurrentInstance();
console.log(instance.props);
</script>
子组件调用emit
<script>
import { defineComponent } from "vue";
export default defineComponent({
setup(props,ctx){
ctx.emit('myClick','这是传给父组件的值');
}
})
</script>
父组件调用子组件方法
//子组件
setup(props, { expose }){
const myChild = (val) => {
console.log(val)
}
expose({
myChild,
})
}
// 父组件
<test ref="childs"></test>
const childs = ref()
onMounted(() => {
childs.value.myChild(111)
})
Script setup里
<script setup>
import { defineProps, defineEmits, defineExpose } from "vue";
const props = defineProps({
foo: String
})
const emit = defineEmits(['update', 'delete'])
const b = ref(2)
defineExpose({ b })
</script>
Provide / Inject
默认情况下,provide/inject 绑定 并不是 响应式的。
我们可以通过传递一个 ref property 或 reactive 对象(也可以用Computed)给 provide 来改变这种行为。
<!-- src/components/MyMap.vue -->
<template>
<MyMarker />
</template>
<script>
import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue'
export default {
components: {
MyMarker
},
setup() {
// 添加 provide 值和 inject 值之间的响应性
const location = ref('North Pole')
const getlocation = reactive({
longitude: 90,
latitude: 135
})
provide('location', location)
provide('getlocation', getlocation)
}
}
</script>
<!-- 使用 inject -->
<script>
import { inject } from 'vue'
export default {
setup() {
//inject 第二个参数默认值,可选
const userLocation = inject('location', 'The Universe')
const userGetlocation = inject('getlocation')
return {
userLocation,
userGetlocation
}
}
}
</script>
Vuex && Router
Vuex 3.x 是支持 Vue 2 的 Vuex , Vuex 4.x 支持 Vue 3
Vuex 属于Vue全家桶一部分,非必需, Vue Core Team 团队也出了 Pinia 详见官网 可以代替Vuex。
就像 Rudex 也可以用更轻量的 mobx-react 代替。
- 在Vue2中,其实可以直接通过
this.$store进行获取,Vue3中,是这么使用的:
import { useStore } from 'vuex'
import { defineComponent, computed } from 'vue'
export default defineComponent({
name: 'Gift',
setup() {
const store = useStore()
const storeData = computed(() => store) // 配合computed,获取store的值。
return {
storeData,
}
},
})
- 在Vue2中,是通过
this.$router的方式,进行路由的函数式编程,Vue3中:
import { useRouter } from "vue-router"
import { defineComponent, computed } from 'vue'
export default defineComponent({
name: 'Gift',
setup() {
const router = useRouter()
const onClick = () => {
router.push({ name: "AddGift" })
}
return {
onClick
}
}
})
CSS变量注入
<template>
<span> Tom </span>
</template>
<script setup>
import { reactive } from 'vue'
const state = reactive({
color: 'red'
})
</script>
<style scoped>
span {
// 使用v-bind绑定state中的变量
color: v-bind('state.color');
}
</style>
Other
NextTick
import { createApp, nextTick } from 'vue'
const app = createApp({
setup() {
const message = ref('Hello!')
const changeMessage = async newMessage => {
message.value = newMessage
await nextTick()
console.log('Now DOM is updated')
}
}
})
Fragments
在 Vue3.x 中,你可以直接写多个根节点
<template>
<div></div>
<div></div>
</template>
Slot 具名插槽
在 Vue2.x 中具名插槽和作用域插槽分别使用slot和slot-scope来实现,
在 Vue3.x 中将slot和slot-scope进行了合并统一使用。 Vue3.0 中v-slot:
<!-- 父组件中使用 -->
<template v-slot:content="scoped">
<div v-for="item in scoped.data">{{item}}</div>
</template>
<!-- 也可以简写成: -->
<template #content="{data}">
<div v-for="item in data">{{item}}</div>
</template>
v-model的变化
v-model 详见官网 在vue3中发生了较大的变化:
-
非兼容:用于自定义组件时,
v-modelprop 和事件默认名称已更改:- prop:
value->modelValue; - 事件:
input->update:modelValue;
- prop:
-
非兼容:
v-bind的.sync修饰符和组件的model选项已移除,可在v-model上加一个参数代替; -
新增:现在可以在同一个组件上使用多个
v-model绑定; -
新增:现在可以自定义
v-model修饰符。
看一下vue2.x中v-model的使用:
<ChildComponent v-model = "title />
它实际上是下面这种写法的简写:
<ChildComponent :value = "title" @input = "title = $event" />
也就是说,它是传递一个属性value,然后接收一个input事件。
Vue3中v-model的基础使用
<ChildComponent v-model = "title">
它是下面这种写法的简写:
<ChildComponent :modelValue = "title" @update:modelValue = "title = $event">
也就是说vue3中,value改成了modelValue,input方法了改成update:modelValue。
在子组件中写法是:
export default defineComponent({
name:"ValidateInput",
props:{
modelValue:String, // v-model绑定的属性值
},
setup(props, { emit }){
const updateValue = (e: KeyboardEvent) => {
emit("update:modelValue",targetValue); // 传递的方法
}
}
}
v-model 参数
modelValue不太具备可读性,在子组件的props中看到这个都不知道是什么。 因此,我们希望能够更加见名知意。可以通过: xxx传递参数xxx,更改名称
若需要更改 model 的名称,现在我们可以为 v-model 传递一个参数
<ChildComponent v-model:title="pageTitle" />
//是以下的简写:
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
在子组件中,就可以使用
title 代替 modelValue
export default defineComponent({
name:"ValidateInput",
props:{
//modelValue:String,
title:String, // title替代了modelValue
},
setup(props, { emit }){
const updateValue = (e: KeyboardEvent) => {
//emit("update:modelValue",targetValue); // 传递的方法
emit("update:title",targetValue); // 传递的方法
}
}
}
允许我们在自定义组件上使用多个 v-model
<ChildComponent v-model:title="pageTitle" v-model:content="pageContent" />
v-model 修饰符
现在 3.x 支持自定义修饰符:
<ChildComponent v-model.capitalize="pageTitle" />
在 Custom Events 中查看自定义 v-model 修饰符的详细信息
Volar
Volar 是个 VS Code 的插件,除了支持如高亮、语法提示等之外,其最大的作用就是解决了 TS 提示问题。
注意,使用它时,要先移除 Vetur,以避免造成冲突。
编辑器快捷分割:点击小图标
vue文件,按照功能,被拆分成了三个视窗,并且每个视窗都负责自己的功能,其他的两个根元素都被合并了。
还有一些对class的友好交互:class追溯;style里面的class引用
Event Bus
是否需要 mitt,详见Github
import mitt from 'mitt'
const emitter = mitt()
// listen to an event
emitter.on('foo', e => console.log('foo', e) )
// listen to all events
emitter.on('*', (type, e) => console.log(type, e) )
// fire an event
emitter.emit('foo', { a: 'b' })
// clearing all events
emitter.all.clear()
// working with handler references:
function onFoo() {}
emitter.on('foo', onFoo) // listen
emitter.off('foo', onFoo) // unlisten
TS
json to ts: 可利用在api数据格式转换,根据json生成对应的 interface
高阶
Teleport
Teleport 是 Vue3.x 新推出的功能。粗暴的理解:Teleport 就像是哆啦 A 梦中的「任意门」,任意门的作用就是可以将人瞬间传送到另一个地方。
在子组件Header中使用到Dialog组件,我们实际开发中经常会在类似的情形下使用到 Dialog ,此时Dialog就被渲染到一层层子组件内部,处理嵌套组件的定位、z-index和样式都变得困难。 Dialog从用户感知的层面,应该是一个独立的组件,从 dom 结构应该完全剥离 Vue 顶层组件挂载的 DOM;同时还可以使用到 Vue 组件内的状态(data或者props)的值。
简单来说就是,即希望继续在组件内部使用Dialog, 又希望渲染的 DOM 结构不嵌套在组件的 DOM 中。 此时就需要 Teleport 上场,我们可以用<Teleport>包裹Dialog, 此时就建立了一个传送门,可以将Dialog渲染的内容传送到任何指定的地方。
demo定义一个Dialog组件Dialog.vue,留意 to 属性,与要达到的元素id选择器一致
Directive自定义指令
在 Vue 3 中对自定义指令的 API 进行了更加语义化的修改, 就如组件生命周期变更一样, 都是为了更好的语义化, 变更如下:
- 在 Vue3 中, 可以这样来自定义指令:
const { createApp } from "vue"
const app = createApp({})
app.directive('focus', {
mounted(el) {
el.focus()
}
})
然后可以在模板中任何元素上使用新的 v-focus指令, 如下:
<input v-focus />
- 或者:
有了fragments的支持,组件可能会有多个根节点。当被应用于多根组件时,自定义指令将被忽略,并将抛出警告。
文中部分参考以下链接内容片段,感谢🙏