1. vue3中的ref、toRef、toRefs
ref: 用于创建一个包装对象,将普通 JavaScript 值转换为响应式对象。通常用于定义组件内部的响应式数据。
import { ref } from 'vue';
const count = ref(0); // 创建一个包装对象,初始值为 0
console.log(count.value); // 访问响应式数据时需要使用 .value 属性
toRef: 用于创建一个基于响应式对象的引用。当我们需要在组件内将一个响应式对象的属性传递给另一个函数或组件时,可以使用 toRef 创建一个基于响应式对象属性的引用,而不是直接将属性值传递。
import { toRef } from 'vue';
const state = reactive({ count: 0 });
// 创建一个基于 state.count 的引用
const countRef = toRef(state, 'count');
console.log(countRef.value); // 访问引用的值
toRefs: 用于将一个响应式对象的所有属性转换为引用。通常在组件的 setup 函数中使用,将响应式对象解构为多个引用,方便在模板中使用。
import { reactive, toRefs } from 'vue';
const state = reactive({ count: 0, name: 'Vue' });
// 将 state 的所有属性转换为引用
const { count, name } = toRefs(state);
console.log(count.value); // 访问引用的值
console.log(name.value); // 访问引用的值
总之,ref 用于创建一个包装对象,将普通值转换为响应式对象;toRef 用于创建一个基于响应式对象的引用;toRefs 用于将一个响应式对象的所有属性转换为引用。
2. 为什么template中不用加.value吗?
其实 Vue3 中有个方法proxyRefs,这属于底层 API 方法,在官方文档中并没有阐述,但是 Vue 里是可以导出这个方法。
例如:
<script setup>
import { onMounted, proxyRefs, ref } from "vue";
const user = {
name: "wendZzoo",
age: ref(18),
};
const _user = proxyRefs(user);
onMounted(() => {
console.log(_user.name);
console.log(_user.age);
console.log(user.age);
});
</script>
上面代码定义了一个普通对象user,其中age属性的值是ref类型。当访问age值的时候,需要通过user.age.value,而使用了proxyRefs,可以直接通过user.age来访问。
这也就是为何template中不用加.value的原因,Vue3 源码中使用proxyRefs方法将setup返回的对象进行处理。
3. ref 对比 reactive
宏观角度看:
ref用来定义:基本类型数据、对象类型数据;reactive用来定义:对象类型数据。
区别:
ref创建的变量必须使用.value(可以使用volar插件自动添加.value)。reactive重新分配一个新对象,会失去响应式(可以使用Object.assign去整体替换)。
使用原则:
- 若需要一个基本类型的响应式数据,必须使用
ref。 - 若需要一个响应式对象,层级不深,
ref、reactive都可以。 - 若需要一个响应式对象,且层级较深,推荐使用
reactive。
4. provide/inject如何实现跨层传递数据
定义
-
provide和inject用来实现组件之间的数据传递,它们更适用跨多个层级的组件传递数据,而不仅仅是父子组件之间的通信。 -
provide:用于提供可以被后代组件注入的值。 -
provide和inject通常成对一起使用,使一个祖先组件作为其后代组件的依赖注入方,无论这个组件的层级有多深都可以注入成功,只要他们处于同一条组件链上。 -
这个
provide选项应当是一个对象或是返回一个对象的函数。这个对象包含了可注入其后代组件的属性。 -
inject:用于声明要通过从上层提供方匹配并注入进当前组件的属性。
实现
新建项目 App.js 代码如下:
import { h, provide, inject } from "vue";
const Provider = {
name: "provider",
setup() {
provide("foo", "fooVal");
provide("bar", "barVal");
return {};
},
render() {
return h("div", {}, [h("p", {}, "provider"), h(Consumer)]);
},
};
const Consumer = {
name: "consumer",
setup() {
const foo = inject("foo");
const bar = inject("bar");
return {
foo,
bar
};
},
render() {
return h("div", {}, [
h("p", {}, `consumer:${this.foo} - ${this.bar}`),
]);
},
};
export const App = {
name: "app",
render() {
return h("div", {}, [h("p", {}, "app"), h(Provider)]);
},
setup() {
return {};
},
};
- 以上代码,定义了三个组件
app,provide,consumer形成一个树状的层级关系。 app组件中引用了provide组件。provide组件中注入了foo和bar,并且引用了consumer组件。consumer组件通过inject获取到foo和bar,并在组件内部渲染这两个值。
4. props 父子传值
父组件通过v-bind绑定一个数据,然后子组件通过defineProps接受传过来的值,
如以下代码
给Menu组件 传递了一个title 字符串类型是不需要v-bind
<template>
<div class="layout">
<Menu title="我是标题"></Menu>
<div class="layout-right">
<Header></Header>
<Content></Content>
</div>
</div>
</template>
传递非字符串类型需要加v-bind 简写 冒号
<template>
<div class="layout">
<Menu v-bind:data="data" title="我是标题"></Menu>
<div class="layout-right">
<Header></Header>
<Content></Content>
</div>
</div>
</template>
<script setup lang="ts">
import Menu from './Menu/index.vue'
import Header from './Header/index.vue'
import Content from './Content/index.vue'
import { reactive } from 'vue';
const data = reactive<number[]>([1, 2, 3])
</script>
子组件接受值
通过defineProps 来接受 defineProps是无须引入的直接使用即可
如果我们使用的TypeScript
可以使用传递字面量类型的纯类型语法做为参数
如 这是TS特有的
<template>
<div class="menu">
菜单区域 {{ title }}
<div>{{ data }}</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
title:string,
data:number[]
}>()
</script>
如果你使用的不是TS
defineProps({
title:{
default:"",
type:string
},
data:Array
})
TS 特有的默认值方式
withDefaults是个函数也是无须引入开箱即用接受一个props函数第二个参数是一个对象设置默认值
type Props = {
title?: string,
data?: number[]
}
withDefaults(defineProps<Props>(), {
title: "张三",
data: () => [1, 2, 3]
})
子组件给父组件传参
是通过defineEmits派发一个事件
<template>
<div class="menu">
<button @click="clickTap">派发给父组件</button>
</div>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
const list = reactive<number[]>([4, 5, 6])
const emit = defineEmits(['on-click'])
const clickTap = () => {
emit('on-click', list)
}
</script>
我们在子组件绑定了一个click 事件 然后通过defineEmits 注册了一个自定义事件
点击click 触发 emit 去调用我们注册的事件 然后传递参数
父组件接受子组件的事件
<template>
<div class="layout">
<Menu @on-click="getList"></Menu>
<div class="layout-right">
<Header></Header>
<Content></Content>
</div>
</div>
</template>
<script setup lang="ts">
import Menu from './Menu/index.vue'
import Header from './Header/index.vue'
import Content from './Content/index.vue'
import { reactive } from 'vue';
const data = reactive<number[]>([1, 2, 3])
const getList = (list: number[]) => {
console.log(list,'父组件接受子组件');
}
</script>
我们从Menu 组件接受子组件派发的事件on-click 后面是我们自己定义的函数名称getList
会把参数返回过来
子组件暴露给父组件内部属性
通过defineExpose
我们从父组件获取子组件实例通过ref
<Menu ref="menus"></Menu>
const menus = ref(null)
然后打印menus.value 发现没有任何属性
这时候父组件想要读到子组件的属性可以通过 defineExpose暴露
const list = reactive<number[]>([4, 5, 6])
defineExpose({
list
})
5. Watch 和 WatchEffect
Vue的watch和watchEffect函数允许我们观察值的变化并相应地做出反应。
watch函数用于监视特定数据的变化,并在数据变化时执行回调函数。它接受两个参数:要监视的数据和回调函数。当监视的数据发生变化时,回调函数将被触发。watchEffect函数也用于监视数据的变化,但它不需要指定要监视的特定数据。相反,它会自动追踪在其函数体中使用的任何响应式数据,并在这些数据发生变化时触发回调函数。
import { ref, watch, watchEffect } from 'vue';
const count = ref(0);
watch(count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`);
});
watchEffect(() => {
console.log(`Count is: ${count.value}`);
});
在这个例子中,watch函数观察count值的变化,而watchEffect函数在每次渲染后观察count值。
注意点:
- 监视的要是对象里的属性,那么最好写函数式
- 若是对象监视的是地址值,需要关注对象内部,需要手动开启深度监视。
watch(() => person.car, (newValue,oldValue)=>{
console.log('person.car变化了',newValue,oldValue)
}, { deep:true })
6. v-model
v-model的本质
<!-- 使用v-model指令 -->
<input type="text" v-model="userName">
<!-- v-model的本质是下面这行代码 -->
<input
type="text"
:value="userName"
@input="userName =(<HTMLInputElement>$event.target).value"
>
组件标签上的v-model的本质::moldeValue + update:modelValue事件。
<!-- 组件标签上使用v-model指令 -->
<AtguiguInput v-model="userName"/>
<!-- 组件标签上v-model的本质 -->
<AtguiguInput :modelValue="userName" @update:model-value="userName = $event"/>
AtguiguInput组件中:
<template>
<div class="box">
<!--将接收的value值赋给input元素的value属性,目的是:为了呈现数据 -->
<!--给input元素绑定原生input事件,触发input事件时,进而触发update:model-value事件-->
<input
type="text"
:value="modelValue"
@input="emit('update:model-value',$event.target.value)"
>
</div>
</template>
<script setup lang="ts" name="AtguiguInput">
// 接收props
defineProps(['modelValue'])
// 声明事件
const emit = defineEmits(['update:model-value'])
</script>
也可以更换value,例如改成abc
<!-- 也可以更换value,例如改成abc-->
<AtguiguInput v-model:abc="userName"/>
<!-- 上面代码的本质如下 -->
<AtguiguInput :abc="userName" @update:abc="userName = $event"/>
7. $attrs
$attrs用于实现当前组件的父组件,向当前组件的子组件通信(祖→孙)。
具体说明:$attrs是一个对象,包含所有父组件传入的标签属性。
父组件:
<template>
<div class="father">
<h3>父组件</h3>
<Child :a="a" :b="b" :c="c" :d="d" v-bind="{x:100,y:200}" :updateA="updateA"/>
</div>
</template>
<script setup lang="ts" name="Father">
import Child from './Child.vue'
import { ref } from "vue";
let a = ref(1)
let b = ref(2)
let c = ref(3)
let d = ref(4)
function updateA(value){
a.value = value
}
</script>
子组件:
<template>
<div class="child">
<h3>子组件</h3>
<GrandChild v-bind="$attrs"/>
</div>
</template>
<script setup lang="ts" name="Child">
import GrandChild from './GrandChild.vue'
</script>
孙组件:
<template>
<div class="grand-child">
<h3>孙组件</h3>
<h4>a:{{ a }}</h4>
<h4>b:{{ b }}</h4>
<h4>c:{{ c }}</h4>
<h4>d:{{ d }}</h4>
<h4>x:{{ x }}</h4>
<h4>y:{{ y }}</h4>
<button @click="updateA(666)">点我更新A</button>
</div>
</template>
<script setup lang="ts" name="GrandChild">
defineProps(['a','b','c','d','x','y','updateA'])
</script>
8. mitt
安装mitt
npm i mitt
新建文件:src\utils\emitter.ts
// 引入mitt
import mitt from"mitt";
// 创建emitter
const emitter = mitt()
// 创建并暴露mitt
export default emitter
接收数据的组件中:绑定事件、同时在销毁前解绑事件:
import emitter from"@/utils/emitter";
import { onUnmounted } from"vue";
// 绑定事件
emitter.on('send-toy',(value)=>{
console.log('send-toy事件被触发',value)
})
onUnmounted(()=>{
// 解绑事件
emitter.off('send-toy')
})
【第三步】:提供数据的组件,在合适的时候触发事件
import emitter from"@/utils/emitter";
function sendToy(){
// 触发事件
emitter.emit('send-toy',toy.value)
9. Vue3 新组件
1. Teleport
是一种能够将我们的组件html结构移动到指定位置的技术。
<teleport to='body' >
<div class="modal" v-show="isShow">
<h2>我是一个弹窗</h2>
<p>我是弹窗中的一些内容</p>
<button @click="isShow = false">关闭弹窗</button>
</div>
</teleport>
2. Suspense
- 等待异步组件时渲染一些额外内容,让应用有更好的用户体验
- 使用步骤:
-
- 异步引入组件
- 使用
Suspense包裹组件,并配置好default与fallback
<template>
<div class="app">
<h3>我是App组件</h3>
<Suspense>
<template v-slot:default>
<Child/>
</template>
<template v-slot:fallback>
<h3>加载中.......</h3>
</template>
</Suspense>
</div>
</template>
10. 设置全局变量
const app = createApp({})
app.config.globalProperties.$http = () => {}
组件内 setup 读取值
import { getCurrentInstance, ComponentInternalInstance } from 'vue';
const { appContext } = <ComponentInternalInstance>getCurrentInstance()
console.log(appContext.config.globalProperties.$http);
11. 插槽slot
匿名插槽
1.在子组件放置一个插槽
<template>
<div>
<slot></slot>
</div>
</template>
父组件使用插槽
在父组件给这个插槽填充内容
<Dialog>
<template v-slot>
<div>2132</div>
</template>
</Dialog>
具名插槽
具名插槽其实就是给插槽取个名字。一个子组件可以放多个插槽,而且可以放在不同的地方,而父组件填充内容时,可以根据这个名字把内容填充到对应插槽中
<div>
<slot name="header"></slot>
<slot></slot>
<slot name="footer"></slot>
</div>
父组件使用需对应名称
<Dialog>
<template v-slot:header>
<div>1</div>
</template>
<template v-slot>
<div>2</div>
</template>
<template v-slot:footer>
<div>3</div>
</template>
</Dialog>
插槽简写
<Dialog>
<template #header>
<div>1</div>
</template>
<template #default>
<div>2</div>
</template>
<template #footer>
<div>3</div>
</template>
</Dialog>
作用域插槽
在子组件动态绑定参数 派发给父组件的slot去使用
<div>
<slot name="header"></slot>
<div>
<div v-for="item in 100">
<slot :data="item"></slot>
</div>
</div>
<slot name="footer"></slot>
</div>
通过结构方式取值
<Dialog>
<template #header>
<div>1</div>
</template>
<template #default="{ data }">
<div>{{ data }}</div>
</template>
<template #footer>
<div>3</div>
</template>
</Dialog>