一、computed
<script setup>
import { computed, ref } from 'vue'
const count = ref(1)
// 通过computed获得doubleCount
const doubleCount = computed(() => {
return count.value * 2
})
// 获取
console.log(doubleCount.value)
</script>
二、watch与watchEffect区别
watch
默认情况是懒执行(惰性),也就是只有当数据发生变化时才执行第二个参数函数。
watch(data,()=>{},{})
- 参数一,监听的数据
- 参数二,数据改变时触发的回调函数(newVal,oldVal)
- 参数三: immediate:在侦听器创建时立即触发回调。第一次调用时,旧值将为 undefined。 deep:如果源是对象或数组,则强制深度遍历源,以便在深度变更时触发回调。详见深层侦听器。 flush:调整回调的刷新时机。详见回调的触发时机及 watchEffect()。 onTrack / onTrigger:调试侦听器的依赖关系。详见侦听器调试。
1、监听ref定义的一个响应式数据
<script setup lang="ts">
import { ref, watch } from "vue";
const str = ref('张三')
//3s后改变str的值
setTimeout(() => { str.value = '王五' }, 3000)
watch(str, (newV, oldV) => {
console.log(newV, oldV) //王五 张三
})
</script>
2、监听多个ref 这时候写法变为数组的形式
<script setup lang="ts">
import { ref, watch } from "vue";
let name = ref('张三')
let age = ref(18)
//3s后改变值
setTimeout(() => {
name.value = '王五'
age.value = 19
}, 3000)
watch([name, age], (newV, oldV) => {
console.log(newV, oldV) // ['王五', 19] ['张三', 18]
})
</script>
3、监听Reactive定义的响应式对象
<script setup lang="ts">
import { reactive, watch } from "vue";
let info = reactive({
name: '张三',
age: 18
})
//3s后改变值
setTimeout(() => {
info.age = 19
}, 3000)
watch(info, (newV, oldV) => {
console.log(newV, oldV)
})
</script>
当 watch 监听的是一个响应式对象时,会隐式地创建一个深层侦听器,即该响应式对象里面的任何属性发生变化,都会触发监听函数中的回调函数。即当 watch 监听的是一个响应式对象时,默认开启 deep:true
4、监听reactive 定义响应式对象的单一属性 我们可以使用 getter 函数的形式,即将watch第一个参数修改成一个回调函数的形式
// 其他不变
watch(()=>info.age, (newV, oldV) => {
console.log(newV, oldV) // 19 18
})
5、监听reactive定义的 引用数据
<script setup lang="ts">
import { reactive, watch } from "vue";
let info = reactive({
name: '张三',
age: 18,
obj: {
str: '吃面'
}
})
//3s后改变s值
setTimeout(() => {
info.obj.str = '吃火锅'
}, 3000)
// 需要自己开启 deep:true深度监听,不然不发触发 watch 的回调函数
watch(() => info.obj, (newV, oldV) => {
console.log(newV, oldV)
}, {
deep: true
})
</script>
有两个小坑: 1.监视reactive定义的响应式数据的时候:oldValue无法获取到正确的值,强制开启了深度监视(deep配置无效) 2.监视reactive定义的响应式数据中某个属性的时候:deep配置有效
<script setup lang="ts">
import { reactive, watch } from "vue";
const tem = reactive({
name: "张三",
});
setTimeout(() => {
tem.name = "李四";
}, 1000);
// 监视reactive定义的所有响应式数据,
// 1.此处无法获取正确的oldValue(newValue与oldValue是一致值),且目前无法解决
// 2.强制开启了深度监视(deep配置无效)
watch(
() => tem,
(newVal, oldVal) => {
console.log(newVal);
console.log(oldVal);
},
{ deep: true }
);
</script>
watchEffect
会立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。(有点像计算属性) 如果用到 a 就只会监听 a, 就是用到几个监听几个 而且是非惰性,会默认调用一次
<script setup lang="ts">
import { ref, watchEffect } from "vue";
let num = ref(0)
//3s后改变值
setTimeout(() => {
num.value++
}, 3000)
watchEffect(() => {
console.log('num 值改变:', num.value)
})
</script>
可以在控制台上看到,第一次进入页面时,打印出num 值改变:0,三秒后,再次打印num 值改变:1
停止监听 当 watchEffect 在组件的 setup() 函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。 但是我们采用异步的方式创建了一个监听器,这个时候监听器没有与当前组件绑定,所以即使组件销毁了,监听器依然存在。 这个时候我们可以显式调用停止监听
<script setup lang="ts">
import { watchEffect } from 'vue'
// 它会自动停止
watchEffect(() => {})
// ...这个则不会!
setTimeout(() => {
watchEffect(() => {})
}, 100)
const stop = watchEffect(() => {
/* ... */
})
// 显式调用
stop()
</script>
清除副作用(onInvalidate) watchEffect 的第一个参数——effect函数——可以接收一个参数:叫onInvalidate,也是一个函数,用于清除 effect 产生的副作用 就是在触发监听之前会调用一个函数可以处理你的逻辑,例如防抖
import { ref, watchEffect } from "vue";
let num = ref(0)
//3s后改变值
setTimeout(() => {
num.value++
}, 3000)
watchEffect((onInvalidate) => {
console.log(num.value)
onInvalidate(() => {
console.log('执行');
});
})
控制台依次输出:0 => 执行 => 1 例子:
watchEffect(async (onCleanup) => {
const { response, cancel } = doAsyncWork(id.value)
// `cancel` 会在 `id` 更改时调用
// 以便取消之前
// 未完成的请求
onCleanup(cancel)
data.value = await response
})
配置选项 watchEffect的第二个参数,用来定义副作用刷新时机,可以作为一个调试器来使用 flush (更新时机): 1、pre:组件更新前执行 2、sync:强制效果始终同步触发 3、post:组件更新后执行
<script setup lang="ts">
import { ref, watchEffect } from "vue";
let num = ref(0)
//3s后改变值
setTimeout(() => {
num.value++
}, 3000)
watchEffect((onInvalidate) => {
console.log(num.value)
onInvalidate(() => {
console.log('执行');
});
}, {
flush: "post", //此时这个函数会在组件更新之后去执行
onTrigger(e) { //作为一个调试工具,可在开发中方便调试
console.log('触发', e);
},
})
</script>
三、props父传子
子组件
<template>
<span>{{props.name}}</span>
// 可省略【props.】
<span>{{name}}</span>
</template>
<script setup>
// import { defineProps } from 'vue'
// defineProps在<script setup>中自动可用,无需导入
// 需在.eslintrc.js文件中【globals】下配置【defineProps: true】
// 声明props
const props = defineProps({
name: {
type: String,
default: ''
}
})
</script>
父组件
<template>
<child name='Jerry'/>
</template>
<script setup>
// 引入子组件
import child from './child.vue'
</script>
四、emit子传父
子组件
<template>
<span>{{props.name}}</span>
// 可省略【props.】
<span>{{name}}</span>
<button @click='changeName'>更名</button>
</template>
<script setup>
// import { defineEmits, defineProps } from 'vue'
// defineEmits和defineProps在<script setup>中自动可用,无需导入
// 需在.eslintrc.js文件中【globals】下配置【defineEmits: true】、【defineProps: true】
// 声明props
const props = defineProps({
name: {
type: String,
default: ''
}
})
// 声明事件
const emit = defineEmits(['updateName'])
const changeName = () => {
// 执行
emit('updateName', 'Tom')
}
</script>
父组件
<template>
<child :name='state.name' @updateName='updateName'/>
</template>
<script setup>
import { reactive } from 'vue'
// 引入子组件
import child from './child.vue'
const state = reactive({
name: 'Jerry'
})
// 接收子组件触发的方法
const updateName = (name) => {
state.name = name
}
</script>
五、v-model
Vue2 中组件的双向绑定采用的是 v-model 或 .snyc 修饰符,两种写法多少显得有点重复,于是在 Vue3 中合成了一种。Vue3 统一使用 v-model 进行处理,并且可以和多个数据进行绑定,如 v-model:foo、v-model:bar。 v-model 等价于 :model-value="someValue" 和 @update:model-value="someValue = event"
支持绑定多个v-model,v-model 是 v-model:modelValue 的简写 绑定其他字段,如:v-model:name
子组件
<template>
<span @click="changeInfo">我叫{{ modelValue }},今年{{ age }}岁</span>
</template>
<script setup>
defineProps({
modelValue: String,
age: Number
})
const emit = defineEmits(['update:modelValue', 'update:age'])
const changeInfo = () => {
// 触发父组件值更新
emit('update:modelValue', 'Tom')
emit('update:age', 30)
}
</script>
父组件
<template>
// v-model:modelValue简写为v-model
// 可绑定多个v-model
<child
v-model="state.name"
v-model:age="state.age"
/>
</template>
<script setup>
import { reactive } from 'vue'
// 引入子组件
import child from './child.vue'
const state = reactive({
name: 'Jerry',
age: 20
})
</script>
六、ref子组件实例和defineExpose
- 在标准组件写法里,子组件的数据都是默认隐式暴露给父组件的,但在 script-setup 模式下,所有数据只是默认 return 给 template 使用,不会暴露到组件外,所以父组件是无法直接通过挂载 ref 变量获取子组件的数据。
- 如果要调用子组件的数据,需要先在子组件显示的暴露出来,才能够正确的拿到,这个操作,就是由 defineExpose 来完成。
子组件
<template>
<span>{{state.name}}</span>
</template>
<script setup>
import { reactive, toRefs } from 'vue'
// defineExpose无需引入
// import { defineExpose, reactive, toRefs } from 'vue'
// 声明state
const state = reactive({
name: 'Jerry'
})
// 将方法、变量暴露给父组件使用,父组件才可通过ref API拿到子组件暴露的数据
defineExpose({
// 解构state
...toRefs(state),
// 声明方法
changeName () {
state.name = 'Tom'
}
})
</script>
父组件
- 获取一个子组件实例
<template>
<child ref='childRef'/>
</template>
<script setup>
import { ref, nextTick } from 'vue'
// 引入子组件
import child from './child.vue'
// 子组件ref(TypeScript语法)
const childRef = ref<InstanceType<typeof child>>()
// nextTick
nextTick(() => {
// 获取子组件name
console.log(childRef.value.name)
// 执行子组件方法
childRef.value.changeName()
})
</script>
- 获取多个子组件实例:在 v-for 中获取子组件实例
- 这种情况仅适用于 v-for 循环数是固定的情况 ,因为如果 v-for 循环数 在初始化之后发生改变,那么就会导致 childRefs 再一次重复添加,childRefs 中会出现重复的子组件实例
<template>
<div v-for="item in 3" :key="item">
<child :ref='addChildRef'/>
</div>
</template>
<script setup>
// 省略...
// 子组件实例数组
const childRefs = ref([])
// 通过 addChildRef 方法向 childRefs 添加子组件实例
const addChildRef = (el) => {
childRefs.value.push(el)
}
</script>
- 获取多个子组件实例:动态 v-for 获取子组件实例
- 通过下标来向 childRefs 添加/修改,初始化之后,动态修改 v-for 循环数,会自动根据下标重新修改该下标对应的数据
<template>
<button @click='childNums++'></button>
<div v-for="(item, i) in childNums" :key="item">
// 通过下标向 childRefs 动态添加子组件实例
<child :ref='(el) => childRefs[i] = el'/>
</div>
<button @click='childNums--'></button>
</template>
<script setup>
// 省略...
// 子组件数量
const childNums = ref(1)
// 子组件实例数组
const childRefs = ref([])
</script>
七、插槽slot
v-slot 有对应的简写方式 #,因此
<template v-slot:header>
可以简写为
<template #header>
例如:
<hello-world>
<template v-slot:header>
<button>按钮01</button>
</template>
</hello-world>
等价于
<hello-world>
<template #header>
<button>按钮01</button>
</template>
</hello-world>
使用方式
子组件
<template>
<!-- 匿名插槽 -->
<slot />
<!-- 具名插槽 -->
<slot name="title" />
<!-- 作用域插槽 -->
<slot name="footer" :scope="state" />
</template>
<script setup>
import { useSlots, reactive } from "vue";
const state = reactive({
name: "张三",
age: "25岁",
});
const slots = useSlots();
// 匿名插槽使用情况
const defaultSlot = reactive(slots.default && slots.default().length);
console.log(defaultSlot); // 1
// 具名插槽使用情况
const titleSlot = reactive(slots.title && slots.title().length);
console.log(titleSlot); // 3
</script>
父组件
<template>
<child>
<!-- 匿名插槽 -->
<span>我是默认插槽</span>
<!-- 具名插槽 -->
<template #title>
<h1>我是具名插槽</h1>
<h1>我是具名插槽</h1>
<h1>我是具名插槽</h1>
</template>
<!-- 作用域插槽 -->
<template #footer="{ scope }">
<footer>作用域插槽——姓名:{{ scope.name }},年龄{{ scope.age }}</footer>
</template>
</child>
</template>
<script setup>
// 引入子组件
import child from "./child.vue";
</script>
八、路由useRoute和useRouter
<script setup>
import { useRoute, useRouter } from 'vue-router'
// 必须先声明调用
const route = useRoute()
const router = useRouter()
// 路由信息
console.log(route.query)
// 路由跳转
router.push('/newPage')
</script>
九、路由导航守卫
<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
// 添加一个导航守卫,在当前组件将要离开时触发。
onBeforeRouteLeave((to, from, next) => {
next()
})
// 添加一个导航守卫,在当前组件更新时触发。
// 在当前路由改变,但是该组件被复用时调用。
onBeforeRouteUpdate((to, from, next) => {
next()
})
</script>
十、store
Vuex Vue3 中的Vuex不再提供辅助函数写法
<script setup>
import { useStore } from 'vuex'
import { key } from '../store/index'
// 必须先声明调用
const store = useStore(key)
// 获取Vuex的state
store.state.xxx
// 触发actions的方法
store.commit('fnName')
// 触发actions的方法
store.dispatch('fnName')
// 获取Getters
store.getters.xxx
</script>
Pinia
- 同时支持 Composition Api 和 Options api 的语法;
- 去掉 mutations ,只有 state 、getters 和 actions ;
- 不支持嵌套的模块,通过组合 store 来代替;
- 更完善的 Typescript 支持;
- 清晰、显式的代码拆分;
安装
# 使用 npm
npm install pinia
# 使用 yarn
yarn add pinia
main.js 引入
import App from './App.vue'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')
配置 store.js
import { defineStore } from 'pinia'
// defineStore 调用后返回一个函数,调用该函数获得 Store 实体
export const useStore = defineStore({
// id: 必须,在所有 Store 中唯一
id: 'globalState',
// state: 返回对象的函数
state: () => ({
count: 1,
data: {
name: 'Jerry',
sex: '男'
}
}),
// getter 第一个参数是 state,是当前的状态,也可以使用 this 获取状态
// getter 中也可以访问其他的 getter,或者是其他的 Store
getters: {
// 通过 state 获取状态
doubleCount: (state) => state.count * 2,
// 通过 this 获取状态(注意this指向)
tripleCount() {
return this.count * 3
}
},
actions: {
updateData (newData, count) {
// 使用 this 直接修改
this.data = { ...newData }
this.count = count
// 使用 $patch 修改多个值
this.$patch({
data: { ...newData },
count
})
}
}
})
使用 store
<template>
// 获取 store 的 state
<p>姓名:{{store.data.name}}</p>
<p>性别:{{store.data.sex}}</p>
// 调用 actions 方法 / 修改 store
<button @click='update'>修改用户信息</button>
// 获取 getter
<p>获取getter:{{store.doubleCount}}</p>
</template>
<script setup>
import { useStore } from '@store/store.js'
const store = useStore()
function update () {
// 通过 actions 定义的方法修改 state
store.updateData({ name: 'Tom', sex: '女' })
// 通过 store 直接修改
store.data = { name: 'Tom', sex: '女' }
// 同时改变多个状态
store.$patch((state) => {
state.data = { name: 'Tom', sex: '女' }
state.count = 2
})
}
</script>
<style lang="scss" scoped>
</style>
其他方法 替换整个 state $state 可以让你通过将 store 的属性设置为新对象来替换 store 的整个 state
const store = useStore()
store.$state = {
name: 'Bob',
sex: '男'
}
重置状态 调用 store 上的 $reset() 方法将状态重置为初始值
const store = useStore()
store.$reset()
十一、生命周期
| Option API | setup中 |
|---|---|
| beforeCreate | 不需要(直接写到setup函数中) |
| created | 不需要(直接写到setup函数中) |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeUnmount | onBeforeUnmount |
| unmounted | onUnmounted |
| errorCaptured | onErrorCaptured |
| renderTracked | onRenderTracked |
| renderTriggered | onRenderTriggered |
| activated | onActivated |
| deactivated | onDeactivated |
十二、原型绑定与组件内使用
main.js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// 获取原型
const prototype = app.config.globalProperties
// 绑定参数
prototype.name = 'Jerry'
组件内使用
<script setup>
import { getCurrentInstance } from 'vue'
// 获取原型
const { proxy } = getCurrentInstance()
// 输出
console.log(proxy.name)
</script>
十三、v-bind() CSS变量注入
<template>
<span>Jerry</span>
</template>
<script setup>
import { ref, reactive } from 'vue'
// prop接收样式
const props = defineProps({
border: {
type: String,
default: '1px solid yellow'
}
})
// 常量声明样式
const background = 'red'
// 响应式数据声明样式
const color = ref('blue')
const style = reactive({
opacity: '0.8'
})
</script>
<style lang="scss" scoped>
span {
// 使用常量声明的样式
background: v-bind(background);
// 使用响应式数据声明的样式
color: v-bind(color);
opacity: v-bind('style.opacity');
// 使用prop接收的样式
border: v-bind('props.border');
}
</style>
十四、provide和inject
父组件
<template>
<child/>
</template>
<script setup>
import { ref, watch, provide } from 'vue'
// 引入子组件
import child from './child.vue'
let name = ref('Jerry')
// 声明provide
provide('provideState', {
name,
changeName: () => {
name.value = 'Tom'
}
})
// 监听name改变
watch(name, () => {
console.log(`name变成了${name}`)
setTimeout(() => {
console.log(name.value) // Tom
}, 1000)
})
</script>
子组件
<script setup>
import { inject } from 'vue'
// 注入,第二个参数为默认值
const provideState = inject('provideState', {})
// 子组件触发name改变
provideState.changeName()
</script>
十五、自定义指令
Vue3相较于Vue2的自定义声明方法有些不同
const app = createApp({})
// 使 v-demo 在所有组件中都可用
app.directive('demo', {
// 在绑定元素的 attribute 前或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode, prevVnode) {}
})
比如实现一个默认密文身份证号,点击才展示的指令
app.directive('ciphertext', {
created: (el: any) => {
console.log(el, 1111)
el.style.cursor = 'pointer'
const value = el.innerText
if (!value || value === 'null' || value === '--') {
el.innerText = '--'
} else {
el.setAttribute('title', '点击查看')
el.innerText = hideText(value)
el.addEventListener('click', () => {
if (el.innerText.indexOf('*') > -1) {
el.innerText = value
} else {
el.innerText = hideText(value)
}
})
}
}
})
<span v-ciphertext>{{idNumber}}</span>
全局注册的自定义指令将正常工作。本地的自定义指令在
<script setup>
const vMyDirective = {
beforeMount: (el) => {
// 在元素上做些操作
}
}
</script>
<template>
<h1 v-my-directive>This is a Heading</h1>
</template>
如果指令是从别处导入的,可以通过重命名来使其符合命名规范:
<script setup>
import { myDirective as vMyDirective } from './MyDirective.js'
</script>
十六、对 await 的支持
不必再配合 async 就可以直接使用 await 了,这种情况下,组件的 setup 会自动变成 async setup
<script setup>
const post = await fetch('/api').then(() => {})
</script>
十七、定义组件的name
<script>
export default {
name: 'ComponentName',
}
</script>
更优雅的方式,安装插件:vite-plugin-vue-setup-extend,就可以按以下方式定义name了 配置 vite.config.ts
import { defineConfig } from 'vite'
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
export default defineConfig({
plugins: [VueSetupExtend()]
})
使用
<script setup name="ComponentName">
// todo
</script>
十八、动态组件
/**
*由于组件是通过变量引用而不是基于字符串组件名注册的,
*在 <script setup> 中要使用动态组件的时候,应该使用*动态的 :is 来绑定:
*/
<template>
<component :is="Foo" />
<component :is="someCondition ? Foo : Bar" />
</template>
<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>