watch监听器
watch可以是异步的,compute是不能有副作用的,刚学还不太懂区别,打个疑问三
基本配置
watch
的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组
注意,你不能直接侦听响应式对象的属性值,例如:
const obj = reactive({ count: 0 }) // 错误,因为 watch() 得到的参数是一个 number watch(obj.count, (count) => { console.log(`count is: ${count}`) })
这里需要用一个返回该属性的 getter 函数:
// 提供一个 getter 函数 watch( () => obj.count, (count) => { console.log(`count is: ${count}`) } )
watch
第二个参数是一个回调函数 cb(newVal, oldVal)
watch
第三个参数是一个options对象,属性如下
interface WatchOptions extends WatchEffectOptions { immediate?: boolean // 默认:false deep?: boolean // 默认:false flush?: 'pre' | 'post' | 'sync' // 默认:'pre' onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void }
immediate
:在侦听器创建时立即触发回调。第一次调用时旧值是undefined
。deep
:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器。flush
:调整回调函数的刷新时机。参考回调的刷新时机及watchEffect()
。onTrack / onTrigger
:调试侦听器的依赖。参考调试侦听器。
案例
侦听单个数据源
<template> <div> case1:<input v-model="message" type="text"> <hr> case2:<input type="text"> </div> <h1>{{tom}}</h1> </template> <script setup lang="ts"> import {ref} from "vue"; let message = ref<string>('小满') //监听器第一个参数:侦听的数据源(sources) 第二个参数 回调函数 cb(newVal,oldVal) watch(message,(newVal,oldVal)=>{ console.log('新的值----', newVal); console.log('旧的值----', oldVal); }) </script> <style scoped> </style>
侦听多个数据源
<template> <div> case1:<input v-model="message" type="text"> <hr> case2:<input v-model="message2" type="text"> </div> <h1>{{tom}}</h1> </template> <script setup lang="ts"> import {ref} from "vue"; let message = ref<string>('小满') let message2 = ref<string>('喜多川学姐') //使用数组的形式侦听多个数据源,返回的结果也会变成数组,结果顺序 按照 监听顺序 watch([message,message2],(newVal,oldVal)=>{//此时新值旧值也会变成一个数组 console.log('新的值----', newVal); console.log('旧的值----', oldVal); }) </script> <style scoped> </style>
监听 Reactive
使用 reactive 监听深层对象开启和不开启 deep 效果一样
深层次监听啦啦啦,需要给reactive开启第三个属性option(是一个配置项) => 里面有一个deep就是用来代表深度监听的
//监听语句变成如下 case1:<input v-model="message.nav.bar.name" type="text"> import { ref, watch ,reactive} from 'vue' let message = ref({//reactive已经隐性开启deep了,不需要再手动开启了 nav:{ bar:{ name:"学姐好好吃饭" } } }) watch([message,message2],(newVal,oldVal)=>{//此时新值旧值也会变成一个数组 console.log('新的值----', newVal); console.log('旧的值----', oldVal); },{deep:true})//深度监听
通过控制台打印,我们发现了其中的Proxy下的深层次监听
此时会发现有一个问题,那就是旧值跟新值的内容是一样的(原因是因为引用类型返回的新值是跟旧值一样的)
我们在使用的时候会发现一个问题,那就是我只改变了一个值,为什么没有改变的那个值(例如上面的喜多川学姐
)也跟着带出来了,我只想要得到改变的值,那要怎么办到呢? => 简洁版提问:想要侦听单一属性
- Vue推荐是让我们把要监听的变成一个函数(而不是直接.xxx.xxx得到的字符串,这样会报错的,因为不是Proxy所代理的对象)
- 创建一个回调函数去返回这个要侦听的属性
//区别就是()=>message.value.nav.bar.name跟单纯message.value.nav.bar.name,后者会报错 watch(()=>message.value.nav.bar.name,(newVal,oldVal)=>{//此时新值旧值也会变成一个数组 console.log('新的值----', newVal); console.log('旧的值----', oldVal); },{deep:true})
watchEffect高级侦听器
立即运行一个函数,同时会自动
响应式地追踪其依赖,并在依赖更改时重新执行。
如果用到message 就只会监听message 就是用到几个监听几个 而且是非惰性 会默认调用一次
let message = ref<string>('') let message2 = ref<string>('') watchEffect(() => { //console.log('message', message.value); console.log('message2', message2.value); })
清除副作用
就是在触发监听之前会调用一个函数可以处理你的逻辑
import { watchEffect, ref } from 'vue' let message = ref<string>('') let message2 = ref<string>('') watchEffect((oninvalidate) => { //console.log('message', message.value); oninvalidate(()=>{ }) console.log('message2', message2.value); })
停止监听
const unwatch = watchEffect(() => {}) // ...当该侦听器不再需要时 unwatch()
配置项
副作用刷新时机 flush 一般使用 post
pre | sync | post | |
更新时机 | 组件更新前执行 | 强制效果始终同步触发 | 组件更新后执行 |
onTrigger 可以帮助我们调试 watchEffect
import { watchEffect, ref } from 'vue' let message = ref<string>('') let message2 = ref<string>('') watchEffect((oninvalidate) => { //console.log('message', message.value); oninvalidate(()=>{ }) console.log('message2', message2.value); },{ flush:"post", onTrigger () { } })
生命周期
onBeforeMount() 在组件DOM实际渲染安装之前调用。在这一步中,根元素还不存在。
onMounted() 在组件的第一次渲染后调用,该元素现在可用,允许直接DOM访问
onBeforeUpdate() 数据更新时调用,发生在虚拟 DOM 打补丁之前。
onUpdated() DOM更新后,updated的方法即会调用。
onBeforeUnmount() 在卸载组件实例之前调用。在这个阶段,实例仍然是完全正常的。
onUnmounted() 卸载组件实例后调用。调用此钩子时,组件实例的所有指令都被解除绑定,所有事件侦听器都被移除,所有子组件实例被卸载。
父子组件传参
父组件传给子组件
- 父组件通过 v-bind 绑定个数据,然后子组件通过 defineProps 接受传过来的值(字符串数据类型不需要v-bind)
- 子组件接受值,通过 defineProps 来接受 defineProps 是无须引入的直接使用即可(defineProps有返回值,返回值就是接收到的props属性)
父组件
<template> <div> <AboutPage :title="message" /> </div> </template> <script setup lang="ts"> import AboutPage from './views/AboutView.vue' const message = '父传子' </script> <style scoped></style>
子组件
<template> <div> 我是about页面, {{ title }} </div> </template> <script setup lang="ts"> defineProps({ title: { type:String, default:'父传子的默认值', } }) </script> <style scoped></style>
在script中需要使用父组件传给子组件的title时,需要用变量来接受defineProps的返回值
<script setup lang="ts"> const props = defineProps({ title: { type:String, default:'父传子的默认值', } }) // 直接使用title不行 console.log(props.title) </script>
使用ts的话可以直接用泛型来定义,更加的简便
<script setup lang="ts"> defineProps<{ title:string }>() </script>
父传子使用ts设置默认值
withDefaults(defineProps<{ arr: number[] }>(),{ arr:()=>[666] })
子组件传递父组件
- 这时候需要用到
defineEmit
,这个内置函数里参数是一个数组,数组里面放的是我们自定义的名称(这个名称会在父组件中用到),这里我们使用一个常量接住 - 接受的常量里面有两个参数要填写,第一个是我们自定义的参数,也就是刚刚在defineEmit中写到的,这里则是要将其绑定起来
派发
到父组件中,第二个参数则是我们要派发出去的内容(也就是从子组件到父组件的内容) - 下一步我们就要到父组件中去接收了,我们这里要通过事件进行接收(也就是@),还记得我们刚刚说会在父组件名称中用到的自定义名称吗?没错,这里要用到了,@在子组件自定义名称="在父组件自定义名称"
- 父组件自定义名称要在父组件中script中使用,父组件中自定义名称的内容则可以通过子组件触发
看代码吧!!!
子组件
<template> <div> 我是about页面, {{ title }} </div> <button @click="handleClick"></button> </template> <script setup lang="ts"> defineProps<{ title:string }>() // 先注册我们自定义事件的名称,名叫success,待会父组件中会使用到 const emits = defineEmits(['success']) // 触发点击事件的时候,进行派发 const handleClick = () => { // 第一个参数是注册的事件,第二个参数是传递的值 emits('success','子组件给父组件传递数据') } </script>
父组件
子组件传递给父组件的值,可以通过函数的参数进行获取
<template> <div> <AboutPage :title="message" @success="getValue"/> </div> </template> <script setup lang="ts"> import AboutPage from './views/AboutView.vue' const message = '父传子' function getValue(value) { console.log(value); } </script>
使用ts设置
const emits = defineEmits<{ (e: 'success', name:string) :void // 可以设置多个 }>() const handleClick = () => { emits('success','子组件先父组件传递数据') }
defineExpose子组件暴露自己的属性和方法
父组件
<template> <div> <AboutPage ref="aboutRef" :title="message" @success="getValue"/> <button @click="show"></button> </div> </template> <script setup lang="ts"> import AboutPage from './views/AboutView.vue' import {ref} from "vue"; const aboutRef = ref<InstanceType<typeof AboutPage> >() function show() { console.log(aboutRef.value.name); } </script>
子组件
defineExpose({ name:"子组件暴露的属性" })
全局组件/局部组件/递归组件
配置全局组件
在main.ts 引入我们的组件跟随在createApp(App) 后面 切记不能放到mount 后面这是一个链式调用用
其次调用 component 第一个参数组件名称 第二个参数组件实例
import { createApp } from 'vue' import App from './App.vue' import './assets/css/reset/index.less' import Card from './components/Card/index.vue' createApp(App).component('Card',Card).mount('#app')
也可以批量注册全局组件
可以参考element ui 其实就是遍历一下然后通过 app.component 注册
配置局部组件
配置递归组件
type TreeList = { name: string; icon?: string; children?: TreeList[] | []; }; const data = reactive<TreeList[]>([ { name: "no.1", children: [ { name: "no.1-1", children: [ { name: "no.1-1-1", }, ], }, ], }, { name: "no.2", children: [ { name: "no.2-1", }, ], }, { name: "no.3", }, ]);
<div v-for="item in data"> <input type="checkbox"><span>{{item.name}}</span> <Tree v-if="item?.children?.length" :data="item.children"></Tree> </div>
改变递归组件的组件名,在增加一个script通过export添加name
<script lang="ts"> export default { name:"TreeItem" } </script>
动态组件
适用场景:tab标签页的切换
小tip: ??操作符只处理undefined和null
<template> <div style="display: flex"> <div @click="switchCom(item,index)" :class="[active == index ? 'active' : '']" class="tabs" v-for="(item,index) in data"> v-for里的index是索引 class是可以写两个的,一个动态一个静态,但不能两静态或者两动态 <div>{{item.name}}</div> </div> </div> <component :is="comId"></component> 默认展示这个A组件了,component是Vue的内置组件 </template> <script setup lang='ts'> import { ref, reactive } from 'vue' import WaterFall from "./components/water-fall.vue"; import A from "./components/A.vue" import B from "./components/B.vue" import C from "./components/C.vue" const comId = ref(A) const active = ref(0) const data = reactive([ { name:"A组件", com:A }, { name:"B组件", com:B }, { name:"C组件", com:C } ]) const switchCom = (item:any,index:any)=>{ comId.value = item.com,//要切换的组件 active.value = index//切换的索引 } </script> <style lang='less'> .active{ background: skyblue; } .tabs{ border: 1px solid #ccc; padding: 5px 10px; margin: 5px; cursor: pointer; } </style>
slot插槽
teleport
<Teleport>
是一个内置组件,它可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去。
通过to 属性 插入指定元素位置 to="body" 便可以将Teleport
内容传送到指定位置
<Teleport to="body"> <Loading></Loading> </Teleport>
也可以通过disabled来控制to属性是否生效
使用disabled 设置为 true则 to属性不生效 false 则生效
<teleport :disabled="true" to='body'> <A></A> </teleport>
keep-alive缓存组件
有时候我们不希望组件被重新渲染影响使用体验;或者处于性能考虑,避免多次重复渲染降低性能。而是希望组件可以缓存下来,维持当前的状态。这时候就需要用到 keep-alive
组件。
keep-alive就是保持活跃的意思,你在里面填入的东西在你切换成其他组件的时候不会被初始化成最初的样子
开启 keep-alive 生命周期的变化
- 初次进入时: onMounted> onActivated
- 退出后触发
deactivated
- 再次进入:
- 只会触发 onActivated
- 事件挂载的方法等,只执行一次的放在 onMounted 中;组件每次进去执行的方法放在 onActivated 中
<!-- 基本 --> <keep-alive> <component :is="view"></component> </keep-alive> <!-- 多个条件判断的子组件 --> <keep-alive> <comp-a v-if="a > 1"></comp-a> <comp-b v-else></comp-b> //v-if与else的切换组件,切换后另外一个组件虽然被切换走了,但是通过声明周期我们可以看到不会被销毁 </keep-alive> <!-- 和 `<transition>` 一起使用 --> <transition> <keep-alive> <component :is="view"></component> </keep-alive> </transition>
include
和 exclude
- include 和 exclude 允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示:
- 写在
include
里面的组件会被缓存起来,只写keep-alive是默认里面信息全部缓存的 - 写在
exclude
里面的组件不会被缓存起来,跟include的属性是完全相反的。如何选择使用可以根据实际情况选择 - :
max
属性是决定了我们缓存的最大组件数量,假设我们:max="10",也就是最多缓存10个组件,可我们keep-alive内部即将缓存的有11个组件,他有内置算法会优先替换掉我们不常用的那一个
<keep-alive :include="keep-alive里的组件" :exclude="" :max=""></keep-alive> <keep-alive :max="10"> <component :is="view"></component> </keep-alive>
这个是只有开启keep-alive的时候才会出现的两个生命周期
//新的两个 onActivated(()=>{ console.log('keep-alive的初始化') }) onDeactivated(()=>{ console.log('keep-alive的卸载') }) //对应有关联的两个生命周期 onMounted(()=>{ console.log('初始化')//这个会随着onActivated一起生效 }) onUnMounted(()=>{ console.log('卸载')//如果有onDeactivated,则优先生效onDeactivated,onUnMounted则不再生效 }) //所以有一些卸载操作我们可以写在keep-alive独有的生命卸载周期里面 //一些一次性操作则写到onMounted里面,比如一些接口请求一次就行了。 //onMounted只会在刚开始的时候挂载一次,你组件之前的切换就不会在重新初始化了 //但是这个keep-alive的onActivated初始化则会在组件切换的时候不断触发
依赖注入Provide / Inject
provide()
接受两个参数:第一个参数是要注入的 key,可以是一个字符串或者一个 symbol,第二个参数是要注入的值。
inject()
注入一个由祖先组件或整个应用 (通过 app.provide()
) 提供的值。
第一个参数是注入的 key。Vue 会遍历父组件链,通过匹配 key 来确定所提供的值。如果父组件链上多个组件对同一个 key 提供了值,那么离得更近的组件将会“覆盖”链上更远的组件所提供的值。如果没有能通过 key 匹配到值,inject()
将返回 undefined
,除非提供了一个默认值。
第二个参数是可选的,即在没有匹配到 key 时使用的默认值。它也可以是一个工厂函数,用来返回某些创建起来比较复杂的值。如果默认值本身就是一个函数,那么你必须将 false
作为第三个参数传入,表明这个函数就是默认值,而不是一个工厂函数。
与注册生命周期钩子的 API 类似,inject()
必须在组件的 setup()
阶段同步调用。
tsx风格
完整版用法 请看 @vue/babel-plugin-jsx - npm
下一步计划
- 视频中的源码部分和案例
- vue的高级使用
- 状态管理库的学习
- 继续迭代和完善这个入门文档