目录

一、项目创建 

1、Vue CLI搭建项目

2、vite搭建项目

3、集成ts

4、项目目录简介

二、组合式API

1、setup

2、setup语法糖

1、使用setup语法糖

2、内置编译器宏

1、组件通信类

2、组件配置类

3、插槽操作类

4、依赖注入类

3、响应式API

1、ref

2、reactive

3、computed

4、watch和watchEffect

5、性能优化的API

6、只读响应式的API

4、响应式工具函数

5、逻辑复用

1、组合式函数

2、自定义指令

三、生命周期

1、组合式API的生命周期

2、选项式API的生命周期

3、生命周期执行顺序

4、Vue2 Vue3生命周期区别

四、Vue3内置特殊组件

1、动态组件

2、teleport组件

3、slot插槽组件

五、相关面试题

1、vue响应式原理

2、ref reactive区别

3、vue的生命周期

4、watch watchEffect区别


一、项目创建 

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生命周期说明
beforeCreatesetup()在组件实例创建之前执行
createdsetup()组件实例创建完成后执行,可以拿到data中的数据
beforeMountonBeforeMount()挂载开始之前执行,可以拿到data中的数据,但页面没有dom标签
mountedonMounted()页面完成初次渲染后触发,可以获取页面的dom标签
beforeUpdateonBeforeUpdate()数据更新时执行,但在虚拟dom重新渲染和打补丁之前
updatedonUpdated()数据更新后触发,可以获取更新后的数据和dom标签
beforeDestroyonBeforeUnmount()组件实例销毁之前执行
destroyedonUnmounted组件实例销毁之后执行,可以获取数据,但页面没有任何响应
activatedonActivated()组件激活时执行
deactivatedonDeactivated()组件切出时执行

四、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的回调只有清理函数,没法直接获取依赖的新旧值。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐