前言
1.Options API 存在的问题
使用传统OptionsAPI中,新增或者修改一个需求,就需要分别在data,methods,computed里修改 。
|---------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------|
| | |
2.Composition API 的优势
我们可以更加优雅的组织我们的代码,函数。让相关功能的代码更加有序的组织在一起。
|---------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------|
| | |
一、Vue3带来了什么
1.性能的提升
-
打包大小减少41%
-
初次渲染快55%, 更新渲染快133%
-
内存减少54%
2.源码的升级
-
使用Proxy代替defineProperty实现响应式
-
重写虚拟DOM的实现和Tree-Shaking
3.拥抱TypeScript
- Vue3可以更好的支持TypeScript
4.新的特性
- Composition API(组合API)
-
setup 配置
-
ref与reactive
-
watch与watchEffect
-
provide与inject
-
......
- 新的内置组件
-
Fragment
-
Teleport
-
Suspense
- 其他改变
-
新的生命周期钩子
-
data 选项应始终被声明为一个函数
-
移除keyCode支持作为 v-on 的修饰符
-
......
二、创建Vue3.0工程
vue.js 三种方式安装(vue-cli)_vue安装-CSDN博客
使用 vite 创建
vite官网:https://vitejs.cn
-
什么是vite?------ 新一代前端构建工具。
-
优势如下:
-
开发环境中,无需打包操作,可快速的冷启动。
-
轻量快速的热重载(HMR)。
-
真正的按需编译,不再等待整个应用编译完成。
创建工程
npm init vite-app <project-name>
进入工程目录
cd <project-name>
安装依赖
npm install
运行
npm run dev
三、常用 Composition API
1.拉开序幕的 setup
-
理解:Vue3.0中一个新的配置项,为一个函数。
-
setup是所有Composition API(组合API)" 表演的舞台 "。
-
组件中所用到的:数据、方法等等,均要配置在setup中。
-
setup函数的两种返回值:
- 若返回一个对象,则对象中的属性、方法, 在模板中均可以直接使用。(重点关注!)
- 若返回一个渲染函数:则可以自定义渲染内容。(了解)
-
注意点:
- 尽量不要与Vue2.x配置混用
Vue2.x配置(data、methos、computed...)中可以访问到setup中的属性、方法。
但在setup中不能访问到Vue2.x配置(data、methos、computed...)。
如果有重名, setup优先。
- setup不能是一个async函数,因为返回值不再是return的对象, 而是promise, 模板看不到return对象中的属性。(后期也可以返回一个Promise实例,但需要Suspense和异步组件的配合)
setup的两种写法:
方式一:vue2,setup(){ return {....}}
javascript
<template>
<div>
<h1>ref() 和 reactive() 函数</h1>
<div>
姓名:{{ name }} <br />
年龄:{{ age }} <br />
工作信息:{{ job.type }}---{{ job.salary }} <br />
住址:{{ obj.address }} <br />
爱好:{{ hobby }} <br />
<button @click="changeInfo">修改个人信息</button>
</div>
</div>
</template>
<script>
import { ref, reactive } from "vue";
export default {
name: "ComponentName",
setup() {
// 数据
let name = ref("muzidigbig");
let age = ref(18);
let job = ref({
type: "web前端",
salary: "20k",
});
let obj = reactive({
address: "武汉江夏",
a: {
b: {
c: 666,
},
},
});
let hobby = reactive(["抽烟", "喝酒", "烫头"]);
function changeInfo() {
name.value = "Lee";
age.value = 20;
job.value.salary = "30k";
job.value = {
type: "UI工程师",
salary: "40k",
};
obj.address = "武汉洪山";
obj.a.b.c = 999;
hobby[0] = "学习";
console.log(name, age);
}
return {
name,
age,
job,
obj,
hobby,
changeInfo,
};
},
};
</script>
<style scoped>
</style>
方式二:语法糖
javascript
<script setup>
import { ref, getCurrentInstance } from "vue";
</script>
<template>
<router-view></router-view>
</template>
<style scoped>
</style>
2.ref函数
作用: 定义一个响应式的数据对象
语法: const xxx = ref(initValue)
创建一个包含响应式数据的引用对象(reference对象,简称ref对象)
JS中操作数据: xxx.value
模板中读取数据: 不需要.value,直接: <div>{{xxx}}</div>
.value 是数据,无.value 是ref对象
备注:
-
接收的数据可以是:基本类型、也可以是对象类型。
-
基本类型的数据:响应式依然是靠``Object.defineProperty()``的```get```与```set```完成的。
-
对象类型的数据:内部" 求助 "了Vue3.0中的一个新函数------ ```reactive```函数。
总结:
-
普通ref对象;不会和原始对象挂钩;重新渲染
-
ref 定义一个引用类型的对象,重新分配一个新对象 不会失去响应式
3.reactive函数
作用: 定义一个 引用/对象类型 的响应式数据(基本类型不要用它,要用```ref```函数)
语法:const 代理对象= reactive(源对象);
接收一个对象(或数组),返回一个代理对象(Proxy的实例对象,简称proxy对象)
reactive定义的响应式数据是"深层次的"。
内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据进行操作。
reactive 重新分配一个新对象会失去响应式,可以使用 Object.assign() 去整体替换
javascript
import {ref, reactive} from 'vue'
/*
interface CarInfer{
name:string,
price:number
}
// let car = reactive<CarInfer>({ name: '奥迪', price: 30 }); // ts约束,传泛型
*/
let car = reactive({ name: '奥迪', price: 30 })
let car2 = ref({ name: '奥迪', price: 30 })
const changeCar = () => {
// car = { name: '宝马', price: 30 }; // 页面不会更新
// car = reactive({ name: '宝马', price: 30 }); // 页面不会更新
// 页面会更新
Object.assign(car,{ name: '宝马', price: 30 })
car2.value = {name: '奔驰', price: 40}
}
4.Vue3.0中的响应式原理
vue2.x的响应式
-
实现原理:
-
对象类型:通过```Object.defineProperty()```对属性的读取、修改进行拦截(数据劫持)。
-
数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)。
javascript
Object.defineProperty(data, 'count', {
get () {},
set () {}
})
-
存在问题:
-
新增属性、删除属性, 界面不会更新。
-
直接通过下标修改数组, 界面不会自动更新。
Vue3.0的响应式
-
实现原理:
-
通过Proxy(代理): 拦截对象中任意属性的变化, 包括:属性值的读写、属性的添加、属性的删除等。
-
通过Reflect(反射): 对源对象的属性进行操作。
javascript
new Proxy(data, {
// 拦截读取属性值
get (target, prop) {
return Reflect.get(target, prop)
},
// 拦截设置属性值或添加新属性
set (target, prop, value) {
// return Reflect.set(target, prop, value)
Reflect.set(target, prop, value)
},
// 拦截删除属性
deleteProperty (target, prop) {
return Reflect.deleteProperty(target, prop)
}
})
proxy.name = 'tom'
5.reactive对比ref
从定义数据角度对比:
-
ref用来定义:基本类型数据
-
reactive用来定义:引用类型数据
-
备注:ref也可以用来定义对象(或数组)类型数据,它内部会自动通过 reactive 转为代理对象。
从原理角度对比:
-
ref通过``Object.defineProperty()``的```get```与```set```来实现响应式(数据劫持)。
-
reactive通过使用 Proxy 来实现响应式(数据劫持), 并通过 Reflect 操作源对象内部的数据。
从使用角度对比:
-
ref定义的数据:操作数据需要```.value```,读取数据时模板中直接读取不需要```.value```。
-
reactive定义的数据:操作数据与读取数据:均不需要```.value```。reactive 重新分配一个新对象会失去响应式,可以使用 Object.assign() 去整体替换
6.setup的两个注意点
-
setup执行的时机
-
在 beforeCreate 之前执行一次,this 是 undefined。
-
setup的参数
-
props:值为对象(响应式数据,直接使用解构将失去响应式,可通过 toRefs(props) 解构保留响应式),包含:组件外部传递过来,且组件内部声明接收了的属性。
-
context:上下文对象(非响应式数据,可以使用解构)
-
attrs: 值为对象,包含:组件外部传递过来,但没有在props配置中声明的属性, 父组件又传过来的属性,相当于 ```this.$attrs```。
"透传 attribute" 指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on 事件监听器。最常见的例子就是 class、style 和 id。
如果你不想要一个组件自动地继承 attribute,你可以在组件选项中设置 inheritAttrs: false。
-
slots: 收到的插槽内容, 相当于 ```this.$slots```。
-
emit: 分发自定义事件的函数, 相当于 ```this.$emit```。
注意:
- props中数据流是单项的,即子组件不可改变父组件传来的值
- 在组合式API中,如果想在子组件中用其它变量接收props的值时需要使用 toRef/toRefs 将props中的属性转为响应式。
- setup、data、medths可以同时存在;data可以通过 this.XXX 读取setup中的数据,setup不能读取data中的数据
7.计算属性与监视
1.computed函数,计算属性返回的是一个 ref 响应式数据对象
-
与Vue2.x中computed配置功能一致
-
写法
javascript
import {computed} from 'vue'
setup(){
...
//计算属性------简写;只读,不能修改
let fullName = computed(()=>{
return person.firstName + '-' + person.lastName
})
//计算属性------完整(考虑读和写)
let fullName = computed({
get(){
return person.firstName + '-' + person.lastName
},
set(value){
const nameArr = value.split('-')
person.firstName = nameArr[0]
person.lastName = nameArr[1]
}
})
}
2.watch函数
-
语法: watch(监听的对象, 回调函数, {配置项})
-
与Vue2.x中watch配置功能一致
-
vue3 中的 watch 只能监视以下四种数据:
-
1.ref 定义的数据
-
2.reactive 定义的数据
-
3.函数返回一个值(getter 函数)
-
4.一个包含上述内容的数组
-
两个小"坑":
-
监视reactive定义的响应式数据时:oldValue无法正确获取、强制开启了深度监视(deep配置失效)。
-
监视reactive定义的响应式数据中 某个属性 时:deep配置有效。
javascript
import {watch} from 'vue'
//情况一:监视ref定义的响应式数据(基本数据类型)
const stopWatch = watch(sum,(newValue,oldValue)=>{
console.log('sum变化了',newValue,oldValue)
let index = 0;
if(index > 0){
index++;
// 停止监视
stopWatch();
}
})
//监视多个ref定义的响应式数据(基本数据类型)
watch([sum,msg],(newValue,oldValue)=>{
console.log('newValue,oldValue 不一样',newValue,oldValue)
let [sum,msg] = newValue;
})
//情况二:监视ref定义的响应式数据(引用数据类型),需手动开启 {deep:true}
watch(person,(newValue,oldValue)=>{
console.log('newValue,oldValue 一样',newValue,oldValue); // 原因:地址没变
},{deep:true,immediate:true})
/* 情况三:监视reactive定义的响应式数据
若watch监视的是reactive定义的响应式数据,则无法正确获得oldValue!!
若watch监视的是reactive定义的响应式数据,则强制开启了深度监视,不可关闭
*/
let person = reactive({name:'muzidigbig',obj:'it',car:{car1:'aodi',car2:'BWM'}})
watch(person,(newValue,oldValue)=>{
console.log('newValue,oldValue 一样',newValue,oldValue)
},{immediate:true,deep:false}) //此处的deep配置不再奏效
//情况四:监视reactive定义的响应式数据中的某个属性(且该属性是基本数据类型)
watch(()=>person.job,(newValue,oldValue)=>{
console.log('newValue,oldValue 不一样',newValue,oldValue)
},{immediate:true,deep:true})
//监视reactive定义的响应式数据中的某个属性(且该属性是引用数据类型)
watch(()=>person.car,(newValue,oldValue)=>{
console.log('newValue,oldValue 不一样',newValue,oldValue)
},{deep:true})
// 总结:监视的要是对象里的属性,那么最好写成函数式,注意点:若是对象监视的是地址值,需要关注对象内部,需要手动开启深度监视
//情况五:监视reactive定义的响应式数据中的某些属性
watch([()=>person.job,()=>person.name],(newValue,oldValue)=>{
console.log('person的job变化了',newValue,oldValue)
})
3.watchEffect函数 -- 只会获取 newValue
-
watch的套路是:既要指明监视的属性,也要指明监视的回调。
-
watchEffect的套路是:立即运行一个函数(初始执行),同时响应式的追踪其依赖,并在依赖更改时重新执行该函数。不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。
-
watchEffect有点像computed:
-
但computed注重的计算出来的值(回调函数的返回值),所以必须要写返回值。
-
而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值。
-
computed和watch所依赖的数据必须是响应式的。
javascript
import {watchEffect} from 'vue'
// watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调。
watchEffect(()=>{
const x1 = sum.value
const x2 = person.age
console.log('watchEffect配置的回调执行了')
})
8.生命周期
9.自定义 hook 函数
-
什么是hook?------ 本质是一个函数,把setup函数中使用的Composition API进行了封装。
-
类似于vue2.x中的 mixin。
-
自定义hook的优势: 复用代码, 让setup中的逻辑更清楚易懂。
Vue Mixin 用法(实质是抽离出一个公共的vue实例)_vue app.mixin 公共函数-CSDN博客
Vue3.x 中 hooks 函数封装和使用_mxin和钩子的区别-CSDN博客
10.toRef
作用:创建一个 ref 对象,其value值指向另一个对象中的某个属性。toRef函数可以将原始数据转换为ref对象
语法:```const name = toRef(person,'name')```
应用: 要将响应式对象中的某个属性单独提供给外部使用时。
toRef 一次仅能设置一个数据,接收两个参数,第一个参数是哪个对象,第二个参数是对象的哪个属性
toRefs 作用是将响应式对象中所有属性转换为单独的响应式数据,将对象中的每个属性都做一次ref操作,使每个属性都具有响应式。
扩展:```toRefs``` 与```toRef```功能一致,但可以批量创建多个 ref 对象,语法:```toRefs(person)```
javascript
let car = reactive({ name: '奥迪', price: 30 })
// let {name,age} = car; // 此解构 name,age 不会变为响应式;响应式数据解构将失去响应式
let {name,age} = toRefs(car);
console.log(name.value)
let route = useRoute()
// let {query} = route; // query失去响应式
let {query} = toRefs(route);
总结:
-
特殊ref对象;创建的ref对象,与原始对象挂钩;不会触发渲染
-
响应式对象的处理,是加给对象的,如果对对象做了展开操作,那么就会丢失响应式的效果。
11.unref
作用:如果参数是一个ref则返回它的value,否则返回参数本身。unref():是 val = isRef(val) ? val.value : val 的语法糖。
语法:```const value = unref(refObj)```
应用: 在使用ref、reactive或readonly创建响应式数据时,它们会返回一个代理对象,而不是原始值。如果需要访问原始值,可以使用unref函数获取。
可以用于拷贝
javascript
setup() {
const user = reactive<any>({
name: '小明',
age: 10,
addr: {
province: '山东',
city: '青岛'
}
})
// const {name,age} = user; // name,age 不会变成响应式
const city = unref(user.addr.city)
return {
...toRefs(user) // 变为响应式
}
}
12.triggerRef (强制更新)
-
非递归监听,只监听首层 ,消耗的资源小;
-
配合 triggerRef 强制更新 => 性能要大于 > 直接使用 (ref 和 reactive)
-
它可以让浅层的 ref 即 shallowRef 深层属性发生改变的时候强制触发更改,比如上面触发不了响应式的代码示例加入triggerRef后
javascript
import {triggerRef, shallowRef} from 'vue';
setup(){
const obj1 = shallowRef({a:1,b:{c:2})
obj1.value.a = 2 // 页面没有发生更新,因为只监听value第一层
triggerRef(obj1); // 加入triggerRef强制触发更改,这时a就会变
}
四、其它 Composition API
1.shallowReactive 与 shallowRef
-
shallowReactive:只处理对象最顶层属性的响应式(浅响应式)。
-
shallowRef:只处理第一层的响应式(基本数据:无响应式;引用数据全部替换可响应)只能改 .value = XXX 响应; .value.name=xxx 不响应。应用场景:整体修改
-
什么时候使用?
-
如果有一个对象数据,结构比较深, 但变化时只是顶层属性变化 ===> shallowReactive。
-
如果有一个对象数据,后续功能不会修改该对象中的属性,而是生新的对象来替换 ===> shallowRef。
2.readonly 与 shallowReadonly
-
readonly: 让一个响应式数据变为只读的(深只读)。
-
语法:readonly(响应式数据)
-
shallowReadonly:让一个响应式数据变为只读的(浅只读)。
-
应用场景: 不希望数据被修改时。
3.toRaw 与 markRaw
-
toRaw (不影响本身):
-
作用:用于获取```reactive```生成的响应式对象的原始对象,toRaw 返回的对象不再是响应式的,也不会触发视图更新。
-
使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新。
-
总结:
-
不影响原数据 ,相当于拷贝当前的值
-
拷贝的值,不在是响应式对象
-
markRaw(添加 非响应对象 属性):
-
作用:标记一个对象,使其永远不会再成为响应式对象。
-
应用场景:
-
有些值不应被设置为响应式的,例如复杂的第三方类库等。
-
当渲染具有不可变数据源的大列表时,跳过响应式转换可以提高性能。
-
总结:
-
通过 markRaw 添加的值 => 其中的属性变化,页面不会监听的到
-
用于添加搞定的参数,不会发生不会的 ( 从而节约资源 )
4.customRef
-
作用:创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。
-
实现防抖效果:
javascript
<template>
<input type="text" v-model="keyword">
<h3>{{keyword}}</h3>
</template>
<script>
import {ref,customRef} from 'vue'
export default {
name:'Demo',
setup(){
// let keyword = ref('hello') //使用Vue准备好的内置ref
//自定义一个myRef
function myRef(value,delay){
let timer
//通过customRef去实现自定义
return customRef((track,trigger)=>{
return{
get(){
track() //告诉Vue这个value值是需要被"追踪"的
return value
},
set(newValue){
clearTimeout(timer)
timer = setTimeout(()=>{
value = newValue
trigger() //告诉Vue去更新界面
},delay)
}
}
})
}
let keyword = myRef('hello',500) //使用程序员自定义的ref
return {
keyword
}
}
}
</script>
5.响应式数据的判断
isRef: 检查一个值是否为一个 ref 对象
isReactive: 检查一个对象是否是由 `reactive` 创建的响应式代理
isReadonly: 检查一个对象是否是由 `readonly` 创建的只读代理
isProxy: 检查一个对象是否是由 `reactive` 或者 `readonly` 方法创建的代理
五、新的组件
1.Fragment
-
在Vue2中: 组件必须有一个根标签
-
在Vue3中: 组件可以没有根标签, 内部会将多个标签包含在一个Fragment虚拟元素中
-
好处: 减少标签层级, 减小内存占用
2.Teleport
-
什么是Teleport?------ `Teleport` 是一种能够将我们的组件html结构移动到指定位置的技术。
-
只改变了渲染的 DOM 结构,它不会影响组件间的逻辑关系。
-
to 允许接收值:
期望接收一个 CSS 选择器字符串或者一个真实的 DOM 节点。
提示:
<Teleport> 挂载时,传送的 to 目标必须已经存在于 DOM 中。理想情况下,这应该是整个 Vue 应用 DOM 树外部的一个元素。
如果目标元素也是由 Vue 渲染的,你需要确保在挂载 <Teleport> 之前先挂载该元素。
- 禁用传送功能: <Teleport to="body" :disabled="true">
html
<teleport to="移动位置">
<div v-if="isShow" class="mask">
<div class="dialog">
<h3>我是一个弹窗</h3>
<button @click="isShow = false">关闭弹窗</button>
</div>
</div>
</teleport>
3.Suspense
-
等待异步组件时渲染一些额外内容,让应用有更好的用户体验
-
使用步骤:
-
异步引入组件
javascript
import {defineAsyncComponent} from 'vue'
const Child = defineAsyncComponent(()=>import('./components/Child.vue'))
- 使用```Suspense```包裹组件,并配置好```default``` 与 ```fallback```
html
<template>
<div class="app">
<h3>我是App组件</h3>
<Suspense>
<template v-slot:default>
<Child/>
</template>
<template v-slot:fallback>
<h3>加载中.....</h3>
</template>
</Suspense>
</div>
</template>
六、其他
1.全局API的转移
-
Vue 2.x 有许多全局 API 和配置。
-
例如:注册全局组件、注册全局指令等。
javascript
//注册全局组件
Vue.component('MyButton', {
data: () => ({
count: 0
}),
template: '<button @click="count++">Clicked {{ count }} times.</button>'
})
//注册全局指令
Vue.directive('focus', {
inserted: el => el.focus()
}
-
Vue3.0中对这些API做出了调整:
-
将全局的API,即:```Vue.xxx```调整到应用实例(```app```)上
2.其他改变
1.移除keyCode作为 v-on 的修饰符,同时也不再支持```config.keyCodes```
2.移除```v-on.native```修饰符
javascript
- 父组件中绑定事件
```vue
<my-component
v-on:close="handleComponentEvent"
v-on:click="handleNativeClickEvent"
/>
```
- 子组件中声明自定义事件
```vue
<script>
export default {
emits: ['close']
}
</script>
```
3.移除过滤器(filter)
过滤器虽然这看起来很方便,但它需要一个自定义语法,打破大括号内表达式是 "只是 JavaScript" 的假设,这不仅有学习成本,而且有实现成本!建议用方法调用或计算属性去替换过滤器。
七、vue-router 的使用
1.使用路由
第一步:安装
javascript
npm install vue-router@4
第二步:创建 router 实例
javascript
// router/index.js
// Vue 3版本则改为在createRouter()API中通过history参数来设置,且history参数是必须的,如果不填会报错。
import { createRouter, createWebHashHistory, createWebHistory } from "vue-router";
const routes = [
{
path: '/',
name: 'home',
component: () => import("../views/Main.vue"),
// 第一种写法:将路由收到的 params 参数作为 props 传给路由组件
// props:true,
// 第二种写法:函数写法;可以自己决定将什么作为 props 传递
props(route){
return route.query;
},
// 第三种写法:对象写法;可以自己决定将什么作为 props 传递
/*props:{
},*/
// children: [{}]
}
]
const router = createRouter({
history: createWebHashHistory(),
routes, // `routes: routes` 的缩写
})
// 获取所有路由 router.getRoutes()/router.getRoute(); 获取这个这个路由数据的数组实例
export default router;
第三步:在 main.js 中引入 router/index.js
javascript
import router from "./router/index";
//确保 _use_ 路由实例使 //整个应用支持路由。
app.use(router)
- router-link
router-link 来创建链接。这使得 Vue Router 可以在不重新加载页面的情况下更改 URL,处理 URL 的生成以及编码
<router-link to="/">首页</router-link>
- router-view
router-view 将显示与 url 对应的组件。你可以把它放在任何地方,以适应你的布局。
<router-view></router-view>
2.导航守卫
2.1 全局导航守卫
- 全局导航守卫
全局前置钩子 router.beforeEach((to, from, next) => { ...})
在路由切换开始之前执行,可以用来进行权限控制或者全局拦截等操作。
全局解析守卫 router.beforeResolve 注册一个全局守卫。这和 router.beforeEach 类似,因为它在每次导航时都会触发,不同的是,解析守卫刚好会在导航被确认之前、所有组件内守卫和异步路由组件被解析之后调用。
全局后置钩子 router.afterEach((to, from) => { ...})
在路由切换完成之后执行,可以用来进行一些全局的数据清理或者动画效果等操作。
2.2 路由独享的守卫
- 路由独享的守卫
router.beforeEnter((to, from) => { ...})
在路由进入之前执行,可以用来进行路由独享的权限控制等操作。
javascript
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
2.3 组件内的守卫
- 组件内的守卫
beforeRouteEnter
beforeRouteUpdate
在当前路由被复用时执行,例如在同一个路由中切换参数时执行。
beforeRouteLeave
在路由离开之前执行,可以用来进行离开确认等操作。
javascript
const UserDetails = {
template: `...`,
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被验证前调用
// 不能获取组件实例 `this` !
// 因为当守卫执行时,组件实例还没被创建!
},
beforeRouteUpdate(to, from) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
// 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
},
beforeRouteLeave(to, from) {
// 在导航离开渲染该组件的对应路由时调用
// 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
},
}
beforeRouteEnter 守卫 不能 访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。
不过,你可以通过传一个回调给 next 来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数
beforeRouteEnter (to, from, next) {
next(vm => {
// 通过 `vm` 访问组件实例
})
}
注意 beforeRouteEnter 是支持给 next 传递回调的唯一守卫。对于 beforeRouteUpdate 和 beforeRouteLeave 来说,this 已经可用了,所以不支持传递回调,因为没有必要了。
beforeRouteUpdate (to, from, next) {
// just use `this`
this.name = to.params.name
next()
}
这个 离开守卫 通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消。
beforeRouteLeave (to, from, next) {
const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
if (answer) {
next()
} else {
next(false)
}
}
完整的导航解析流程
1.导航被触发。
2.在失活的组件里调用 beforeRouteLeave 守卫。
3.调用全局的 beforeEach 守卫。
4.在重用的组件里调用 beforeRouteUpdate 守卫(2.2)。
5.在路由配置里调用 beforeEnter 。
6.解析异步路由组件。
7.在被激活的组件里调用 beforeRouteEnter 。
8.调用全局的 beforeResolve 守卫(2.5+)。
9.导航被确认。
10.调用全局的 afterEach 钩子。
11.触发 DOM 更新(mounted)。
12.调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例 会作为回调函数的参数传入。
3.路由组件传参
方式一:获取当前路由对象
当我们获取路由参数时,通常在模板中使用 $route
,在逻辑中调用 useRoute()
方法,如:
javascript
<template>
<div>User {{ $route.params.id }}</div>
</template>
<script setup lang="ts">
import { useRouter, useRoute } from 'vue-router'
// 获取路由实例对象
let $router = useRouter()
// 获取当前路由对象
const $route = useRoute()
console.log($route.params.id)
</script>
方式二:props 解耦
以上方法比较麻烦,而且与路由紧密耦合,不利于组件封装。我们可以在创建路由时通过 props
配置来解除这种行为:
javascript
const routes = [
{
path: '/user/:id',
name: 'user',
component: User,
props: true
}
]
此时 route.params
将直接被设置为组件的 props
,这样组件就和路由参数解耦了:
javascript
<template>
<div>User {{ id }}</div>
</template>
<script setup lang="ts">
const props = defineProps<{
id: string
}>()
console.log(props.id)
</script>
布尔模式
当 props
设置为 true
时,route.params
将被设置为组件的 props
。
命名视图
对于有命名视图的路由,你必须为每个命名视图定义 props
配置:
javascript
const routes = [
{
path: '/user/:id',
name: 'User',
components: { default: User, sidebar: Sidebar },
props: { default: true, sidebar: false }
}
]
对象模式
当 props
是一个对象时,它会将此对象设置为组件 props
。当 props
是静态的时候很有用。
javascript
const routes = [
{
path: '/user',
component: User,
props: { newsletterPopup: false }
}
]
函数模式
我们也可以创建一个返回 props
的函数。这允许你将参数转换为其他类型:
javascript
const routes = [
{
path: '/user',
component: User,
props: route => ({ id: route.query.id })
}
]
如 /user?id=123
参数会被转为 { id: '123' }
作为 props
传给组件。
4.动态路由
添加路由
当我们做用户权限的时候,添加路由非常有用。可以使用 router.addRoute()
来添加一个路由:
javascript
router.addRoute({ path: '/about', name: 'about', component: About })
注意:跟之前版本不同的是,路由只能一个一个添加,不能批量添加。
删除路由
以下几个方法都可以删除路由:
1.通过使用 router.removeRoute()
按名称删除路由
javascript
router.addRoute({ path: '/about', name: 'about', component: About })
// 删除路由
router.removeRoute('about')
2.通过添加一个名称相同的路由,替换掉之前的路由:
javascript
router.addRoute({ path: '/about', name: 'about', component: About })
// 这将会删除之前已经添加的路由,因为他们具有相同的名字且名字必须是唯一的
router.addRoute({ path: '/other', name: 'about', component: Other })
3.通过调用 router.addRoute()
返回的回调函数:
javascript
const removeRoute = router.addRoute(routeRecord)
removeRoute() // 删除路由如果存在的话
当路由没有名称时,这种方法非常有用。
添加嵌套路由
要将嵌套路由添加到现有的路由中,可以将路由的 name
作为第一个参数传递给 router.addRoute()
,这和通过 children
添加的效果一样:
javascript
router.addRoute({ name: 'admin', path: '/admin', component: Admin })
// 添加嵌套路由
router.addRoute('admin', { path: 'settings', component: AdminSettings })
相当于:
javascript
router.addRoute({
name: 'admin',
path: '/admin',
component: Admin,
children: [{ path: 'settings', component: AdminSettings }]
})
5. 捕获所有路由
必须使用带有正则表达式的参数进行定义: /:catchAll(.*),一般放到路由的最下面
javascript
// vue2
const router = new Router({
mode: history,
routes: [
...
{
path: '*'
}
]
})
// vue3
import {createRouter, createWebHistory} from 'vue-next-router';
const router = createRouter({
history: createWebHistory(),
routes: [
...
{
// vue2使用* vue3使用 /:pathMatch('*') 或者 /:pathMatch('*')* 或者 /:catchAll(.*)*
path: '/:catchAll(.*)*',
name: '404',
component: () => import("../views/404.vue"),
}
]
})
当路由为 /user/123/sunny 时,捕获到的 params 为 {"id": "123", "catchAll": "/sunny"}
6.监听路由变化 区别
javascript
import { watch} from 'vue'
import { useRoute } from 'vue-router'
setup(){
const route = useRoute();
watch(route.query,(query)=>{ console.log(query); })
}
7.push或者resolve一个不存在的命名路由时,会引发错误,不会导航到根路由,页面空白
javascript
// vue2 当push一个不存在的路由,路由会导航到根路由,并且不会渲染任何内容
const router = new Router({
mode: history,
routes: [
{
path: /,
name: foo,
component: Foo
}
]
}
this.$router.push({
name: baz
});
// vue3
const router = createRouter({
history: routerHistory(),
routes: [{
path: /,
name: foo,
component: Foo
}]
});
...
import {useRouter} from 'vue-next-router';
const router = userRouter()
router.push({name: baz})); // 这段代码会报错
八、组件间的通信
在setup语法糖下,defineEmits和defineProps会被自动引入。其它情况下,需要主动引入。
1.props (可父传子--非函数,也可子传父--函数)
props可以实现父子组件通信,在vue3中我们可以通过 defineProps 获取父组件传递的数据。且在组件内部可不需引入defineProps方法可以直接使用!
子组件获取到props数据就可以在模板中使用了,但是切记props是只读的(只能读取,不能修改)
javascript
// import {defineProps} from 'vue'
父组件给子组件传递数据
<Child info="我爱祖国" :money="money" :sentToy="getToy"></Child>
const getToy = (value) => {
console.log(value)
}
子组件获取父组件传递数据:方式1
let props = defineProps({
info:{
type:String,//接受的数据类型
default:'默认参数',//接受默认数据
},
money:{
type:Number,
default:0
}})
子组件获取父组件传递数据:方式2
// defineProps(["info",'money','sentToy']); 也可默认
let props = defineProps(["info",'money','sentToy']);
子可触发 props.sentToy('XXX') 并传递参数
子组件获取父组件传递数据:方式3 限制类型
let props = defineProps<{info:string,money:number}>();
子组件获取父组件传递数据:方式4 限制类型+限制必要性+指定默认类型
import {defineProps, withDefaults} from 'vue'
withDefaults(defineProps<{info:string,money?:number}>(),{
info:'默认参数',
money:0,
list:()=>[{},{}]
})
html模板中使用1:
props.info
2.自定义事件(子传父)
在vue框架中事件分为两种:一种是原生的DOM事件,另外一种自定义事件。
原生DOM事件可以让用户与网页进行交互,比如click、dbclick、change、mouseenter、mouseleave....
自定义事件可以实现子组件给父组件传递数据
javascript
原生DOM事件
```js
<pre @click="handler"> 我是祖国的老花骨朵 </pre>
当前代码给pre标签绑定原生DOM事件点击事件,默认会给事件回调注入event事件对象。当然点击事件想注入多个参数可以按照下图操作。但是切记注入的事件对象务必叫做$event.
<div @click="handler1(1,2,3,$event)">我要传递多个参数</div>
```
- 在vue3框架click、dbclick、change(这类原生DOM事件),不管是在标签、自定义标签上(组件标签)都是原生DOM事件。
- vue2中却不是这样的,在vue2中组件标签需要通过 .native 修饰符才能变为原生DOM事件
子传父-自定义事件 defineEmits
javascript
自定义事件可以实现子组件给父组件传递数据.在项目中是比较常用的。
比如在父组件内部给子组件(Event2)绑定一个自定义事件
<Event2 @xxx="handler3" @changeMoney="handerFun"></Event2>
在Event2子组件内部触发这个自定义事件
<template>
<div>
<h1>我是子组件2</h1>
<button @click="handler">点击我触发xxx自定义事件</button>
<button @click="$emit('changeMoney',item)">点击我触发xxx自定义事件</button>
</div>
</template>
<script setup lang="ts">
let $emit = defineEmits(["xxx"]);
const handler = () => {
$emit("xxx", "法拉利", "茅台");
};
const emit = defineEmits<{
(e: 'changeMoney', money: number): void
(e: 'changeCar', car: string): void
}>()
我们会发现在script标签内部,使用了defineEmits方法,此方法是vue3提供的方法,不需要引入直接使用。defineEmits方法执行,传递一个数组,数组元素即为将来组件需要触发的自定义事件类型,此方执行会返回一个$emit方法用于触发自定义事件。
当点击按钮的时候,事件回调内部调用$emit方法去触发自定义事件,第一个参数为触发事件类型,第二个、三个、N个参数即为传递给父组件的数据。
3.mitt 的使用(任意组件通信,一般用于兄弟组件间的通信)
简单的说 mitt 就是一个全局的总线程,在 vue2.0中,我们经常会使用EventBus去处理问题,这时候你会说啥是总线程呢。更简单的说,其实就是发布订阅事件。
全局事件总线可以实现任意组件通信,在vue2中可以根据VM与VC关系推出全局事件总线。
但是在vue3中没有Vue构造函数,也就没有Vue.prototype.以及组合式API写法没有this,
那么在Vue3想实现全局事件的总线功能就有点不现实啦,如果想在Vue3中使用全局事件总线功能
第一步:安装
npm install --save mitt
第二步:创建一个 EventBus.ts 文件
javascript
import mitt from 'mitt'
export default emitter = mitt()
第三步:在需要的组件中引入 EventBus.ts
javascript
const TOPIC = 'topic'; // 事件名称
// 订阅
emitter.on(TOPIC, (data)=>{
console.log(data);
})
// 发布事件
emitter.emit(TOPIC, { a: 'b' })
// 取消订阅(一般在 onUnmounted 中移除)
emitter.off(TOPIC, onFoo)
// 清空所有的事件
emitter.all.clear()
4.refs(父->子)与$parent(子->父) 加上defineExpose对外暴露来拿到数据进行修改
ref,提及到ref可能会想到它可以获取元素的DOM或者获取子组件实例的VC。既然可以在父组件内部通过ref获取子组件实例VC,那么子组件内部的方法与响应式数据父组件可以使用的。
标签的 ref 属性
-
1.用在普通 DOM 标签上,获取的是 DOM 节点。
-
2.用在组件标签上,获取的是组件实例对象。
javascript
<h2 ref="title">TTTTT</h2>
let title = ref();
比如:在父组件挂载完毕获取组件实例
父组件内部代码:
javascript
<template>
<div>
<h1>ref与$parent</h1>
<button @click="getAllChild($refs)">获取所有的子组件实例对象</button>
<Son ref="son"></Son>
</div>
</template>
<script setup lang="ts">
import Son from "./Son.vue";
import { onMounted, ref } from "vue";
// 获取单一的子组件实例对象
const son = ref();
function getAllChild(refs:object){
}
onMounted(() => {
console.log(son.money);
});
</script>
但是需要注意,如果想让父组件获取子组件的数据或者方法需要在子组件通过 defineExpose({}) 对外暴露,因为vue3中组件内部的数据对外"关闭的",外部不能访问
javascript
// Son.vue
<script setup lang="ts">
import { ref } from "vue";
//数据
let money = ref(1000);
//方法
const handler = ()=>{
}
defineExpose({
money,
handler
})
</script>
$parent可以获取某一个组件的父组件实例VC,因此可以使用父组件内部的数据与方法。必须子组件内部拥有一个按钮点击时候获取父组件实例,当然父组件的数据与方法需要通过 defineExpose 方法对外暴露
javascript
<button @click="handler($parent)">点击我获取父组件实例</button>
function handler(parent:object){
}
5.v-model
- v-model指令可是收集表单数据(数据双向绑定),除此之外它也可以实现父子组件数据同步。
而v-model实指利用**props[modelValue]与自定义事件[update:modelValue]**实现的。
-
下方代码:相当于给组件Child传递一个props(modelValue)与绑定一个自定义事件update:modelValue实现父子组件数据同步
-
在vue3中一个组件可以通过使用多个v-model,让父子组件多个数据同步,下方代码相当于给组件Child传递两个props分别是pageNo与pageSize,以及绑定两个自定义事件update:pageNo与update:pageSize实现父子数据同步
javascript
<Child v-model="username" v-model:pageNo="msg" v-model:pageSize="msg1"></Child>
是指:
<Child :modelValue="username" @update:modelValue="username = $event"></Child>
子组件 Child.vue
<script setup lang="ts">
let props = defineProps(["modelValue","pageNo","pageSize","xxx"]);
let $emit = defineEmits(["upadte:modelValue","update:pageNo","update:pageSize","update:xxx"]);
const handler = () => {
$emit("update:xxx", "法拉利", "茅台");
};
</script>
$event 到底是啥?啥时候能 .target?
对于原生事件,$event 就是事件对象 ==> 能.target
对于自定义事件,$event 就是触发事件时 所传递的数据 ==> 不能.target
6.provide 与 inject 跨组件通信
-
作用:实现祖与后代/孙组件间通信
-
套路:父组件有一个 `provide` 选项来提供数据,后代组件有一个 `inject` 选项来开始使用这些数据(注入数据)
-
具体写法:
- 祖组件中:
javascript
import { provide } from "vue";
setup(){
......
let car = reactive({name:'奔驰',price:'40万'})
function updateCar(value:string){
car.price = value
}
// 提供数据;参数:provide(key,value)
provide('carContext',{car,updateCar})
......
}
- 后代组件中:
javascript
<!-- 孙传组 -->
<button @click="updateCar('66万')"> 更新car</button>
import { inject } from "vue";
setup(props,context){
......
// 注入祖先组件提供的数据 inject(key, 默认值)
// 需要参数:即为祖先提供的数据的 key
// const car = inject('carContext')
let {car,updateCar} = inject('carContext',{car:{name:'奥迪',price:'50w'},updateCar:(value:string)=>{}})
return {car,updateCar}
......
}
6.$attrs 与 useAttrs
- 在Vue3中可以利用 useAttrs 方法获取组件的属性与事件(包含:原生DOM事件或者自定义事件),此函数功能类似于Vue2框架中attrs属性与listeners方法。
javascript
比如:在父组件内部使用一个子组件my-button
<my-button type="success" size="small" title='标题' :sendFun='getA' @click="handler" v-bind="$attrs"></my-button>
当template有根元素的时候,绑定到组件上的属性和事件会自动继承到根元素上
子组件内部可以通过 useAttrs 方法获取组件属性与事件.因此你也发现了,它类似于props,可以接受父组件传递过来的属性与属性值。需要注意如果 props接受了某一个属性,useAttrs 方法返回的对象身上就没有相应属性与属性值。
<script setup lang="ts">
// 如果你不想要一个组件自动地继承 attribute,你可以在组件选项中设置 inheritAttrs: false 不论单节点还是多节点的组件,都不会继承任何 attr 和 事件!(默认为true,多根节点不设置为false会有警告)
// 组件为多根节点,且 inheritAttrs: true 的时候,如果该组件使用有` v-bind="$attrs" `,组件标签绑定了attr和事件,是不会抛出警告的!但是也不会继承这些 attr 和 事件。
// 禁止继承的内容有:attribute、各种事件(emit事件除外)
import {useAttrs} from 'vue';
let attrs = useAttrs();
</script>
在使用 useAttrs 和 defineProps 接收属性时,使用 defineProps 接收的属性会优先级更高,因为它可以对每个属性进行类型验证和默认值设置,同时这些属性会被添加到组件实例的 $props 属性中。
7.pinia 的使用
Pinia 是 Vue 的专属状态管理库,它允许你跨组件或页面共享状态(可以实现任意组件之间的通信)
Pinia与Vuex的不同
- actions 中支持同步和异步方法修改 state 状态。
- 与 TypeScript 一起使用具有可靠的类型推断支持。
- 不再有模块嵌套,只有 Store 的概念,Store 之间可以相互调用。
- 支持插件扩展,可以非常方便实现本地存储等功能。
- 更加轻量,压缩后体积只有 2kb 左右。
- mutations 不复存在。只有 state 、getters 、actions。
第一步:安装
javascript
npm install pinia
第二步:main.js 中引入
javascript
// 创建大仓库
import { createPinia } from 'pinia'
const pinia = createPinia()
app.use(pinia)
第三步:创建 store
javascript
// stores/counter.js
// 创建小仓库
import { defineStore } from 'pinia'
// 第一个参数:小仓库Id 第二个参数:小仓库配置对象
// defineStore 方法执行会返回一个函数,函数作用就是让组件可以获取到仓库数据
export const useCounterStore = defineStore('counter', {
state: () => { // 存储数据
return { count: 0 }
},
// 也可以这样定义
// state: () => ({ count: 0 })
getters: { // 计算属性
double: (state) => state.count * 2,
},
actions: { // 处理业务
// 注意:函数没有 context 上下文对象
increment() {
this.count++
},
// 进行异步操作
async fetchData() {
try {
this.loading = true
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1')
const data = await response.json()
this.data = data
this.error = null
} catch (error) {
this.error = error.message
} finally {
this.loading = false
}
},
resetAll() {
// 调用 reset 方法可以将store中的所有状态重置为它们的默认值
this.$reset();
/**
* 可能需要手动重置store中的数据
Object.assign(this.$state, this.$options.state())
*/
},
},
})
第四步:在一个组件中使用该 store
javascript
<script setup>
import { useCounterStore } from '@/stores/counter'
import {storeToRefs} from 'pinia'
// state 中的属性(方法),都可以通过store对象直接访问
const counter = useCounterStore()
/*
store 实例本身就是一个 reactive 对象,
可以通过它直接访问 state 中的数据
但是如果直接将 state 中数据解构出来,那么数据将丧失响应性
可以通过 storeToRefs 来对store进行解构:
它可以将state和getters解构为ref属性,从而保留其响应性
state的修改:
1.直接修改
2.通过 $patch({}) // 批量修改
3.通过 $patch((state) => {}) 传函数的形式修改
4.直接替换 $state
5.重置 state ==> store实例.$reset();
*/
counter.count += 1;
/*
stuStore.$patch({
count:1,
skills:["救命毫毛"] // 直接覆盖
})
*/
// let {count} = counter; // 直接解构将失去响应式
// storeToRefs 只会关注 store 中数据,不会对方法进行 ref 包裹
let {count} = storeToRefs(counter);
/*
store的订阅:
- 当store中的state发生变化时,做一些响应的操作
- store.$subscribe(函数,配置对象)
- 与 watch() 相比,使用$subscribe()的优点是,store 多个状态发生变化之后,回调函数只会执行一次。
*/
counter.$subscribe(
(mutation, state) => {
// mutation 表示修改的信息
console.log("mutation", mutation);
console.log("state 发生变化了", state.age);
// 使用订阅不要在回调函数中直接修改state
// state.age++;
},
// { detached: true } 卸载组件后保留它们
{ detached: true }
);
// 仓库调用自身的方法去修改自身的数据
// counter.increment()
也可以监听 pinia
实例上所有 store
的变化:
javascript
// src/main.ts
import { watch } from 'vue'
import { createPinia } from 'pinia'
const pinia = createPinia()
watch(
pinia.state,
(state) => {
// 每当状态发生变化时,将所有 state 持久化到本地存储
localStorage.setItem('piniaState', JSON.stringify(state))
},
{ deep: true }
)
组合式api的写法:
javascript
// 组合式api
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter',() => {
let todos = ref([{id:1,name:'muzidigbig',age:28}]);
let arr = ref([1,2,3])
const total = computed(() => {
return arr.value.reduce((tot,next) => {
return tot+next;
},0)
})
// 务必返回一个对象:属性与方法可以提供给组件使用
return {
todos,
total,
updateTodo(){
todos.value.push({id:2,name:"组合式api方法",age:22})
}
}
})
实现本地存储
相信大家使用 Vuex
都有这样的困惑,F5 刷新一下数据全没了。在我们眼里这很正常,但在测试同学眼里这就是一个 bug 。Vuex
中实现本地存储比较麻烦,需要把状态一个一个存储到本地,取数据时也要进行处理。而使用 Pinia ,一个插件就可以搞定。
javascript
npm i pinia-plugin-persist
然后引入插件,并将此插件传递给 pinia :
javascript
// src/main.ts
import { createPinia } from 'pinia'
import piniaPluginPersist from 'pinia-plugin-persist'
const pinia = createPinia()
pinia.use(piniaPluginPersist)
接着在定义 store 时开启 persist 即可:
javascript
// src/stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 1 }),
// 开启数据缓存
persist: {
enabled: true
}
})
这样,无论你用什么姿势刷新,数据都不会丢失啦!
默认情况下,会以 storeId 作为 key 值,把 state 中的所有状态存储在 sessionStorage
中。我们也可以通过 strategies
进行修改:
javascript
// 开启数据缓存
persist: {
enabled: true,
strategies: [
{
// 存储的 key 值,默认为 storeId
key: 'myData',
// 存储的位置,默认为 sessionStorage
storage: localStorage,
// 需要存储的 state 状态,默认存储所有的状态
paths: ['name', 'age'],
}
]
}
九、组件起名称
方式一:
javascript
<script setup lang="ts"></script>
<!-- vue2的方式给组件起名字 -->
<script lang="ts">
export default {
name: "Menu",
};
方式二:
第一步:安装插件
javascript
npm install vite-plugin-vue-setup-extend
第二步:vite.config.ts 中引入:
javascript
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
export default defineConfig({
plugins: [vue(),VueSetupExtend()],
})
第三步:组件中设置 name :
javascript
<script setup lang="ts" name='Menu'></script>
十、其它
1.获取上下文对象
Vue3 的 setup
中无法使用 this 这个上下文对象。可能刚接触 Vue3 的兄弟会有点懵,我想使用 this 上的属性和方法应该怎么办呢。虽然不推荐这样使用,但依然可以通过 getCurrentInstance
方法获取上下文对象:
javascript
<script setup>
import { getCurrentInstance } from 'vue'
// 以下两种方式都可以获取到上下文对象
const { ctx } = getCurrentInstance()
const { proxy } = getCurrentInstance()
</script>
值得注意的是 ctx
只能在开发环境使用,生成环境为 undefined
。 推荐使用 proxy
,在开发环境和生产环境都可以使用。
案例:获取挂载到全局的api getCurrentInstance类似vue2中的this
const { proxy } = getCurrentInstance(); proxy.$refs