Vue3(二):创建项目、组合式API、生命周期、内置组件
目录
一、项目创建
1、Vue CLI搭建项目
Vue CLI内置了一套经过验证的Webpack配置,涵盖了代码压缩、热更新、资源优化等功能,不需要我们手动配置复杂的构建流程,此外,它还内置了热模块替换(HMR),开发过程中修改代码可实时更新页面,无需手动刷新,提升了开发效率,同时,它也集成了ESLint、Prettier等代码规范工具,可以保持团队代码风格的一致性,减少代码审查的成本
要是使用Vue CLI搭建项目的话,需要我们的电脑已经安装了14版本以上的node.js
安装Vue CLI
npm install -g @vue/cli
//我们还可以使用
yarn global add @vue/cli
创建项目
vue create my-project//项目名称
选择第一项,按回车键就可以了

安装项目的其它依赖
如果我们需要安装项目的其它依赖,我们可以使用vue add命令后面指定具体的插件就可以了
//安装路由插件
vue add router
//安装状态管理插件
vue add pinia
//安装UI组件库
vue add element-plus
启动项目
![]()
在启动项目之前,我们要先cd ./按tab键再按回车键进入项目目录
![]()
然后我们在根据package.json文件中的scripts对象中的第一个属性来确定如何启动项目

这个我们可以使用npm run serve命令来启动项目
2、vite搭建项目
vite简介
极致的开发体验:vite采用了原生的ECMAScript模块加载机制,无需打包即可启动开发服务器,对于大型项目也能快速启动,它还能极速热更新HMR,修改代码后几乎能实时更新页面,能精准保留组件的状态,极大的提高了开发效率
配置轻量化:它默认集成了开发、构建所需的核心功能如热更新、代码压缩、资源优化等,不需要手动配置繁琐的构建工具链。它还有可扩展的插件体系,支持通过vite插件自定义构建流程,生态中有丰富的插件,如@vitejs/plugin-vue、unplugin-vue-components等等
技术支持友好:它原生与主流的前端框架深度集成,如vue3、react等等,能充分发挥前端框架的最新特性,此外,它还支持多种语言与语法,如ts、jsx、css预处理器等等,不需要额外配置
生产构建高效:它在生产环境中使用Rollup进行打包,能生成体积小、性能优的产物,还支持代码分割、懒加载等等,它还可以自动处理静态资源,如图片、字体的压缩,按需加载资源,提升应用程序的性能
要是使用vite搭建项目的话,需要我们的电脑已经安装了18.3版本以上的node.js
创建项目
npm create vite@latest my-project --template-vue
注:my-project是项目名称
安装项目依赖
生成的项目可能没有node_modules文件,我们需要额外安装依赖
npm i
安装项目的其它依赖
如果我们需要安装项目的其它依赖,我们可以使用npm i命令后面指定具体的插件就可以了
//安装路由插件
npm i router
//安装状态管理插件
npm i pinia
//安装UI组件库
npm i element-plus
启动项目
然后我们在根据package.json文件中的scripts对象中的第一个属性来确定如何启动项目

这个我们可以使用npm run dev命令来启动项目
注:npm仓库在国外,它的默认下载地址是国外原版地址,我们在安装依赖的时候如果有卡顿的情况,我们可以给npm设置淘宝镜像
npm config set registry https://registry.npmmirror.com
3、集成ts
如果我们在创建项目时,没有选择ts,可以在后续集成ts
安装依赖
npm i -D typescript vue-tsc @types/node @types/vue
创建ts配置文件
在项目的根目创建tsconfig.json文件
//tsconfig.json文件
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ES2020", "DOM"],
"skipLibCheck": true,
"noEmit": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
在项目的根目创建tsconfig.node.json文件
//tsconfig.node.json文件
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.js"]
}
将项目的入口文件main.js改为main.ts,并调整App.vue的引用为ts格式就可以了
注:如果我们在创建项目时,选择了ts,在tsconfig.app.json文件和tsconfig.node.json文件中可能会报错,因为ts规定,tsBuildInfoFile选项必须在启用incremental或composite编译选项时才能使用,我们在两个文件中的compilerOptions对象中添加属性incremental为true就可以了
4、项目目录简介
项目根目录(vite+ts)
node_modules:项目的核心模块,依赖包,由npm i自动生成
public:存放静态资源文件
.ico:页签的logo
.gitignore:声明一些在git上传时需要忽略的文件
package.json:项目的说明文件,有哪些依赖,依赖了哪个版本
package-lock.json:项目依赖的安装包,一些版本不会做一些限制,进行版本锁定
Readme.md:项目的说明文件
tsconfig.json:ts配置文件,定义ts编译规则,如目标ES版本、是否严格校验类型
tsconfig.node.json:针对node环境的ts配置文件,与业务代码的配置隔离,它会指定node相关的类型依赖,确保node环境下的代码能正确编译
tsconfig.app.json:是ts项目的应用级配置文件,用于指定当前应用编译ts代码的规则和目标环境
src目录
assets:项目的静态资源目录,存放图片、字体、全局样式等等
utils:工具函数目录,存放项目的通用工具,如axios的二次封装、防抖节流函数等等,提升代码的复用性
api:接口请求目录,统一管理后端接口,避免请求散落在组件中
types:ts类型定义目录,存放全局通用的ts接口
main.ts:项目的入口文件,初始化vue实例、挂载根组件、引入全局依赖
App.vue:项目的根组件,项目所有页面或组件的容器,通常包含路由出口
components:用于存放项目可复用的组件,如侧边导航、面包屑、弹窗等
views:页面组件目录,对应路由的页面级组件,如Home.vue、UseDetail.vue等
router:路由配置目录,通常含index.ts,定义所有页面的路由规则
store:状态管理目录,用于存放全局共享状态,如用户信息、购物车数据等等
二、组合式API
在vue2中,我们使用选项式API来组织代码,如data、methods、computed等,这种方式在组件简单时很直观,但是当组件变得复杂时,同一功能相关的代码会被拆分到不同的选项中,导致代码难以维护,逻辑混乱
组合式API是vue3引入的一种新的编写组件逻辑的方式,它可以让我们通过函数的形式来组织和复用代码逻辑,也就是说组合式API可以让我们将同一逻辑相关的代码,如响应式数据、计算属性、方法、生命周期等组织在一起,从而提高代码的可读性
1、setup
在vue3中,setup函数是组合式API的入口点,它可以用于定义组件的响应式数据、计算属性、方法和生命周期钩子等等,它是在组件实例创建之前执行的,此时的this为undefined,所以在组合式API中,使用属性或函数时,不需要用this调用
setup作为组合式API的入口,有两种写法,一种是setup函数,另一种是setup语法糖
setup函数
setup函数接受两个参数,props和context,并返回一个对象,该对象中所有的属性、函数和响应式数据都可以在模板中直接使用
props:它是组件接收的属性,是一个响应式对象,不能解构props,否则会失去响应性,如果需要解构,可以使用toRefs或toRef
父组件
//父组件
<template>
<MyChild :fatherData="fatherData" />
</template>
<script>
import MyChild from "./views/MyChild.vue";
export default {
setup() {
let fatherData = "我是父组件数据";
return { fatherData };
},
components: {
MyChild,
},
};
</script>
子组件
//子组件
<template>
<div>
<h1>我是子组件数据{{ fatherData }}</h1>
<button @click="fn">按钮</button>
</div>
</template>
<script>
export default {
//父组件传来的数据
props: ["fatherData"],
setup(props) {
function fn() {
console.log(props)
console.log(props.fatherData);
}
return { fn };
},
};
</script>
控制台输出

context:是setup函数的第二个参数,它是一个普通的对象,有三个属性,分别是attrs,emit,slots

emit:用于触发自定义事件
父组件
//父组件
<template>
<div>
<MyChild @myfn="handler"/>
</div>
</template>
<script>
import MyChild from "./views/MyChild.vue";
export default {
setup() {
function handler(data){
console.log("我是子组件传来的数据",data)
}
return { handler };
},
components: {
MyChild,
},
};
</script>
子组件
//子组件
<template>
<div>
<h1>我是子组件数据</h1>
<button @click="fn">按钮</button>
</div>
</template>
<script>
export default {
setup(context) {
let childData = "我是子组件数据111";
function fn() {
console.log(context);
context.emit("myfn", childData);
}
return { fn, childData };
},
};
</script>
注:setup必须return数据,它的第一个参数是props,第二个参数是上下文对象context
attrs:用来接收未在props中声明的属性
我们要想使用其它组件传来的数据,必须要通过props声明我们要接收的属性,才能在setup函数中使用。如果我们不想通过props来声明要接收的属性的话,我们可以使用context对象的atrrs属性
<script>
export default {
props: ["fatherData"],
setup(context) {
//msg其它组件传来的数据,这个数据未在props中声明
console.log(context.attrs.msg);
return { fn };
},
};
</script>
注:在props中声明过的属性,attrs是拿不到的
slots:包含所有传入的插槽内容
//父组件
<template>
<div>
<h1>{{msg}}</h1>
//子组件标签
<MyChild @myfn="handler" :fatherData="fatherData">
<template v-slot:before>
<h1>您好</h1>
</template>
<template v-slot:after>
<h1>哈哈哈</h1>
</template>
</MyChild>
</div>
</template>
//子组件
<template>
<div>
<slot name="before"></slot>
<h1>我是子组件数据</h1>
<slot name="after"></slot>
<button @click="fn">按钮</button>
</div>
</template>
<script>
export default {
setup(props,context) {
function fn() {
console.log(context.slots.after())
}
return { fn };
},
};
</script>
注:在工作中,我们不使用jsx语法的话,slots属性几乎不用,我们了解一下即可
2、setup语法糖
在vue3.2及以上版本中,我们可以直接使用setup语法糖,来创建组合式API的入口点,它是在单文件组件中使用组合式API编译时的语法糖,我们无需手动定义setup函数,代码直接写在<script setup>标签内,让代码更加的简洁
1、使用setup语法糖
在setup语法糖中,可以直接使用ref、reactive、watch等组合式API,同时定义的变量、函数会自动暴露给模板,直接使用就可以了,不需要通过return暴露给模板
<template>
<div>
<p>{{ count }}</p>
<p>{{ obj }}</p>
<p>{{ add }}</p>
<button @click="fn">按钮</button>
<button @click="fn1">改对象</button>
</div>
</template>
<script setup>
import { ref, reactive, watch, computed, watchEffect } from "vue";
const count = ref(1);
const obj = reactive({
name: "小明",
age: 18,
hobby: ["跳舞", "唱歌"],
});
//函数
function fn1() {
obj.hobby = [1, 2];
}
function fn() {
count.value++;
}
//计算属性
const add = computed(() => count.value + 1);
//侦听器
watch(count, (n, o) => {
console.log(n, o);
});
watchEffect(() => {
console.log("11", obj.hobby);
});
</script>
在setup语法糖中,可以直接导入生命周期钩子并在setup中使用
<script setup>
import { onMounted, onUnmounted } from "vue";
onMounted(() => {
console.log("组件挂载完毕");
});
onUnmounted(() => {
console.log("组件卸载了");
});
</script>
Vue3组件通信,主要有父传子、子传父、跨层级三种场景,每种场景的传值方案有多种,这里主要讲解在setup语法糖中,不同场景最常用的传值方式
在setup语法糖中,要想实现父子组件通信的话,可以使用defineProps和defineEmits来替换props和emits
父传子
父组件
//Father.vue
<template>
<div>
<MyChild :msg="msg" :title="title" />
</div>
</template>
<script setup>
import { ref } from "vue";
//子组件不需要注册,直接使用就可以了
import MyChild from "./views/MyChild.vue";
const msg = ref("我是父组件数据");
const title = ref("哈哈");
</script>
子组件
//Child.vue
<template>
<div>{{ props.msg }}:{{ props.title }}</div>
</template>
<script setup>
const props = defineProps(["msg", "title"]);
</script>
子传父
子组件
//Child.vue
<template>
<div>
<button @click="fn">按钮</button>
</div>
</template>
<script setup>
import { ref } from "vue";
const childData = ref("我是子组件数据");
const emit = defineEmits();
function fn() {
emit("myFn", childData);
}
</script>
父组件
//Father.vue
<template>
<div>
<p>{{ fatherData }}</p>
<MyChild @myFn="handler" />
</div>
</template>
<script setup>
import { ref } from "vue";
import MyChild from "./views/MyChild.vue";
const fatherData = ref("我是爸爸");
function handler(data) {
fatherData.value = data;
}
</script>
在setup语法糖中要想实现多级组件通信的话,我们可以使用provide和inject
祖先组件
//Ancestor.vue
<template>
<div>
<MyChild />
</div>
</template>
<script setup>
import { ref, provide } from "vue";
import MyChild from "./views/MyChild.vue";
const msg = ref("我是组先组件");
function changeMsg() {
msg.value = "我将数据传给了子组件";
}
provide("msg", msg);
provide("changeMsg", changeMsg);
</script>
后代组件
//GrandSon.vue
<template>
<div>
<p>{{ msg }}</p>
<button @click="fn">按钮</button>
</div>
</template>
<script setup>
import { ref,inject } from "vue";
const msg=inject("msg")
const changeMsg=inject("changeMsg")
function fn() {
changeMsg()
}
组件不需要注册,直接使用,动态组件使用的时候,is前必须加冒号,变成动态属性
<template>
<div>
<component :is="currentHome" />
</div>
</template>
<script setup>
import { ref } from "vue";
import Home from "./views/Home.vue";
//当前要渲染的组件,初始值为Home
const currentHome = ref(Home);
</script>
2、内置编译器宏
Vue3的内置编译器宏是仅在<script setup>语法中生效的编译器级工具,编译时会被转为vue运行时API,不需要手动导入就可以使用,可以简化组件开发的流程,如通信、配置、dom操作等等,可以让代码逻辑更简洁、类型更安全
1、组件通信类
defineProps和defineEmits是<script setup>语法糖中使用的编译器宏,用于定义组件的props和emits而withDefaults是一个与defineProps配合使用的宏,用于为props提供默认值
defineProps
defineProps用于在<script setup>中声明子组件接收父组件的props。它接受一个参数,可以是对象形式或数组形式
defineProps接受对象形式的参数
传入对象,为每个Props配置类型、必填性、默认值、自定义校验等规则,增强组件的健壮性,适合生产环境
<script setup>
//defineProps是vue3默认全局导入的,不需要引入
const props = defineProps({
phone: {
type: string, //类型
required: true, //是否必传(必传时父组件必须传递,否则报警告)
default: "13783763340", //默认值,仅required为false时生效
validator: (value) => {
//自定义校验函数:返回true表示校验通过,false则报警告
return /^1[3-9]\d{9}$/.test(value); //校验手机格式
},
},
});
</script>
defineProps接受数组形式的props
数组形式只指定props的名称,不指定类型和其它配置项
<script setup>
//defineProps是vue3默认全局导入的,不需要引入
const props = defineProps(["title", "msg"]);
</script>
在ts中使用defineProps
在ts中,我们可以使用基于类型的声明,可以利用ts类型的检查
<script setup lang="ts">
//defineProps是vue3默认全局导入的,不需要引入
interface Props{
title:string,
phone:number
}
const props = defineProps<Props>();
</script>
defineEmits
defineEmits专门用于声明组件可触发的自定义事件,并返回用于触发事件的emit函数
defineEmits可以接受数组形式的数据,也可以接受对象形式的数据。在做项目时,接受对象形式的数据很少见,这里就不介绍了
defineEmits接受数组形式的数据
仅声明事件名称,不用做参数校验,可以提高开发效率
<script setup>
import { ref } from "vue";
//defineEmits是vue3默认全局导入的,不需要引入
const count = ref("我是子组件数据");
const emit = defineEmits(["change", "submit"]);
//触发事件:第一个参数是事件名,后面的参数是要传递给父组件的数据
const handleClick = () => {
//子组件传给父组件的数据
emit("change", count);
emit("submit", { username: "李四", password: "123456" });
};
</script>
在ts中使用defineEmits
用ts函数类型声明事件的参数类型和返回值,会获得完整的类型提示,避免参数类型错误
<script setup lang="ts">
import { ref } from "vue";
//defineEmits是vue3默认全局导入的,不需要引入
const emit = defineEmits<{
//事件名,参数类型,返回值
(e: "change", formData: { username: string; password: string }): void;
}>();
//触发事件,TS会自动校验参数类型,传错类型会报错
function fn() {
emit("change", {
username: "admin",
password: "123456",
});
}
</script>
withDefaults
withDefaults专门为defineProps<Type>()设置的默认值,弥补其无法直接写default选项的缺陷,仅在ts项目中使用
<script setup lang="ts">
// 步骤1:用 TS 类型声明 Props(含可选属性)
interface Props {
name?: string // 可选属性,需要默认值
age?: number // 可选属性,需要默认值
hobbies?: string[] // 可选引用类型,需要默认值
}
// 步骤2:用 withDefaults 包裹 defineProps,设置默认值
//第二个参数为父组件传入的数据
const props = withDefaults(defineProps<Props>(), {
// 基础类型默认值:直接赋值
name: '匿名用户',
age: 18,
// 引用类型(数组/对象)默认值:必须用函数返回(避免共享引用)
hobbies: () => ['看剧', '运动']
})
</script>
defineModel
defineModel是vue3.4推出的简化父子组件双向绑定的宏,能替代传统的v-model实现,让代码更加的简洁
基本用法
子组件
//child.vue
<template>
<div>
<input type="text" v-model="model">
</div>
</template>
<script setup>
const model=defineModel()
</script>
父组件使用
<template>
<div>
<MyChild v-model="message"/>
</div>
</template>
<script setup>
import MyChild from "./views/MyChild.vue";
import { ref } from "vue";
const message=ref("hello")
</script>
具名模型
子组件
<template>
<div>
<input type="text" v-model="title">
</div>
</template>
<script setup>
const title=defineModel("title")
</script>
父组件使用
<template>
<div>
<MyChild v-model:title="title"/>
</div>
</template>
<script setup>
import MyChild from "./views/MyChild.vue";
import { ref } from "vue";
const title=ref("hello111")
</script>
多个v-model
子组件
<template>
<div>
<input type="text" v-model="modelValue" />
<input type="text" v-model="title" />
</div>
</template>
<script setup>
const modelValue = defineModel();
const title = defineModel("title");
</script>
父组件
<template>
<div>
<MyChild v-model:title="title" v-model="message"/>
</div>
</template>
<script setup>
import MyChild from "./views/MyChild.vue";
import { ref } from "vue";
const title=ref("hello111")
const message=ref("哈哈")
</script>
修饰符处理
内置修饰符:defineModel可以自动处理内置修饰符,如.trim、.number等等
子组件
<template>
<div>
<input type="text" v-model="model" />
</div>
</template>
<script setup>
const model = defineModel({
set(value) {
//处理修饰符,如.trim
if (typeof value === "string") {
return value.trim();
}
return value;
},
});
</script>
自定义修饰符
子组件
<template>
<div>
<input type="text" v-model="model1" />
</div>
</template>
<script setup>
const [model1, modify] = defineModel({
// 这里定义一个自定义修饰符,capitalize
//在父组件中使用v-model.capitalize
set(value) {
if (modify.capitalize && typeof value === "string") {
return value.charAt(0).toUpperCase() + value.slice(1);
}
return value;
},
});
</script>
在ts中使用defineModel
<script setup lang="ts">
const model=defineModel<number>()//指定为数字类型
const title=defineModel<string>("title")//具名模型,字符串类型
</script>
深层响应式
当模型是一个对象时,默认是深层的响应式
<template>
<div>
<input type="text" v-model="formData.name" />
<input type="text" v-model.number="formData.age" />
</div>
</template>
<script setup>
const formData=defineModel("formData",{default:()=>({name:"李四",age:18})})
</script>
注:defineModel只能在setup语法糖中使用。如果是使用选项式API,使用props和emit
2、组件配置类
defineOptions和defineExpose是用于组件配置的编译器宏,可以用于配置组件的基础信息,如名称、属性透传规则或控制组件对外暴露的内容
defineExpose
defineExpose是setup语法糖中用于暴露组件内部属性或方法的函数,让父组件能通过模板访问子组件非props中的数据
在子组件的<script setup>里,调用defineExpose并传入要暴露的对象
<script setup>
import { ref } from "vue";
const count = ref(0);
const add = () => {
count.value++;
};
//暴露count和add函数给父组件
defineExpose({ count, add });
</script>
父组件通过ref获取子组件实例,直接调用暴露的属性和方法
<template>
<div>
<MyChild ref="childRef" />
<button @click="fn">按钮</button>
</div>
</template>
<script setup>
import MyChild from "./views/MyChild.vue";
import { ref } from "vue";
const childRef = ref(null);
//调用子组件暴露的方法
const fn = () => {
childRef.value.add();
console.log("子组件的数据", childRef.value.count);
};
</script>
注:在setup语法糖中,组件内部的成员默认是封闭的,需要显示暴露才能被外部访问。且暴露的属性会失去响应式,父组件访问的是原始值的代理,需要通过.value操作
defineOptions
defineOptions是vue3.3中setup语法糖的专属宏,用于直接在脚本内部定义组件选项,如name、props、emits等等,不需要额外编写独立的<script>标签
defineOptions主要是弥补早期<script setup>无法直接声明组件部分选项的缺陷,让代码更聚合
定义组件名
用于递归组件、keep-alive缓存或调试时标识组件
<script setup>
defineOptions({
name:"childComponent"//父组件或开发者工具中可以识别该名称
})
</script>
声明props与emits
可以替代defineProps和defineEmits,适合需要更详细配置的场景
<script setup>
defineOptions({
props:{
title:{
type:string,
required:true,
default:"哈哈"
}
},
emits:["submit"]
})
</script>
配置其它选项
defineOptions支持inheritAttrs、components等传统组件选项,如关闭组件继承
<script setup>
defineOptions({
inheritAttrs:false//阻止父组件非props属性自动挂载到子组件根元素上
})
</script>
注:defineOptions的优先级要低于defineProps和defineEmits,如果两者同时声明props,以defineProps为准
3、插槽操作类
useAttrs和useSlots这类宏用于获取父组件传递的非props内容,如属性、插槽,支持属性透传、插槽内容的访问
属性透传
属性透传就是当父组件传递属性给子组件时,如果子组件没有在props中声明接收,这些属性会自动传递给子组件的根元素
单根节点组件属性透传
如果子组件是单根节点,那么透传的属性会自动添加到根元素上
父组件
<template>
<div>
<MyChild id="title" class="box" />
</div>
</template>
子组件
<template>
<div id="child" class="shop">
<p>哈哈</p>
</div>
</template>
渲染结果

注:在vue3中,由于class和style会被特殊处理,如果在子组件中设置了class和style,它们会与父组件传递的合并,但如果是手动绑定$attrs.class和$attrs.style,会覆盖子组件根元素上原有的class和style
多根节点组件透传
如果子组件是多根节点,则不会自动透传,控制台会给出警告,此时,需要手动指定透传的位置
子组件
<template>
<div>
<p>哈哈</p>
</div>
<div>
<p>123</p>
</div>
</template>
此时,父组件传递的属性不会透传到任何一个根元素上,需要子组件使用v-bind显示绑定,指定将属性加给哪个根节点
//Child.vue子组件
<template>
<div v-bind="$attrs">
<p>哈哈</p>
</div>
<div>
<p>123</p>
</div>
</template>
此时,父组件传透的属性就会绑定到第一个根节点上
在vue3中$attrs包含了父组件传递的所有非props属性,在子组件中,可以通过$attrs来访问所有透传属性,包括class、style和事件监听
父组件
<template>
<div>
<MyChild id="title" class="box" style="color:red" @click="handler" />
</div>
</template>
子组件
//Child.vue
<template>
<div :class="$attrs.class" :style="$attrs.style">
<p>哈哈</p>
</div>
<div>
<p>123</p>
</div>
</template>
注:如果父组件传了事件监听器,在子组件中,$attrs会包含{onClick:handler},但如果子组件声明了emits,则事件监听器不会出现在$attrs中
如果我们不希望子组件的根元素继承父组件的属性,可以在子组件中进行设置
<script>
export default{
inheritAttrs:false
}
</script>
useAttrs
useAttrs是组合式函数,在组合式API中,可以使用useAttrs来获取父组件传递的所有非props属性和事件,如class、style等等,并返回一个响应式的对象,可以访问父组件传递的额外属性
父组件
<template>
<div>
<MyChild id="title" class="box" style="color:red" @click="handler" />
</div>
</template>
子组件
<script setup>
//useAttrs需要引入,否则会报错
import { useAttrs } from "vue";
const attrs=useAttrs()
console.log(attrs.id)
</script>
useSlots
useSlots是vue3组合式API中用于获取组件接受的所有插槽内容的钩子,它返回一个包含所有插槽内容的对象,可以在setup语法糖中访问和操作插槽
useSlots返回的对象中,键是插槽名,默认插槽为default,值是对应的插槽内容,可以是一个或多个虚拟dom节点
useSlots返回的对象本身不具有响应式,但插槽内容的变化会自动反映到组件渲染中,因为插槽内容是由父组件控制的。可以直接通过slots来访问具名插槽,作用域插槽需要调用并传递props来获取内容
父组件
//Parent.vue
<template>
<div>
<MyChild>
<!-- 默认插槽 -->
<p>123</p>
<!-- 具名插槽 -->
<template #title>
<h1>我是具名插槽</h1>
</template>
<!-- 作用域插槽 -->
<template #item="slotProps">
<h2>{{ slotProps.text }}</h2>
</template>
</MyChild>
</div>
</template>
子组件
//Child.vue
<template>
<div>
<!-- 默认插槽 -->
<slot></slot>
<!-- 具名插槽 -->
<slot name="title"></slot>
<!-- 作用域插槽 -->
<slot name="item" :text="content"></slot>
</div>
</template>
<script setup>
import { useSlots, ref } from "vue";
const slots = useSlots();
const content = ref(null);
//检查默认插槽是否存在,存在则返回默认插槽的虚拟dom节点
console.log(slots.default);
//访问具名插槽
console.log(slots.title); //返回具名插槽的内容
//调用作用域插槽并传递props
if (slots.item) {
//传递props给作用域插槽,并获取渲染内容
const scopedTitle = slots.item({ text: "我是作用域插槽" });
content.value = scopedTitle[0].children;
}
</script>
4、依赖注入类
provide和inject是一对用于跨代组件通信的API,不属于编译器宏,但它们常与宏搭配使用
provide:在祖先组件中,我们常使用provide函数来给后代组件提供数据。provide接受两个参数,第一个参数是提供的键名,第二个参数是提供的数据
祖先组件
<script setup>
import MyChild from "./views/MyChild.vue";
import { provide,ref } from "vue";
const count = ref(0);
provide("count1",count)
</script>
inject:在后代组件中,使用inject函数来注入祖先组件提供的数据。inject接受两个参数,第一个参数是要注入的键名,第二个参数是默认值,可传,也可不传
后代组件
<template>
<div>
<p>{{ message }}</p>
</div>
</template>
<script setup>
import { inject } from "vue";
const message = inject("count1");
</script>
provide和inject提供和注入的数据可以是响应式数据,也可以是非响应式数据,当祖先组件响应式数据发生变化时,后代组件会自动更新
祖先组件
<script setup>
import MyChild from "./views/MyChild.vue";
import { provide, ref } from "vue";
const count = ref(0);
provide("count1", count);
setInterval(() => {
count.value++;
}, 1000);
</script>
后代组件
<template>
<div>
<p>{{ message }}</p>
</div>
</template>
<script setup>
import { inject } from "vue";
//message每隔一秒会发生变化
const message = inject("count1");
</script>
除了提供和注入数据外,provide和inject也可以提供和注入方法,让后代组件可以调用祖先组件的方法
祖先组件
<script setup>
import MyChild from "./views/MyChild.vue";
import { provide, ref } from "vue";
const count = ref(0);
function add() {
count.value++;
}
provide("add", add);
</script>
后代组件
<template>
<div>
<button @click="add">按钮</button>
</div>
</template>
<script setup>
import { inject } from "vue";
const message = inject("add");
</script>
provide提供的数据,在后代组件中是可以修改的,为了保证数据的单向流,当我们希望祖先组件提供的数据不被后代组件直接修改时,可以使用readonly来包装提供的响应式数据
祖先组件
<script setup>
import MyChild from "./views/MyChild.vue";
import { provide,ref,readonly } from "vue";
const count = ref(0);
function add() {
count.value++;
}
provide("count1",readonly(count))
provide("add",add)
</script>
后代组件
<template>
<div>
<p>{{message}}</p>
<button @click="add1">按钮</button>
</div>
</template>
<script setup>
import { inject } from "vue";
const message = inject("count1");
const add1=inject("add")
</script>
注:此时的count是不可以修改的,如果后代组件要修改count的话,会得到一个警告
为了确保数据流向清晰,最好在祖先组件中提供专门的方法来修改数据,而不是让后代组件直接修改响应式数据
祖先组件
<script setup>
import MyChild from "./views/MyChild.vue";
import { provide, ref } from "vue";
const count = ref(0);
//后代组件按照祖先组件的意图去修改数据
const setCount = (newCount) => {
count.value = newCount;
};
provide("count",count)
provide("setCount", setCount);
</script>
后代组件
<template>
<div>
{{ count }}
<button @click="updateCount">按钮</button>
</div>
</template>
<script setup>
import { inject } from "vue";
const count = inject("count");
const setCount = inject("setCount");
const updateCount = () => {
setCount(100);
};
</script>
注:provide和inject是Vue中用于跨组件传递数据的高级特性,它们可以解决深层嵌套组件的props传递的问题,但在使用时,要注意保持响应性,和数据流的单向性,通常与readonly和提供修改函数来配合使用
3、响应式API
1、ref
ref函数用于创建一个响应式的数据引用,它可以包装任意类型的值,包括值类型和引用数据类型
![]()
ref函数中的value属性指向的是实际数据
ref处理基础数据类型
<template>
<div>
<p>{{ message }}</p>
<p>{{ num }}</p>
<button @click="changeFn">按钮1</button>
</div>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
//字符串
let message=ref("我是响应式数据")
//数字
let num=ref(0)
function changeFn(){
console.log("222",message)
message.value="您好"
num.value=10
}
return {changeFn,message,num };
},
};
</script>
注:ref在模板中使用会自动解包,不需要使用.value,在js中,需要通过.value来访问和修改ref值,但ref作为响应式对象的属性会自动解包,不需要使用.value
<script>
import { reactive, ref } from 'vue';
export default {
setup(props,context) {
let obj1=reactive({name:"小芳",age:20,num:ref(10)})
function changeFn(){
//此时的ref是响应式对象的属性,会自动解包,不需要使用.value
obj1.num=20
}
return { changeFn,obj1 };
},
};
</script>
ref处理引用数据类型
ref也可以定义引用数据类型,内部通过reactive转为代理对象,它是深层次的响应式,就算更改嵌套数组或对象依旧会触发视图更新
<script>
import { ref } from 'vue';
export default {
setup(props,context) {
// 数组
let arr=ref([1,2,3,4])
let arr1=ref([1,2,3,4,[8,9]])
// 对象
let obj=ref({name:"小明",age:19,hobby:["跳舞","唱歌"]})
function changeFn(){
arr.value[0]=100
arr1.value[4][0]=200
obj.value.name="小红"
obj.value.hobby[0]="看电视"
}
return { changeFn,arr,arr1,obj };
},
};
</script>
2、reactive
reactive函数用于创建一个响应式对象,返回一个对象的响应式代理,它只能接收引用数据类型,不能直接处理基本数据类型
reactive的核心原理通过Proxy代理目标对象,拦截对象的读写、删除等操作,当数据变化时自动触发依赖它的组件重新渲染,实现数据更改,视图更新
<script>
import { reactive } from 'vue';
export default {
setup(props,context) {
let obj1=reactive({name:"小芳",age:20})
//添加对象属性,也是响应式的
let like={type:"sport",name:"篮球"}
function changeFn(){
obj1.name="李四"
obj1.like=like
}
return { changeFn,obj1 };
},
};
</script>
reactive的局限性
有限的值类型:reactive只能用于引用数据类型,如对象、数组和Map、set这样的类型集合,它不能处理引用数据类型
不能替换整个对象:由于Vue的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用,如果替换了响应式对象,会与第一个引用的响应性连接将丢失
也就是说reactive是将对象变成了一个响应式对象,ref是把数据包裹在一个响应式对象中,所以reactive整体赋值会丢失响应式,而ref不会
<script>
import { reactive } from 'vue';
export default {
setup(props,context) {
let obj1=reactive({name:"小芳",age:20})
function changeFn(){
//这种写法是错误的,整体赋值,响应式会丢失,用ref,响应式不会丢失
obj1={name:"李四",age:30}
}
return { changeFn,obj1 };
},
};
</script>
对解构赋值操作不友好:当我们将响应式对象中的属性进行解构时,或将该属性传递给函数时,响应式会丢失
<script>
import { reactive } from 'vue';
export default {
setup(props,context) {
let obj1=reactive({name:"小芳",age:20})
function changeFn(){
//这种写法是错误的,相当于我们重新声明了一个变量,不是响应式的
let {name}=obj1
name="王五"
}
return { changeFn,obj1 };
},
};
</script>
注:ref对解构赋值操作也不友好
3、computed
Vue3计算属性是基于响应式依赖进行缓存的衍生值,它可以将复杂的响应式数据计算逻辑封装起来,可以提高应用程序的性能和代码的复用性
在setup函数中,computed接受一个getter函数,并根据getter的返回值返回一个只读的的响应式ref对象,访问时需要加.value
<script>
import { ref, computed } from "vue";
export default {
setup() {
const count = ref(1);
//add是只读的,不可以修改
const add = computed(() => count.value + 1);
console.log(add.value);
}
};
</script>
computed可以接受一个对象,使用具有get和set函数的对象来创建可写的ref对象
<script>
import { ref, computed } from "vue";
export default {
setup() {
const count = ref(1);
//add可以修改
const add = computed({
get: () => count.value + 1,
//更改的是依赖项的数据,而不是计算属性本身
set: (val) => {
count.value = val - 1;//val是add的值
},
});
//修改了add才能触发set函数,不修改add不会触发set函数
add.value=1
console.log(count.value)//0
}
};
</script>
注:更改计算属性在做项目时,几乎不用,了解即可
4、watch和watchEffect
watch和watchEffect都是vue3中响应式侦听器,它们的主要区别就是watch需要指定明确的监听源,watchEffect自动追踪函数内用到的响应式数据
watch
watch需要手动指定要监听的响应式数据和一个回调函数,用于在响应式数据发生变化时执行回调函数,它可以监听ref或reactive定义的响应式数据、计算属性、getter函数
监听ref定义的单个基本类型数据
<template>
<div>
<p>{{ count }}</p>
<button @click="fn">按钮</button>
</div>
</template>
<script>
import { ref, watch } from "vue";
export default {
setup() {
const count=ref(1)
function fn(){
count.value++
}
//当点击按钮时,才会触发watch中的回调函数
//第一个参数是要监听的变量名,不需要加.value
watch(count,(newValue,oldValue)=>{
console.log(oldValue,newValue)
})
return{ count, fn }
}
};
</script>
<style lang="scss" scoped>
</style>
监听ref定义的多个基本类型数据
watch监听多个数据的话,要传入一个数组
<template>
<div>
<p>{{ count }}:{{ num }}</p>
<button @click="fn">按钮</button>
</div>
</template>
<script>
import { ref, watch } from "vue";
export default {
setup() {
const count = ref(1);
const num = ref(0);
function fn() {
count.value++;
num.value++;
}
watch([count, num], ([newCount, newNum], [oldCount, oldNum]) => {
console.log(oldCount, newCount);
console.log(newNum, oldNum);
});
return { count, fn, num };
}
};
</script>
watch监听ref定义的引用数据类型数据
watch监听ref定义的引用数据类型,默认是浅层监听,要想深度监听,可以配置watch的第三个参数{deep:true}
<script>
import { ref, watch } from "vue";
export default {
setup() {
let obj = ref({
name: "小红",
age: 20,
});
function fn() {
obj.value.name = "小明";
}
watch(
obj,
(n, o) => {
//n和o的值相同,它们指向同一个对象
console.log(n, o);
},
{ deep: true }
);
return { obj, fn };
}
};
</script>
watch监听reactive定义的整个对象数据或数组
<template>
<div>
<p>{{ obj }}</p>
<button @click="fn">按钮</button>
</div>
</template>
<script>
import { reactive, watch } from "vue";
export default {
setup() {
const obj = reactive({
name: "小红",
age: 19,
hobby: ["跳舞", "看电影"],
});
function fn() {
obj.name= "小明";
}
watch(obj, (newName,oldName) => {
console.log(newName,oldName);//newName:小明,oldName:小明
});
return { obj, fn };
}
};
</script>
watch监听reactive定义的整个对象或数组,默认是深度监听,获取不到旧数据,因为reactive返回的是Proxy代理对象,其内部属性的引用地址不会发生改变,watch无法通过对比引用差异来捕获旧值,所以默认情况下旧值会和新值指向同一个对象
![]()
监听监听reactive定义对象的某个属性
watch监听对象定义的某个属性,默认是浅层监听,会接受一个getter函数,我们可以手动开启深度监听
<template>
<div>
<p>{{ obj.hobby.name }}</p>
<button @click="fn">按钮</button>
</div>
</template>
<script>
import { reactive,watch } from "vue";
export default {
setup() {
const obj = reactive({
name: "小明",
hobby: {
id: 1,
name: "dance",
},
});
watch(
() => obj.hobby.name,
(newObj, oldObj) => {
//newObj和oldObj是同一个对象,因为它们是同一个响应式对象
console.log(newObj, oldObj);
}
);
function fn() {
obj.hobby.name = "小红";
}
return { obj, fn };
}
};
</script>
watch监听计算属性
<script>
import { computed, reactive, ref, watch } from "vue";
export default {
setup() {
let firstName = ref("李");
let lastName = ref("四");
const fullName = computed(() => ({
firstName: firstName.value,
lastName: lastName.value,
}));
function fn() {
firstName.value = "张";
}
watch(fullName, () => {
console.log("名字发生变化了");
},{ immediate:true});//watch的第三个配置参数也可以是immediate,第一次就会执行
return { fullName, fn };
}
};
</script>
watchEffect
watchEffect无需指定监听源,组件初始化时会立即执行一次传入的回调函数,响应式的追踪其依赖,会在其依赖变更时重新运行。简单理解就是函数内用到哪个响应式数据就自动监听哪个,不需要手动指定数据,回调函数内也没有新旧值参数
<template>
<div>
<p>{{ count }}</p>
<button @click="fn">按钮</button>
</div>
</template>
<script>
import { ref, watchEffect } from "vue";
export default {
setup() {
const count = ref(1);
function fn() {
count.value++;
}
//自动追踪count,初始化时执行一次,count变更时再执行
watchEffect(() => {
console.log(count.value);//立即打印1,点击一次按钮后会打印2
});
return { count, fn };
}
};
</script>
watchEffect返回值是一个停止监听的函数,当我们调用这个函数时,watchEffect就会停止监听
<script>
import { ref, watchEffect } from "vue";
export default {
setup() {
const count = ref(1);
function fn() {
count.value++;
}
//自动追踪count,初始化时执行一次,count变更时再执行
const stop = watchEffect(() => {
console.log(count.value);//立即打印1,点击一次按钮后会打印2
});
stop()//watchEffect不会监听了
return { count, fn };
}
};
</script>
在watchEffect的回调函数中可以返回清理函数,可以自动清理副作用。如果多次触发,会自动清理上一次未完成的副作用,如取消上一次的接口请求
<script>
import { ref, watchEffect } from "vue";
export default {
setup() {
const count = ref(1);
function fn() {
count.value++;
}
//自动追踪count,初始化时执行一次,count变更时再执行
watchEffect((onInvalidate) => {
console.log(count.value);
//onInvalidate默认在每次回调函数重新执行之前触发,写在watchEffect内部
onInvalidate(()=>{
console.log("触发了")
})
});
return { count, fn };
}
};
</script>
watchEffect监听嵌套对象或数组
watchEffect如果监听嵌套对象或数组,必须要整体替换才可以被监听到,否则不可以
<script>
import { reactive, watchEffect } from "vue";
export default {
setup() {
const obj = reactive({
name: "小明",
age: 18,
hobby: ["跳舞", "唱歌"],
});
function fn() {
obj.hobby = [1, 2];
}
watchEffect(() => {
console.log("obj变化了", obj.hobby);
});
return { obj, fn };
}
};
</script>
5、性能优化的API
在vue3中,shallowRef和shallowReactive是用于性能优化的响应式API,与ref和reactive类似,但shallowRef和shallowReactive只对对象的第一层属性进行响应式处理,嵌套对象不会转为响应式
shallowRef
shallowRef可以创建一个响应式的引用,但它只对.value的变化进行响应,不会对.value内部属性的变化进行响应
也就是说,如果shallowRef的值是一个对象,那么修改这个对象的属性是不会触发响应式更新的,只有整个.value被替换时才会触发
<script>
import { shallowRef } from 'vue';
export default {
setup(props,context) {
let obj1=shallowRef({name:"小芳",age:20})
function changeFn(){
//整体替换,页面才会触发响应式更新
obj1.value={name:"李四",age:35}
}
return { changeFn,obj1 };
},
};
</script>
注:shallowRef是浅层响应式,必须要整体替换,才能触发响应式更新
customRef
customRef是vue3提供的一个专门用来创建一个自定义ref的函数,我们可以显式的控制其依赖追踪和更新触发。使用customRef,我们可以实现各种复杂的需求,如防抖、持久化、权限控制等等
customRef接受一个工厂函数作为参数,这个工厂函数接收两个函数track和trigger作为参数,并返回一个带有get和set方法的对象
在自定义ref内部,我们可以在get方法中调用track来追踪依赖,在set方法中调用trigger来触发更新
<template>
<div>
<p>{{ count }}</p>
<button @click="fn">按钮</button>
</div>
</template>
<script>
import { customRef } from "vue";
export default {
setup() {
function _ref(value) {
let timer;
return customRef((track, trigger) => {
return {
get() {
track(); //用来追踪数据变化
return value;
},
set(val) {
clearTimeout(timer);
timer = setTimeout(() => {
value = val;
trigger(); //延迟后触发更新
}, 1000);
},
};
});
}
let count = _ref(1);
function fn() {
count.value++;
}
return { count, fn };
},
};
</script>
triggerRef
triggerRef与shallowRef
shallowRef是浅层响应式,它只对.value的变化进行响应,不会对.value内部属性的值进行响应,所以当我们直接修改shallowRef的内部属性时,不会触发组件的重新渲染或watch效应
triggerRef主要用于强制触发一个浅层ref的副作用,我们可以通过手动调用triggerRef来通知shallowRef的值已经发生变化了,从而实现对shallowRef内部的值进行深度更改,并触发更新
<script>
import { shallowRef,triggerRef } from 'vue';
export default {
setup(props,context) {
let obj1=shallowRef({name:"小芳",age:20})
function changeFn(){
//不会触发响应式更新
obj1.value.name="李四"
//手动调用triggerRef可以触发响应式更新
triggerRef(obj1)
}
return { changeFn,obj1 };
},
};
</script>
在某些情况下,我们可能会对shallowRef进行多次修改,但不用每次修改都触发更新,我们可以在进行一系列修改后,一次性调用triggerRef来减少不必要的更新
<script>
import { shallowRef,triggerRef } from 'vue';
export default {
setup(props,context) {
let obj1=shallowRef({name:"小芳",age:20})
function changeFn(){
//进行了多次修改
obj1.value.name="李四"
obj1.value.age=30
//一次性触发更新
triggerRef(obj1)
}
return { changeFn,obj1 };
},
};
</script>
triggerRef与customRef
在customRef中,我们会在setter中调用trigger来通知更新。如果我们创建了一个自定义ref,需要从外部触发更新,如异步操作完成后,可以使用triggerRef
<script>
import { customRef, triggerRef } from "vue";
export default {
setup() {
const count=customRef((track, trigger) => {
let value=0
return {
get() {
track(); //用来追踪数据变化
return value;
},
set(val) {
value = val;
//如果我们想手动控制触发时机,这里不需要调用trigger函数
},
};
});
//修改值,不会触发更新,因为set函数中没有调用trigger
count.value=10
//使用triggerRef手动触发更新
triggerRef(count)
},
};
</script>
注:triggerRef是一个高级的API,主要用于处理浅层ref的更新触发,在大多数情况下,我们不需要使用它,但在性能优化或特殊场景下,它需要与shallowRef或customRef配合使用才有意义。由于ref是深层次响应式,不需要使用triggerRef。Vue的响应式系统通常会自动处理更新,所以只有在明确需要手动控制处理更新时才使用triggerRef
shallowReactive
shallowReactive也可以创建一个响应式对象,它只对对象的第一层属性进行响应式处理。也就是说,如果某个属性是对象,那么修改这个嵌套对象的属性是不会触发响应式更新的
<script>
import { shallowReactive } from 'vue';
export default {
setup(props,context) {
let obj1=shallowReactive({name:"小芳",age:20,hobby:["跳舞","唱歌"]})
function changeFn(){
//会触发响应式更新
obj1.name="王五"
//修改嵌套的属性,不会触发响应式更新
obj1.hobby[0]="看电影"
}
return { changeFn,obj1 };
},
};
</script>
注:我们在做项目时,还是首选ref和reactive。当我们的项目某些数据很复杂,有多层数据嵌套,而我们不需要进行深层次监听时,可以选择shallowRef或shallowReactive来进行浅层监听,以优化性能
6、只读响应式的API
readonly和shallowReadonly是vue3中用于创建只读响应式对象的API,可以防止数据被意外修改,增强应用程序的稳定性和可维护性
readonly
readonly接受一个对象、ref或reactive,这个对象可以是响应式的也可以是普通对象,并返回一个原值的只读代理,相当于拷贝了一份数据,这个数据是只读的。只读代理是深层的,对任何嵌套属性的访问都将是只读的
<script>
import { reactive,readonly } from 'vue';
export default {
setup(props,context) {
let obj1=reactive({name:"小芳",age:20})
//obj是只读的,不能进行修改
let obj=readonly(obj1)
function changeFn(){
//obj1还是响应式的,可以继续修改,并且会更新obj
obj1.name="李四"
//不能修改,会得到警告
obj.name=""李四
}
return { changeFn,obj1,obj };
},
};
</script>
注:当原始对象发生改变时,只读代理也会更新,但不能直接修改
readonly使用场景
父子组件传值:当父组件通过props传递数据给子组件时,而子组件不能修改传入的props时,可以使用readonly进行包裹。但vue的props属性本身就是只读的,我们通常在组合式函数中返回状态给多个组件时,会使用
全局状态管理:在全局状态中,我们希望某些状态是只读的,可以使用readonly进行包裹,避免意外修改
保护重要数据:在多个组件共享数据时,确保数据不会被意外修改
shallowReadonly
shallowReadonly只将对象的第一层属性设置为只读的,深层嵌套的属性则保持不变,可以进行修改
<script>
import { reactive,shallowReadonly } from 'vue';
export default {
setup(props,context) {
let obj1=reactive({name:"小芳",age:20,address:{id:1,city:"郑州"}})
//obj.name,obj.age是只读的,不能进行修改
let obj=shallowReadonly(obj1)
function changeFn(){
//obj1.address不是只读的,可以进行修改
obj.address.id=2
}
return { changeFn,obj1,obj};
},
};
</script>
注:浅层只读是只有根级别的属性是只读的,嵌套对象仍然是可以进行修改的。如果传入的是响应式对象,它的第一层属性是只读的,嵌套对象仍然是响应式的,可以进行修改
shallowReadonly使用场景
性能优化:对于一些大型项目,深度只读可能会带来一些性能开销,如果只需要保护顶层,可以使用shallowReadonly减少开销
保护部分对象:如果对象的顶层属性可以不被修改,但嵌套对象需要被修改时,可以使用shallowReadonly
4、响应式工具函数
工具函数在处理响应式数据时,是非常有用的,特别是在组合式API中,工具函数可以帮我们更灵活地处理ref和reactive对象
isRef:用于判断一个数据是不是ref包装过的数据。当编写组合式函数或工具函数时,可能需要判断一个值是否为ref,以便分别处理
<script>
import { ref,isRef } from 'vue';
export default {
setup() {
const count=ref(10)
const num=10
function changeFn(){
console.log(isRef(count))//true
console.log(isRef(num))//false
}
return { changeFn,count,num };
},
};
</script>
unref:如果参数是一个ref,则返回内部的值,否则返回参数本身。当我们需要获取一个可能是ref的实际值时使用,避免重复判断
unref也就等同于isRef(val)?val.value:val
<script>
import { ref,unref } from 'vue';
export default {
setup() {
const count=ref(1)
const num=0
function changeFn(){
console.log(unref(count))//1
console.log(unref(num))//0
}
return { changeFn,count,num };
}
};
</script>
toRef:就是将对象的某一属性变成响应式的,可以解决对象解构赋值后失去响应式的问题,toRef函数接收两个参数,第一个参数是操作对象,第二个参数是对象的属性,
<script>
import { reactive,toRef } from 'vue';
export default {
setup() {
const state=reactive({count:0})
//第一个参数是操作的对象,第二个参数是对象的属性
const stateRef=toRef(state,"count")
//修改ref会更新原始属性
stateRef.value++
console.log(state.count)//1
//修改原始属性也会更新ref
state.count++
console.log(stateRef.value)//2
return { state,stateRef };
}
};
</script>
toRefs:就是将对象的每个属性都变成响应式的,toRefs接收一个参数,是要操作的对象
写法一
<script>
import { reactive,toRefs } from 'vue';
export default {
setup() {
const state=reactive({
name:"小红",
age:18
})
const stateRefs=toRefs(state)
//修改state会反映在refs上
state.age++
console.log(stateRefs.count.value)//19
//修改toRefs也会反映在state上
stateRefs.age.value++
console.log(state.age)//20
return { state,name:stateRefs.name };
},
};
</script>
写法二
<script>
import { reactive,toRefs } from 'vue';
export default {
setup() {
const state=reactive({
name:"小红",
age:18
})
function fn(){
state.name="小明"
state.age=20
}
return { ...toRefs(obj),fn };
},
};
</script>
在组合式函数中返回响应式对象时,使用toRefs可以很方便地在解构返回对象时不丢失响应性,此外,在模板中,也可以直接使用解构后的属性
<script>
import { reactive,toRefs } from 'vue';
export default {
function useData(){
const state=reactive({
name:"小红",
age:18
})
//返回转换为refs对象,在解构时不会丢失响应性
return toRefs(state)
}
//在使用时,name,age都是ref,模板中可以直接使用,且保持响应式
const {name,age}=useData()
};
</script>
isProxy:返回一个布尔值,用于检查一个对象是否是由reactive、readonly、shallowReactive或shallowReadonly创建的代理
<script>
import { reactive,readonly,isProxy } from 'vue';
export default {
setup(){
const reactiveObj=reactive({
name:"王五"
})
const readonlyObj=readonly(reactiveObj)
console.log(isProxy(reactiveObj))//true
console.log(isProxy(readonlyObj))//true
const obj={name:"李四"}
console.log(isProxy(obj))//false
}
};
</script>
注:如果对象是reactive或readonly代理(包括浅层代理),会返回true,普通的对象会返回false。ref对象不是代理,它是创建了一个对象包裹了数据,没有将原始数据换掉,所以isProxy对ref返回false
toRaw:返回reactive、readonly、shallowReactive、shallowReadonly代理的原始对象,也就是数据本身,不是响应式的,可以临时读取数据而不会引起代理访问,或跟踪开销,也可以用于修改数据而不会触发更新
<script>
import { reactive,readonly,isProxy } from 'vue';
export default {
setup(){
const obj={count:0}
const reactiveObj=reactive(obj)
console.log(reactiveObj)//Proxy{count:0}
console.log(toRaw(reactiveObj))//{count:0}
console.log(toRaw(reactiveObj)===obj)//true
}
};
</script>
当需要读取大量数据而不想引起响应式开销时,可以使用toRaw,提升性能,或某些第三方库需要原始对象时,也可以使用toRaw
修改原始对象不会触发响应式更新,但它们是同一个对象,会反映在代理上,所以不建议持有对原始对象的持久使用
<script>
import { reactive,readonly,isProxy } from 'vue';
export default {
setup(){
const obj={count:0}
const reactiveObj=reactive(obj)
const rawObj=toRaw(reactiveObj)
//修改原始对象
rawObj.count=1
//不会触发响应式更新,由于是同一个对象,会反映在代理上
console.log(reactiveObj.count)//1
reactiveObj.count=2
console.log(rawObj.count)//2
}
};
</script>
markRaw:标记一个对象,使其永远不会被转换为代理,返回对象本身
<script>
import { reactive,markRaw } from 'vue';
export default {
setup(){
//将整个对象都变成原始数据
const obj=markRaw({count:0})
const reactiveObj=reactive(obj)
console.log(isReactive(reactiveObj))//false
//将响应式对象的某个属性变成原始数据
const obj1=reactive({name:"小红",age:19})
//对象的name属性不会变成响应式
const markedRaw=markRaw(obj1.name)
}
};
</script>
5、逻辑复用
1、组合式函数
在vue3中,组合式函数就是利用组合式API封装可复用逻辑的一种方式,通常使用ref、reactive、computed等函数来创建响应式状态和副作用,并返回响应式状态和方法供其它组件使用
组合式函数基本使用
在src目录下创建一个hooks文件夹,用于存放不同功能的组合式函数

在count.js文件中写与计数器相关的代码
///hooks/count.js文件
import { ref, onMounted } from "vue"
function useCount() {
const count = ref(0)
function add() {
count.value++
}
onMounted(()=>{
console.log("组件挂载了")
})
function minus() {
count.value--
}
return { count, add, minus }
}
export default useCount
注:组合式函数要有返回值,命名必须以use打头
在组件中使用这个组合式函数
<template>
<div>
<button @click="minus">-</button>
<p>{{ count}}</p>
<button @click="add">+</button>
</div>
</template>
<script>
import useCount from "./hooks/useCount";
export default {
setup() {
const {count,add,minus}=useCount()
return {count,add,minus };
}
};
</script>
组合式函数传参的情况
当多个组件有相同的功能代码需要复用,但数据不同,我们可以将不同的数据设置成参数,在组件中使用组合式API,传入参数就可以了
//useData.js
import axios from "axios";
import { ref, watchEffect, unref, isRef } from "vue";
function useData(city) {
const weather = ref(null)
const handleBlur = () => {
axios({
method: "get",
url: "https://restapi.amap.com/v3/weather/weatherInfo",
params: {
key: "580fd476df08d90a38d46b4d6b80537e",
//使用unref判断传入的数据是否是ref包裹的响应式数据,是的话,返回其内部值,不是则返回本身
city: unref(city),
extensions: "all"
}
}).then(res => {
weather.value = res.data.forecasts[0].casts
})
}
if (isRef(city)) {
watchEffect(handleBlur)
} else {
handleBlur()
}
return { weather }
}
export default useData
<template>
<div>
<input type="text" v-model="city">
<ul>
<li v-for="item in weather" :key="item.date">{{ item.date }}:{{ item.dayweather }}</li>
</ul>
</div>
</template>
<script>
import { ref } from "vue";
import useData from "./hooks/useData";
export default {
setup() {
const {count,add,minus}=useCount()
const city=ref("郑州")
const {weather} =useData(city)
return { weather,city };
}
};
</script>
2、自定义指令
自定义指令是用于操作dom元素的可复用工具,核心作用是封装重复的dom操作逻辑,可以弥补模板语法在底层dom控制上的不足
指令的生命周期钩子
自定义指令和组件是一样的有生命周期钩子,自定义指令的生命周期钩子只针对指令,它们都接受很多参数,但常用的参数就两个,一个是el,指向使用指令的元素对象。第二个参数是binding,它是一个对象,对象中有两个常用的属性,value传递给指令的值,arg就是获取指令后面的内容
<script setup>
// 指令的生命周期
created((el,binding)=>{
//元素初始化时调用,此时dom元素未挂载,无法操作el
})
mounted((el,binding)=>{
//元素挂载到dom后调用,适合执行初始化dom操作
})
updated((el,binding)=>{
// 元素依赖的响应式数据更新时调用,可以处理数据变化后的dom同步
})
unmounted((el,binding)=>{
//元素从dom卸载时调用,用于清理资源,如移除事件监听、定时器
})
</script>
注:指令的生命周期有七个,我们知道常用的生命周期就可以了
自定义指令有两种定义方式,第一种是全局自定义指令,另一种是局部自定义指令
全局自定义指令
在项目的入口文件main.js中,可以通过应用实例directive方法定义全局自定义指令,所有组件都可以使用
//main.js
const app=createApp(App)
//注册全局指令v-focus
app.directive("focus",{
mounted(el){
el.focus()
}
})
app.mount("#app")
局部自定义指令
在组件内部,可以通过directives选项来定义局部指令,只能在当前组件使用
<template>
<div>
<!-- 使用局部指令 -->
<input type="text" v-focus />
</div>
</template>
<script setup>
import { ref } from "vue";
//定义局部指令,实现输入框自动聚焦
const vFocus = {
mounted(el) {
el.focus();
},
};
</script>
用自定义指令实现控制元素的显示与隐藏
<template>
<div>
<!-- 使用局部指令 -->
<div v-toggle="visible">1111</div>
<button @click="visible = !visible">按钮</button>
</div>
</template>
<script>
import { ref } from "vue";
export default {
setup() {
const visible = ref(true);
return { visible };
},
directives: {
toggle: {
created(el, binding) {
el.style.display = binding.value ? "block" : "none";
},
//当我们点击按钮时,会触发更新的生命周期钩子
updated(el, binding) {
if (binding.value !== binding.oldValue)
el.style.display = binding.value ? "block" : "none";
},
},
},
};
</script>
三、生命周期
生命周期函数也叫生命周期钩子,描述了组件从创建到销毁的整个过程中不同阶段调用的方法,我们可以简单理解为到特定时间节点自动触发的函数
vue3的生命周期函数有两种写法,一种是组合式API的生命周期,另一种是选项式API的生命周期
1、组合式API的生命周期
初始化阶段
setup是组合式API的入口,它在beforeCreate之前调用,此时,组件实例还没有创建,即this为undefined。在这个阶段主要是创建响应式数据、定义方法、注册生命周期钩子等等
组件挂载阶段
组件挂载阶段有两个生命周期钩子,组件挂载前阶段onBeforeMount,组件挂载完成阶段onMounted,挂载阶段在整个生命周期中只执行一次,
onBeforeMount:组件挂载前阶段,在组件挂载到dom前调用,此时dom元素还未生成,无法访问组件根元素
onMounted:组件挂载完成后调用,dom元素已渲染到页面,可以执行dom操作,如初始化图表、绑定事件、发送初始化请求等等
组件更新阶段
组件更新阶段有两个生命周期钩子,组件更新前阶段onBeforeUpdate,组件更新完成阶段onUpdated,更新阶段在整个生命周期中会执行多次
onBeforeUpdate:响应式数据更新、dom重新渲染前调用,可以获取更新前的dom状态
onUpdated:dom更新完成后调用,能获取更新后的dom状态,但需要避免在此修改响应式数据,否则会触发无限更新循环
组件卸载阶段
组件卸载阶段有两个生命周期钩子,组件卸载前阶段onBeforeUnmount,组件卸载完成阶段onUnmounted,卸载阶段在整个生命周期中只执行一次
onBeforeUnmount:组件卸载前调用,可在此清理副作用,如清除定时器、解绑全局事件、销毁第三方实例
onUnmounted:组件完全卸载后调用,此时事件的dom和事件监听已被移除,无法再访问组件内的响应式数据
组合式API生命周期的写法
<script>
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
} from "vue";
export default {
setup() {
//搜索相关逻辑代码
onMounted(() => {
console.log("onMounted");
}),
onUpdated(function () {
console.log("onUpdated");
}),
onUnmounted(() => {
console.log("onUnmounted");
});
//列表相关逻辑代码
onMounted(() => {
console.log("onMounted");
}),
onUpdated(function () {
console.log("onUpdated");
}),
onUnmounted(() => {
console.log("onUnmounted");
});
}
};
</script>
注:组合式API的生命周期钩子要先引入。因为组合式API是按逻辑关注点来划分代码的,所以同一个生命周期钩子可以写多次。生命周期钩子接收的是一个回调函数,可以是普通函数,也可以是箭头函数。在做项目时,我常用的生命周期钩子就三个onMounted、onUpdated、onUnmounted
2、选项式API的生命周期
vue3的生命周期以组合式API为主,选项式API的生命周期了解即可
组件创建阶段
组件创建阶段有两个生命周期钩子,组件创建前阶段beforeCreate,组件创建完成阶段created,这个阶段为组件初始化数据和事件,还没有dom元素
beforeCreate:组件实例刚刚创建,data、methods等选项均未初始化,无法访问
created:组件实例创建完成后调用,data、methods等选项已完成初始化,但dom元素还未生成,可用于初始化数据请求
组件挂载阶段
组件挂载阶段有两个生命周期钩子,组件挂载前阶段beforeMount,组件挂载完成阶段mounted,这个阶段组件完成了dom渲染,将虚拟dom挂载到了真实页面
beforeMount:在组件挂载开始之前被调用,模板已经编译但未渲染到页面,组件根dom不存在
mounted:组件挂载完成后调用,真实dom已经生成,组件根dom可以访问,可用于操作dom,如初始化第三方插件
组件更新阶段
组件更新阶段有两个生命周期钩子,组件更新前阶段beforeUpdate,组件更新完成阶段updated。当组件的data发生改变时触发这个阶段,可用于监控数据变化或优化渲染
beforeUpdate:数据更新完成后,dom重新渲染前调用,可以获取更新前的dom状态
updated:dom重新渲染完成后调用,可以获取更新后的dom状态,要避免修改data,否则会循环触发更新
组件销毁阶段
组件销毁阶段有两个生命周期钩子,组件卸载前阶段beforeUnmounted,组件卸载完成阶段unmounted。这个阶段是组件从页面中卸载、实例被销毁时触发,主要用于清理资源
beforeUnmount:组件卸载前调用,此时组件仍能正常工作,可用于清除定时器、解绑事件监听等等
unmounted:组件卸载后调用,实例完全销毁,data、methods等均不可用
选项式API的写法
<script>
export default {
beforeCreate() {
console.log("child-beforeCreate");
},
created() {
console.log("child-created");
},
beforeMount() {
console.log("child-mount");
},
mounted() {
console.log("child-mounted");
},
beforeUpdate() {
console.log("child-beforeUpdate");
},
updated() {
console.log("child-updated");
},
beforeUnmount() {
console.log("child-beforeDestroy");
},
unmounted() {
console.log("child-destroyed");
},
};
</script>
注:选项式API是按功能模块来划分代码的,所以每个生命周期函数只能出现一次
3、生命周期执行顺序
组合式API的每个生命周期钩子可以写多次,选项API的每个生命周期钩子只能写一次,在组合式API中,相同的生命周期钩子按书写顺序来执行,不同的生命周期钩子,按照生命周期钩子本身的顺序来执行。除此之外,选项式API的生命周期执行顺序与组合式API的生命周期执行顺序基本相同,这里以选项式API的生命周期执行顺序为例

组件嵌套生命周期执行顺序

当我们更新父组件的时候,父子组件生命周期执行顺序,此时子组件没有使用父组件的数据

当子组件使用父组件的数据时,更新父组件,父子组件生命周期的执行顺序

当我们点击了父组件中的按钮时,让子组件销毁时,生命周期的执行顺序
//父组件
<template>
<div>
<MyChild :msg="msg" v-if="show"/>
<button @click="fn1">按钮</button>
</div>
</template>
<script>
import MyChild from "./views/MyChild.vue";
export default {
data(){
return{
msg:"我是父组件",
show:true
}
},
beforeCreate(){
console.log("father-beforeCreate")
},
created(){
console.log("father-created")
},
beforeMount() {
console.log("father-beforeMount")
},
mounted(){
console.log("father-mounted")
},
beforeUpdate(){
console.log("father-beforeUpdate")
},
updated(){
console.log("father-updated")
},
beforeUnmount(){
console.log("father-beforeUnmount")
},
unmounted(){
console.log("father-unmounted")
},
methods:{
fn1(){
this.show=false
}
},
components: {
MyChild
},
};
</script>

注:v-show是不能触发组件销毁的生命周期的,它通过display:none属性来控制元素的显示和隐藏,不算卸载,只是样式隐藏了,组件还在。v-if会触发销毁阶段的生命周期
4、Vue2 Vue3生命周期区别
如果是选项式API的生命周期的话,vue2与vue3的生命周期主要是卸载阶段不同,其它阶段完全相同。vue3的卸载阶段用beforeUnmount和unmounted替代了vue2的beforeDestroy和destroyed
如果是组合式API的生命周期的话,vue2与vue3的生命周期在写法上、组件创建阶段和卸载阶段都有所不同。vue3生命周期前面加on,vue2生命周期前面不加。在组件创建阶段,vue3使用了setup函数代替了vue2中的beforeCreate和created。在组件卸载阶段vue3使用了onBeforeUnmount和onUnmounted代替了vue2中beforeDestroy和destroyed
| Vue2生命周期 | vue3生命周期 | 说明 |
|---|---|---|
| beforeCreate | setup() | 在组件实例创建之前执行 |
| created | setup() | 组件实例创建完成后执行,可以拿到data中的数据 |
| beforeMount | onBeforeMount() | 挂载开始之前执行,可以拿到data中的数据,但页面没有dom标签 |
| mounted | onMounted() | 页面完成初次渲染后触发,可以获取页面的dom标签 |
| beforeUpdate | onBeforeUpdate() | 数据更新时执行,但在虚拟dom重新渲染和打补丁之前 |
| updated | onUpdated() | 数据更新后触发,可以获取更新后的数据和dom标签 |
| beforeDestroy | onBeforeUnmount() | 组件实例销毁之前执行 |
| destroyed | onUnmounted | 组件实例销毁之后执行,可以获取数据,但页面没有任何响应 |
| activated | onActivated() | 组件激活时执行 |
| deactivated | onDeactivated() | 组件切出时执行 |
四、Vue3内置特殊组件
vue3内置的特殊组件是vue3框架原生提供的,不需要额外注册就可以使用的功能型组件,在特定场景下,可以解决组件渲染与交互的问题。vue3内置特殊组件有七个,这里就主要讲解我使用过的内置组件
1、动态组件
vue3动态组件的核心是通过<component>标签的is属性来动态渲染不同组件或原生HTML元素,不需要使用复杂的状态管理或条件渲染,可以实现组件间的灵活切换
在setup语法糖中,我们可以使用<component>组件标签的:is属性来动态切换组件,可以通过一个变量来控制当前要渲染的组件,也可以直接使用组件的选项对象就是导入组件本身来切换
<template>
<div>
<component :is="currentHome" />
<component :is="currentLogin" />
</div>
</template>
<script setup>
import { shallowRef } from "vue";
import Home from "./views/Home.vue";
import Login from "./views/Login.vue";
//当前要渲染的组件,初始值为Home
//使用组件的选项对象
const currentHome = shallowRef(Home);
const currentLogin = shallowRef(Login)
</script>
注:我们可以使用shallowRef来定义组件,因为组件实例不需要深度响应式,可以提高性能
在setup语法糖中,我们可以直接引入组件,通过组件名称来动态切换组件
<template>
<div>
<component :is="currentHome" />
<button @click="fn">登录</button>
</div>
</template>
<script setup>
import { shallowRef } from "vue";
import Home from "./views/Home.vue";
import Login from "./views/Login.vue";
//当前要渲染的组件,初始值为Home
const currentHome = shallowRef (Home);
function fn() {
currentHome.value = Login;
}
</script>
动态组件可以像普通组件一样传递props和监听事件
<template>
<div>
<component
:is="currentHome"
:user="user"
:updateUser="updateUser"
/>
<button @click="fn">登录</button>
</div>
</template>
<script setup>
import { shallowRef,ref } from "vue";
import Home from "./views/Home.vue";
import Login from "./views/Login.vue";
//当前要渲染的组件,初始值为Home
const currentHome = shallowRef (Home);
const user=ref({name:"张三",age:19})
const updateUser=(data)=>{
user.value=data
}
function fn() {
currentHome.value = Login;
}
</script>
当我们切换动态组件时,默认情况下,vue会销毁旧组件并创建新组建,组件的状态,如数据、生命周期等,不会保留。如果我们想保留组件状态的话,可以使用<KeepAlive>组件包裹动态组件
<template>
<div>
<keep-alive>
<component :is="currentHome" />
</keep-alive>
<button @click="fn">登录</button>
<button @click="currentHome=Home">主页</button>
</div>
</template>
<script setup>
import { shallowRef , provide } from "vue";
import Home from "./views/Home.vue";
import Login from "./views/Login.vue";
//当前要渲染的组件,初始值为Home
const currentHome = shallowRef (Home);
function fn() {
currentHome.value = Login;
}
</script>
注:使用<KeepAlive>组件后,被切换掉的组件会被缓存起来,而不是销毁,当再次切换回来时,组件状态会被保留
如果我们想要<KeepAlive>组件同时缓存多个组件或不缓存哪些组件时,还以配置include、exclude和max
<template>
<div>
//max表示缓存组件的最大数量
<KeepAlive :include="["home","login"]" :max="10">
<component :is="currentHome" />
</KeepAlive>
<button @click="fn">登录</button>
<button @click="currentHome=Home">主页</button>
</div>
</template>
<script setup>
import { shallowRef , provide } from "vue";
import Home from "./views/Home.vue";
import Login from "./views/Login.vue";
//当前要渲染的组件,初始值为Home
const currentHome = shallowRef (Home);
function fn() {
currentHome.value = Login;
}
</script>
注:使用include和exclude时,它们匹配的是组件的name选项,所以我们需要给要缓存的组件设置name选项
//在vue3.3中,可以使用setup语法糖中内置的编译器宏
<script setup>
defineOptions({
name:"home"
})
</script>
//也可以使用单独的<script>块
<script>
export default{
name:"home"
}
</script>
注:<KeepAlive>组件还可以与路由配合使用,后面的文章会详细讲解
2、teleport组件
在vue3中,Teleport组件是一个内置组件,它允许我们将组件的部分内容传送到vue应用之外的dom节点,主要用于处理模态框、通知、提示框等需要脱离当前组件层级结构的场景
在vue中,组件的模板结构是嵌套的,但有时我们可能需要将某个子组件在dom中渲染到不同的位置,而不是按照组件层次结构渲染
teleport组件接受两属性,第一个属性是to,指定目标容器,可以是一个css选择器字符串,也可以是一个实际的dom元素。第二个属性是disabled,它是一个可选的属性,用于禁用传送功能,当为true时,内容将保留在原位置
子组件
<template>
<div>
<Teleport to='.parent' >
<h1 class="title">我是子组件</h1>
</Teleport>
</div>
</template>
父组件
<template>
<div class="parent">
<h1>我是父组件</h1>
</div>
<MyChild/>
</template>
<script setup>
import MyChild from './views/MyChild.vue';
</script>
渲染结果

注:要确保Teleport的目标容器在组件挂载时已经存在于dom中,如果目标容器不存在,teleport内容将不会被渲染,teleport只是改变了dom渲染的位置,不会影响组件间的逻辑关系
3、slot插槽组件
插槽就是组件上的一个占位符,它可以让我们在使用组件时,可以给组件插入任意内容,这个内容可以是一段html代码或数据,让组件变得更灵活,可复用。根据插槽的命名可以将插槽分为匿名插槽,具名插槽和作用域插槽
匿名插槽
匿名插槽也叫默认插槽,在子组件内部使用<slot>标签定义插槽,在父组件中使用子组件时,直接在子组件标签内写内容
父组件
<template>
<div class="parent">
<MyChild>
<p>我是子组件</p>
</MyChild>
</div>
</template>
<script setup>
import MyChild from "./views/MyChild.vue";
</script>
子组件
<template>
<div>
<slot></slot>
</div>
</template>
注:匿名插槽的name属性默认为default
具名插槽
具名插槽就是当一个组件需要多个插槽时,在子组件中用name属性给每个插槽起个名字,在父组件中使用v-slot指令指定内容要插入哪个插槽
父组件
<template>
<div class="parent">
<MyChild>
<template #title>
<p>我是具名插槽</p>
</template>
</MyChild>
</div>
</template>
<script setup>
import MyChild from "./views/MyChild.vue";
</script>
子组件
<template>
<div>
<slot name="title"></slot>
</div>
</template>
条件插槽
条件插槽就是根据特定条件来决定是否渲染某个插槽的内容,或者渲染哪个插槽的内容,我们可以使用$slots来访问父组件传递给子组件的插槽内容
父组件
<template>
<div class="parent">
<MyChild>
<template v-slot:after>
<div>我在子组件后面</div>
</template>
</MyChild>
</div>
</template>
<script setup>
import MyChild from "./views/MyChild.vue";
</script>
子组件
<template>
<div>
<h1>我是子组件数据</h1>
//条件插槽
<div v-if="$slots.after">
<slot name="after"></slot>
</div>
</div>
</template>
注:$slots是vue实例上的一个特殊属性,它是一个对象,可以用来访问父组件传递给子组件的插槽内容,它可以访问具名插槽,默认插槽,还可以通过检查$slots.name属性是否存在,来判断父组件是否提供了该插槽
动态插槽
动态插槽就是插槽的名字可以是一个变量,而不是一个写死的字符串,这样我们就可以在父组件中根据不同的条件,动态的决定给子组件的哪个插槽传递内容
父组件
<template>
<div>
<MyChild>
//动态插槽
<template v-slot:[before]>
<h1>我在子组件前面</h1>
</template>
</MyChild>
</div>
</template>
<script setup>
import MyChild from "./views/MyChild.vue";
import { ref } from "vue";
const before = ref("before");
</script>
子组件
<template>
<div>
<slot name="before"></slot>
<h1>我是子组件数据</h1>
</div>
</template>
作用域插槽
作用域插槽就是子组件可以把数据传给父组件,父组件在填充内容时能使用这些数据。在子组件的<slot>上绑定属性,这些属性会成为一个对象,传给父组件,父组件会用v-slot:default="slotProps"接收子组件传来的数据
子组件
<template>
<div>
<slot name="before" :info="info"></slot>
<h1>我是子组件数据</h1>
</div>
</template>
<script setup>
import { ref } from "vue";
const info = ref("我是作用域插槽");
</script>
父组件
<template>
<div>
<MyChild>
//作用域插槽
<template v-slot:before="slotProps">
<h1>我在子组件前面{{ slotProps.info }}</h1>
</template>
</MyChild>
</div>
</template>
<script setup>
import MyChild from "./views/MyChild.vue";
</script>
五、相关面试题
1、vue响应式原理
在面试时,我们先说一下vue响应式原理的核心思想,然后再分别说一下vue2和vue3的响应式原理就可以了
vue的响应式原理核心是数据劫持结合发布订阅模式。通过追踪数据的变化,在数据被读取时收集依赖,在数据被修改时通知依赖更新
vue2的响应式是基于es5的Object.defineProperty劫持对象属性的setter和getter,再配合Dep依赖收集器和watcher观察者实现响应式的。vue底层是通过observer这个对象实现对data中的每个属性利用遍历对象递归的方式,实现监听的,对于每一个属性都会创建一个对应的dep对象来管理依赖。在getter函数里进行依赖收集,dep.target指向当前正在计算的watcher,getter会把这个watcher添加到dep的订阅者列表中。然后在setter函数里进行派发更新,当数据变化时,setter会调用dep.notify方法,通知所有wather更新数据。watcher是响应式的核心纽带,每个组件实例通常会对应一个渲染的watcher,当数据变化通知到watcher时,它并不会立即更新,而是将自己放入一个异步更新队列中,在下一个事件循环中,这些watcher会批量执行,最终会触发组件的重新渲染
vue3使用了es6的proxy来重写了响应式系统,弥补了vue2的不足。proxy可以拦截整个对象,不仅仅是对象的某个属性,它可以拦截多达13种操作,包括属性的读取、设置、和删除等等,解决了vue2无法监听数组索引、数组长度变化和对象新增属性的问题。vue3在track函数里进行依赖收集,在trigger函数里进行派发更新
面试时还可能会问:
vue2为什么不能直接监听数组索引和长度的变化
因为vue2在初始化时,只对data里的对象属性遍历并添加getter和setter,而数组是通过重写7个变更方法,如push、pop、slice来实现响应式的。数组的索引修改和长度修改不会触发重写方法,也没有对应的setter,所以无法进行监听
vue3的proxy相比vue2的Object.defineProperty有哪些优势
proxy能监听整个对象,不需要遍历每个属性,初始化的性能更好,它还原生支持监听数组索引、长度变化和对象新增属性,不用手动调用$set,此外,proxy还能拦截更多操作,如set、get、delete等等,响应式覆盖更广
vue2中data为什么是一个函数而不是一个对象
因为组件会被复用,data是函数的话,能让每个组件实例生成独立的data对象,可以避免多个实例共享同一个数据,导致修改一个实例的data时,其它实例的数据也会被意外篡改
2、ref reactive区别
ref和reactive都是vue3提供的响应式的API,它们的主要区别就是ref可以包装任意类型的值并使其变为响应式数据,我们在js中可以通过.value来访问其内部值,在template模板中或作为响应式属性时,可以自动解包,而reactive是通过proxy进行代理的,它可以直接深度代理一个对象,我们可以直接像访问普通对象一样访问和修改它的属性
reactive代理数据会存在响应式丢失的问题,如果将reactive对象的属性直接解构赋值给一个新变量,或将整个对象传递给函数,其响应式连接会丢失,而ref不存在这个问题,无论在哪里传递ref,都需要通过.value访问,响应式始终与ref本身绑定,所以,我在开发时,会优先使用ref来定义数据,保证数据的一致性,而在处理一些固定的、不需要整体替换的复杂对象时,会使用reactive来定义数据
面试还可能会问我们:
为什么修改reactive定义的对象时,不能直接赋值一个新对象
因为reactive是通过proxy代理了初始对象,如果直接赋值新对象的话,会让原对象指向新的内存地址,这个新对象没有被proxy代理,就失去了响应式能力,我们可以通过修改对象的某个属性或用object.assign合并新属性来保持响应式
3、vue的生命周期
在面试时,我们可以先概括一下生命周期,然后先具体说一下vue2的生命周期,再对比的说一下vue3的生命周期就可以了
vue的生命周期指的是vue实例从创建到销毁的整个过程,分为创建、挂载、更新和销毁四个阶段。每个阶段都提供了钩子函数,可以让我们在特定的时刻执行代码
在vue2中创建阶段有两个生命周期钩子,beforeCreate是组件实例刚刚创建,data、methods等选项均未初始化,无法访问,created组件实例创建完成后调用,data、methods等选项已完成初始化,但dom元素还未生成,可用于初始化数据请求。挂载阶段的两个生命周期钩子是beforeMount在组件挂载开始之前被调用,模板已经编译但未渲染到页面,组件根dom不存在,mounted组件挂载完成后调用,真实dom已经生成,组件根dom可以访问,可用于操作dom,如初始化第三方插件。更新阶段会触发的两个生命周期钩子beforeUpdate数据更新完成后,dom重新渲染前调用,可以获取更新前的dom状态,updated是dom重新渲染完成后调用,可以获取更新后的dom状态,要避免修改data,否则会循环触发更新。销毁阶段的两个生命周期钩子是beforeUnmount组件卸载前调用,此时组件仍能正常工作,可用于清除定时器、解绑事件监听等等,unmounted组件卸载后调用,实例完全销毁,data、methods等均不可用
在vu3中,生命周期有两种写法,一种是选项式API的生命周期,另一种是组合式API的生命周期,如果是选项式API的生命周期的话,vue2与vue3的生命周期主要是卸载阶段不同,其它阶段完全相同。vue3的卸载阶段用beforeUnmount和unmounted替代了vue2的beforeDestroy和destroyed。如果是组合式API的生命周期的话,vue2与vue3的生命周期在写法上、组件创建阶段和卸载阶段都有所不同。在写法上,vue3生命周期前面加on,vue2生命周期前面不加。在组件创建阶段,vue3使用了setup函数代替了vue2中的beforeCreate和created。在组件卸载阶段vue3使用了onBeforeUnmount和onUnmounted代替了vue2中beforeDestroy和destroyed
在做项目时,我常用的生命周期有四个,在使用ajax获取页面初始数据时,我会使用created,因为它获取数据的时间会比mounted更早。当初始化dom的第三方库,如echarts,地图的SDK等等,我会使用mounted。在数据变化后执行需要更新后的dom状态时,会使用updated。清除定时器或移除全局事件时,会使用beforeUnmount
面试中还可能会问:
created和mounted哪个更适合获取数据
从技术上来说,两个都可以,但是created获取数据的时间比mounted更早,可以减少用户等待的时间,如果数据依赖于dom尺寸或位置,必须在mounted中
4、watch watchEffect区别
在vue3中watch和watchEffect的核心区别首先是执行时机,watch默认是惰性监听,首次渲染不执行,只有指定的监听源变化时才会触发,而watchEffect会在首次渲染时立即执行,之后依赖每变化一次都会触发。其次是监听方式,watch需要我们明确指定监听源,比如某个ref或reactive属性,只追踪这个指定源,而watchEffect不用指定源,会自动追踪回调里用到的所有响应式依赖。最后是回调参数,watch的回调能拿到新值、旧值和清理函数,方便做新旧对比,而watchEffect的回调只有清理函数,没法直接获取依赖的新旧值。
更多推荐


所有评论(0)