vue3最新

90 阅读19分钟

学习地址:www.bilibili.com/video/BV1dS…

1. 环境搭建

新建一个 vue3+ts 项目,命令如下 :

// 初始化项目
npm init vue@latest
// 下载依赖包
npm i
// 代码格式调整
npm run format 
// 运行项目
npm run dev

vscode 下载插件,当前我们用 vue3 + ts 开发,需要把如下两个插件给下载下来 Vue Language Features (Volar)TypeScript Vue Plugin (Volar).

注意:如果我们有 vue2 的项目下载了 vscode 插件 vetur, 我们用 vue3 + ts 开发时,要把 vue2 的 vetur 给禁用掉,因为它们俩是相互冲突的,同理,用vue2:vetur开发,要把  vue3:volar 禁用掉。

vue3:volar 插件 image.png

vue2:vetur插件
image.png

2. Vue核心虚拟Dom和 diff 算法

xiaoman.blog.csdn.net/article/det…

3. ref()

可以粗略理解为 ref = shallowRef + triggerRef

ref( ) 接受一个内部值,返回一个ref 对象,这个对象是响应式的、可更改的,且只有一个指向其内部值的属性 .value。

ref() 将传入参数的值包装为一个带 .value 属性的 ref 对象。

a. ref 对象是可更改的,即可以为 .value 赋予新的值

const a = ref(1);
// 为 a.value 赋予新的值
a.value = 2;
console.log("a--->", a);   
// {
//    "__v_isShallow": false,
//    "__v_isRef": true,
//    "_rawValue": 2,
//    "_value": 2
// }
console.log("a.value--->", a.value);   // 2

b. ref 对象是响应式的,即所有对 .value 的操作都将被追踪,并且写操作会触发与之相关的副作用。

ref()方法允许创建可以使用任何值类型的响应式 ref

ref 的 .value 属性也是响应式的。

当ref的值为对象类型时,会用 reactive() 自动转换它的 .value。

举例:一个包含对象类型值的 ref 可以响应式地替换整个对象

const b = ref({ name: 'vue3' });
// 响应式替换
b.value = { name: 'vite' };
console.log("b--->", b);
console.log("b.value--->", b.value);

查看打印结果:

image.png

c. ref 在模板中的解包

当 ref 在模板中作为顶层属性被访问时,它们会被自动“解包”,所以不需要使用 .value。

<script setup>
import { ref } from 'vue';
const a = ref(1);
</script>
 
<template>
  <!-- 无需 .value -->
  <div>a:{{ a }}</div>
</template>

⚠️请注意,仅当 ref 是模板渲染上下文的顶层属性时才适用自动“解包”。

<script setup>
import { ref } from 'vue';
const obj = {
  count: ref(1)
}
</script>
 
<template>
  <div>{{ obj.count + 1 }}</div> 
</template>

渲染的结果是 [object Object]1,因为 object.count 是一个 ref 对象

image.png 可以通过将 count 改成顶层属性来解决这个问题:

<script setup>
import { ref } from 'vue';
const obj = {
  count: ref(1)
}
// 将 count 改成顶层属性
const { count } = obj;
</script>
 
<template>
  <div>{{ count + 1 }}</div>
</template>

渲染结果是 2

image.png ⚠️如果一个 ref 是文本插值计算的最终值,它也将被解包

<script setup>
import { ref } from 'vue';
const obj = {
  count: ref(1)
}
const { count } = obj;
</script>
 
<template>
  <div>{{ count + 1 }}</div>
  <div class="count">{{ obj.count }}</div>
</template>

<div class="count">{{ obj.count }}</div>的渲染结果为 1

这只是文本插值的一个方便功能,相当于 {{ object.foo.value }}

image.png

d. ref 在响应式对象中的解包

当一个 ref 被嵌套在一个响应式对象中,作为属性被访问或更改时,它会自动解包,因此会表现得和一般的属性一样:

import { ref, reactive } from 'vue';
const a = ref(0);
const obj = reactive({
  a
})
console.log("obj.a--->", obj.a);
 
obj.a = 2;
console.log("a.value--->", a.value);

查看打印结果:

image.png

如果将一个新的 ref 赋值给一个关联了已有 ref 的属性,那么它会替换掉旧的 ref:

import { ref, reactive } from 'vue';
const a = ref(0);
const other = ref(1);
const obj = reactive({
  a
})
// 将一个新的 ref 赋值给一个关联了已有 ref 的属性
obj.a = other;
console.log("obj.a--->", obj.a); // 1
// 原始 ref 现在已经和 obj.a 失去联系
console.log("a.value--->", a.value); // 0

查看打印结果:

只有当嵌套在一个深层响应式对象内时,才会发生 ref 解包。当其作为浅层响应式对象的属性被访问时不会解包。

e. ts为 ref() 标注类型

ref 会根据初始化时的值推导其类型:

<script setup lang="ts">
import { ref } from 'vue'
 
const year= ref(2020)
 
year.value = 2020 // 成功!
</script>
 
<template>
  <p>{{year}}</p>
</template>

有时我们可能想为 ref 内的值指定一个更复杂的类型,可以通过使用 Ref 这个类型

<script setup lang="ts">
import { ref } from 'vue'
import type { Ref } from 'vue'
 
const year: Ref<string | number> = ref('2020')
 
year.value = 2020 // 成功!
</script>

或者,在调用 ref() 时传入一个泛型参数,来覆盖默认的推导行为:

// 得到的类型:Ref<string | number>
const year = ref<string | number>('2020')
 
year.value = 2020 // 成功!

如果你指定了一个泛型参数但没有给出初始值,那么最后得到的就将是一个包含 undefined 的联合类型:

// 推导得到的类型:Ref<number | undefined>
const n = ref<number>()

f. ref 引用DOM元素

我们可以使用ref函数来引用DOM元素,例如:

<template>
  <div ref="container"></div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
const container = ref(null) // 注意:此处的变量名必须和标签上的属性名一致
onMounted(() => {
  console.log(container.value) // 输出 <div></div>
})
</script>

<style scoped>
</style>

在这个例子中,我们使用ref函数来创建一个container引用,然后在模板中使用ref="container"来将这个引用绑定到一个<div>元素上。在setup函数中,我们使用onMounted钩子来访问这个引用的值。

g. ref 引用组件实例

我们可以使用ref函数来引用组件实例,例如:

<template>
  <Child ref="child" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import Child from './Child.vue'

const child = ref(null)
</script>

<style scoped>
</style>

在这个例子中,我们使用ref函数来创建一个child引用,然后将这个引用绑定到一个<Child>组件上。在script中,我们将这个引用暴露出去,以便在其他地方使用

4. isRef()

isRef 顾名思义它是用来判断某个值是否是 ref,注意:它判断不了这个值是不是 reactive(可以使用 isReactive 判断)

import { reactive, isRef, ref } from "vue";
const count = ref(1);
const testObj = reactive({
  a: 1,
});
console.log(isRef(count)); //true
console.log(isRef(testObj)); //false

5. toRef

toRef 可以根据一个响应式对象中的一个属性,创建一个响应式的 ref。同时这个 ref 和原对象中的属性保持同步,改变原对象属性的值这个 ref 会跟着改变,反之改变这个 ref 的值原对象属性值也会改变,它接收两个参数,一个是响应式对应,另一个则是属性值,例如下面代码

<template>
    <div>
        {{ count.a }}
        {{ a }}
        <button @click="addCount">+1</button>
    </div>
</template>

<script lang='ts' setup>
import { ref, toRef } from "vue"
const count = ref({
    a: 1,
    b: 2
})
const a = toRef(count.value, 'a')
const addCount = () => {
    a.value++
}
</script>

点击按钮的时候修改了 a 的值,此时 count 中的 a 也会跟着修改,当然这里的 count 也可以用 reactive

v2-e0887ab28fae954e73addebcfebef99e_b.webp

5. shallowRef() 函数

shallowRef() 传入的值是会进行变化,但是视图不会更新。

<template>
  <div>
    <h3>shallowRef: {{ shallowRefOjb }}</h3>
    <button @click="changeName">修改值</button>
  </div>
</template>

<script setup lang="ts">
import { shallowRef } from 'vue'
  const shallowRefOjb = shallowRef({name: 'shallowRef'})
  const changeName = () => {
    shallowRefOjb.value.name = '我变了'
    console.log('shallowRefOjb: ', shallowRefOjb);
  }
</script>

如下,shallowRefOjb.value.name 的值变了,但是视图不会更新

image.png

6. toRefs 将一个响应式对象转成普通对象

toRefs 它可以将一个响应式对象转成普通对象, 而这个普通对象的每个属性都是响应式的 ref

<template>
    <div>
        {{ count.a }}
        {{ countAsRefs.a }}
        <button @click="addCount">+1</button>
    </div>
</template>

<script lang='ts' setup>
import { reactive, toRefs } from "vue"
const count = reactive({
    a: 1,
    b: 2
})
const countAsRefs = toRefs(count)
const addCount = () => {
    countAsRefs.a.value++
}

</script>

此时代码中的countAsRefs类型为

{
  a: Ref<number>,
  b: Ref<number>
}

它的属性 a 和 b 都是响应式的 ref 对象,同样的它们和原对象的 count 的属性也是保持同步的

v2-e0887ab28fae954e73addebcfebef99e_b.webp

根据它的特性我们通常用它来解构一个响应式对象而不会让其失去响应式

import { reactive, toRefs } from "vue";
const count = reactive({
  a: 1,
  b: 2,
});
const { a, b } = toRefs(count);

此时的 a 和 b 都是一个响应式的 ref 对象,并和原对象的 a 和 b 属性保持同步

7. toRaw 把响应式的数据转换成不响应式的

6. triggerRef()

triggerRef() 可以搭配 shallowRef() 使数据 变成响应式

<template>
  <div>
    <h3>shallowRef: {{ shallowRefOjb }}</h3>
    <button @click="changeName">修改值</button>
  </div>
</template>

<script setup lang="ts">
import { shallowRef,triggerRef } from 'vue'

  const shallowRefOjb = shallowRef({name: 'shallowRef'})
  const changeName = () => {
    shallowRefOjb.value.name = '我变了'
    triggerRef(shallowRefOjb)
    console.log('shallowRefOjb: ', shallowRefOjb);
  }
</script>

image.png

7. reactive()

a. reactive()有限的值类型

image.png

  • ref() 支持所有的类型,但是 reactive 只支持引用类型,如Array, Obeject,Map,Set
import { reactive } from 'vue'

type M = {
  name: string,
  age: number
}
const form = reactive<M>({
  name: 'xl',
  age: 18
})

b. reactive 取、赋值不需要加 value

  • ref 取值、赋值都需要加 .value, reactive 是不需要加 .value

image.png

c. reactive 赋值问题

  • 在Vue 3.0 中我们使用 reactive() 定义的响应式数据的时候,当是一个数组或对象时,我们直接进行赋值,发现数据已经修改成功,但是页⾯并没有自动渲染成最新的数据;这是为什么呢?

原因就是reactive函数会返回一个Proxy包装的对象,所以当我们这样直接赋值时:(看下面例子)

import { reactive } from "vue";

let userInfo = reactive([{name:'Eula'}]) 
console.log(userInfo) // Proxy(Array) 打印出来是一个Proxy对象 当然具备响应式

// 直接后端数据进行赋值
userInfo = [{name:'优菈'}]
console.log(userInfo)  // [{name:'优菈'}] 可以看出 就是打印出了一个普通的数组 所以不具备响应式

这样赋值的话,就会把Proxy对象给覆盖掉,从而无法触发对应的 set和get,最终就会导致丢失掉响应性了;

上面的代码 reactive([{name:'Eula'}]) 创建了一个响应式数组,返回一个Proxy包装的对象由 userInfo 变量进行存放,但是后面我又把一个普通的数组(也就是后端返回的数据)赋值给userInfo,注意这时userInfo这个变量存放的已经是一个普通的数组了,当然也就不具备响应式了;

所以:对于reactive创建的响应式数据应该避免直接使用=号进行赋值;会覆盖响应式;

d. reactive赋值问题 3 种 解决方案

第1种解决方案: 再封装一层数据,即定义属性名,在后期赋值的时候,对此属性进行直接赋值

再封装一层数据,注意myRenderList 这个属性就是新增的属性用来存放列表数据,就是比较麻烦了一些。

<script setup>
import { reactive, ref } from "vue";
// 定义响应式 
let list1 = reactive({myRenderList:[]});

// 请求的数据
let newList1 = [
  { name: "Eula", age: "18", isActive: false },
  { name: "Umbra", age: "17", isActive: false },
]

// 更改数据
const setList1 = () => {
  list1.myRenderList = newList1
}
</script>

第2种解决方案: 使用数组的splice来直接更改原数组

还是用 reactive 来定义响应式数据,只不过改数据的方式变了,使用数组的原生方法splice()来更改原数组,不是直接覆盖所以并不会影响响应式;

splice有三个参数时,可以对数组进行修改, 第一项是起始索引, 第二项是长度, 第三项是新插入的元素,可以有多个;

下面的代码是把之前数组的每一项删除掉,然后插入新的数据newList1,使用...扩展符进行展开每一项;

 list1.splice(0,list1.length,...newList1)

当然,push()方法也是可以触发响应式的,只不过只能在后面新增数据。还有pop,shift,unshift等方法(用的不多)

<script setup>
import { reactive, ref } from "vue";
// 定义响应式 
let list1 = reactive([]);

// 请求的数据
let newList1 = [
  { name: "Eula", age: "18", isActive: false },
  { name: "Umbra", age: "17", isActive: false },
]

// 更改数据
const setList1 = () => {
  // splice三个参数时 第一项是起始索引  第二项是长度  第三项是新插入的元素,可以有多个
  list1.splice(0,list1.length,...newList1)
}
</script>
第3种解决方案: 使用 ref 来定义数据

复杂数据类型也可以使用 ref 进行定义,而且数据都是响应式的;原理就有点像第一种方式,重新包装了一层 value;每次使用的时候都要写.value;

ref 实际就是对一个普通值做了一层包装,包装成一个对象,并通过其 getset 实现依赖收集和更新,其实现原理类似于 computed;

<script setup>
import { reactive, ref } from "vue";
// 定义响应式
let list1 = ref([]);

// 请求的数据
let newList1 = [
  { name: "Eula", age: "18", isActive: false },
  { name: "Umbra", age: "17", isActive: false },
]

// 更改数据
const setList1 = () => {
  list1.value = newList1;
}
</script>

8. computed

计算属性就是当依赖的属性的值发生变化的时候,才会触发他的更改,如果依赖的值,不发生变化的时候,使用的是缓存中的属性值。

学习地址:xiaoman.blog.csdn.net/article/det…

a. 第 1 种写法:选项式写法

选项式写法: 支持一个对象传入 get 函数 以及 set 函数 自定义操作

<template>
 <div>
   <div>
     姓:
     <input type="text" v-model="firstName" />
   </div>
   <div>
     名:
     <input type="text" v-model="lastName" />
   </div>

   <div>姓名:{{ name }}</div>

   <button @click="changeName">changeName</button>
 </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
// 1. 选项式写法: 支持一个对象传入 get 函数 以及 set 函数 自定义操作
const name = computed({
 get() {
   return firstName.value + '-' + lastName.value
 },
 set(val) {
  [firstName.value, lastName.value] = val.split('-')
 }
})


</script>

这里的changeName 事件是生效的

image.png

b. 第 2 种写法: 函数式写法

函数式写法: 只支持一个 getter 函数且不允许修改值, 下面的 changeName 事件不起作用

<template>
 <div>
   <div>
     姓:
     <input type="text" v-model="firstName" />
   </div>
   <div>
     名:
     <input type="text" v-model="lastName" />
   </div>

   <div>姓名:{{ name }}</div>

   <button @click="changeName">changeName</button>
 </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')

// 函数式写法: 只支持一个 getter 函数且不允许修改值
const name = computed(() => firstName.value + '-' + lastName.value)

function changeName() {
 name.value = '李-四'
}
</script>

这里的changeName 不起作用, computed 只可读,不可修改

image.png

9. watch

watch 需要侦听特定的数据源,并在单独的回调函数中执行副作用

watch第一个参数监听源、

watch第二个参数回调函数cb(newVal,oldVal)、

watch第三个参数一个options配置项是一个对象{ immediate:true //是否立即调用一次 deep:true //是否开启深度监听 frush: 'pre' 表示在 DOM 更新之前会先调用 watch 的回调;frush: 'sync' 表示同步调用 watch 的回调; frush: 'post' 表示在 DOM 更新之后再调用 watch 的回调 }

watch(message1,
  (newMessage1, oldMessage1) => {     
    console.log('message1:', newMessage1)
    console.log('oldMessage1: ', oldMessage1);
  },
  {
    // 深度监听, 监听 ref 创建的深层次对象需要加个深度监听 deep:true ,不然无法监听到对象内部的变化
    deep:true,
    // 立即监听,immediate: true 表示在监听开始之后立即先触发监听器回调, immediate 默认值为 false, 不执行
    immediate: true,
    // frush: 'pre' 表示在 DOM 更新之前会先调用 watch 的回调;frush: 'sync' 表示同步调用 watch 的回调; frush: 'post' 表示在 DOM 更新之后再调用 watch 的回调
    frush: 'post'
  }
)

a. 监听 Ref 案例

import { ref, watch } from 'vue'
 
let message = ref({
    nav:{
        bar:{
            name:""
        }
    }
})
 
 
watch(message, (newVal, oldVal) => {
    console.log('新的值----', newVal);
    console.log('旧的值----', oldVal);
},{
    immediate:true,
    deep:true
})

b. 监听多个 ref 注意变成数组啦

import { ref, watch ,reactive} from 'vue'
 
let message = ref('')
let message2 = ref('')
 
watch([message,message2], (newVal, oldVal) => {
    console.log('新的值----', newVal);
    console.log('旧的值----', oldVal);
})

c. 使用 reactive 监听深层对象开启和不开启 deep 效果一样

import { ref, watch ,reactive} from 'vue'
 
let message = reactive({
    nav:{
        bar:{
            name:""
        }
    }
})
 
 
watch(message, (newVal, oldVal) => {
    console.log('新的值----', newVal);
    console.log('旧的值----', oldVal);
})

d. 监听 reactive 单一值

import { ref, watch ,reactive} from 'vue'
 
let message = reactive({
    name:"",
    name2:""
})
 
 
watch(()=>message.name, (newVal, oldVal) => {
    console.log('新的值----', newVal);
    console.log('旧的值----', oldVal);
})

10. watchEffect 高级侦听器

学习地址:blog.csdn.net/qq119556631…

a. watchEffect 用法

watchEffect: 立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

如果用到message 就只会监听message, 就是用到几个监听几个, 而且是非惰性, 会默认调用一次。

let message = ref<string>('')
let message2 = ref<string>('')
watchEffect(() => {
    //console.log('message', message.value);
    console.log('message2', message2.value);
})

watchEffect 监听时,默认会调用一次

image.png

b. watchEffect 清除副作用

就是在触发监听之前会调用一个函数可以处理你的逻辑例如防抖

import { watchEffect, ref } from 'vue'
let message = ref<string>('')
let message2 = ref<string>('')
 watchEffect((oninvalidate) => {
    //console.log('message', message.value);
    oninvalidate(()=>{
        
    })
    console.log('message2', message2.value);
})

c. 停止跟踪 watchEffect 返回一个函数 调用之后将停止更新

<template>
  <div>
    <input v-model="message1" type="text" placeholder="请输入关键字" />
    <br>
    <input v-model="message2" type="text" placeholder="请输入关键字" />

    <button @click="stopWatch">停止监听</button>
  </div>
</template>

<script setup lang="ts">
import { ref , watchEffect} from 'vue'
const message1 = ref('小')
const message2 = ref('大')
// 返回一个取消观察函数,用于停止触发回调
const stop = watchEffect((oninvalidate) => {
  console.log(message1.value, 'message1.value=====')
  console.log(message2.value, 'message2.value=====')
  oninvalidate(() => {
    console.log('oninvalidate')
    console.log(message1.value, 'message1.valu++++++')
    console.log(message2.value, 'message2.valu++++++')
  })
})

// 停止监听
const stopWatch=()=>{
  stop()
}
</script>

如下截图:

image.png

11. 认识组件&Vue3生命周期

a. 学习地址

视频:www.bilibili.com/video/BV1dS…

文档:blog.csdn.net/qq119556631…

b. 引入并使用组件

父组件引入子组件 helloWorld 然后直接就可以去当标签去使用 (切记组件名称不能与html元素标签名称一样), 不用再 components 这个对象里面进行注册

image.png

c. 组件的生命周期

简单来说就是一个组件从创建 到 销毁的 过程 成为生命周期

在我们使用Vue3 组合式API 是没有 beforeCreate 和 created 这两个生命周期的, 用 setup 替代这两个生命周期

onBeforeMount()

在组件DOM实际渲染安装之前调用。在这一步中,根元素还不存在。

onMounted()

在组件的第一次渲染后调用,该元素现在可用,允许直接DOM访问

onBeforeUpdate()

数据更新时调用,发生在虚拟 DOM 打补丁之前。

onUpdated()

DOM更新后,updated的方法即会调用。

onBeforeUnmount()

在卸载组件实例之前调用。在这个阶段,实例仍然是完全正常的。

onUnmounted()

卸载组件实例后调用。调用此钩子时,组件实例的所有指令都被解除绑定,所有事件侦听器都被移除,所有子组件实例被卸载。

子组件

<template>
    <div>
        <div ref="div">{{str}}</div>
        <br>
        <br>
        <button @click="change">改变</button>
    </div>
</template>

<script setup lang='ts'>
import { ref, onBeforeMount,onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'
// beforeCreate created setup 语法糖模式是没有beforeCreate和created的这两个生命周期的, 用 setup 替代这两个生命周期
// onBeforeMount 获取不到Dom元素, onMounted 可以获取到Dom元素
// onBeforeUpdate 获取的是更新前的 Dom, onUpdated 获取的是更新后的 Dom
const str = ref('gg')
const div = ref<HTMLElement | null>(null)

const change = ()=>{
    str.value = 'xl'
}

console.log(`set up`)   // set up, this is the first console.log

onBeforeMount(()=>{
    console.log(`创建之前---onBeforeMount`, div.value)  // null
})
onMounted(()=>{
    console.log(`创建完成---onMounted`, div.value)  // <div>元素</div>
})
onBeforeUpdate(()=>{
    console.log(`更新之前---onBeforeUpdate`, div.value?.innerHTML) // gg
})
onUpdated(()=>{
    console.log(`更新完成---onUpdated`, div.value?.innerHTML)   // xl
})
onBeforeUnmount(()=>{
    console.log(`销毁之前---onBeforeUnmount`, div.value?.innerHTML) //  xl
})
onUnmounted(()=>{
    console.log(`销毁完成---onUnmounted`, div.value?.innerHTML) // undefined
})
</script>

父组件:

<template>
  <div>
   <A v-if="flag" />
   <br>
   <br>
   <button @click="flag=!flag">组件创建和销毁</button>
  </div>
</template>

<script setup lang="ts">
import A from './components/A.vue'
import { ref } from 'vue'
const flag = ref(true)
</script>

组件生命周期如下截图:

image.png

12. 父子组件传参

a. 学习

视频地址:www.bilibili.com/video/BV1dS…

文档地址:xiaoman.blog.csdn.net/article/det…

b. 父给子传值

父组件通过 v-bind 绑定一个数据,然后子组件通过 defineProps 接受传过来的值

如以下代码, 给 Menu 组件 传递了一个title 字符串类型是不需要 v-bind

<template>
    <div class="layout">
        <Menu  title="我是标题"></Menu>
    </div>
</template>

传递非字符串类型需要加 v-bind  简写 冒号:如下父组件传 data 对象

<template>
    <div class="layout">
        <Menu v-bind:data="data"  title="我是标题"></Menu>
    </div>
</template>
 
<script setup lang="ts">
import Menu from './Menu/index.vue'
import { reactive } from 'vue';
 
const data = reactive<number[]>([1, 2, 3])
</script>

c. 子组件接受值

子组件接受值, 通过 defineProps 来接受 defineProps是无须引入的直接使用即可

如果你使用的不是TS,

// 子组件接受参数
<template>
  <div class="about">
    <div>子组件</div>
    <div>
    // 根部直接用父组件传递过来的属性
     值: {{ title}}
    </div>
  </div>
</template>

<script setup >
// defineProps 接受父组件传递的属性
// 给 defineProps 赋值给 props 这样父组件传递的属性就可以在子组件中使用
const props = defineProps({
  title: {
    type: String,
    default: '默认值'
  }
})
// 给 defineProps 赋值给 props 这样父组件传递的属性就可以在子组件中使用
console.log('props: ', props.title);
</script>

如果我们使用的 TypeScript, 可以使用传递字面量类型的纯类型语法做为参数

如 这是TS特有的

<template>
    <div class="menu">
        菜单区域 {{ title }}
        <div>{{ data }}</div>
    </div>
</template>
 
<script setup lang="ts">
defineProps<{
    title:string,
    data:number[]
}>()
</script>

TS 特有的默认值方式

withDefaults是个函数也是无须引入开箱即用接受一个props函数第二个参数是一个对象设置默认值

type Props = {
    title?: string,
    data?: number[]
}
withDefaults(defineProps<Props>(), {
    title: "张三",
    data: () => [1, 2, 3]
})

或者:
type Props = {
    title?: string,
    data?: number[]
}
withDefaults(defineProps<Props>(), {
    title: "张三",
    data: () => [1, 2, 3]
})

d. 子组件事件上报,传参给父组件

子组件给父组件传参, 是通过 defineEmits 派发一个事件

我们在子组件绑定了一个click 事件 然后通过 defineEmits 注册了一个自定义事件 点击 click 触发 emit 去调用我们注册的事件 然后传递参数

// 子组件事件上报
<template>
    <div class="menu">
        <button @click="clickTap">派发给父组件</button>
    </div>
</template>
 
<script setup lang="ts">
import { reactive } from 'vue'
const list = reactive<number[]>([4, 5, 6])

const emit = defineEmits(['on-click'])

const clickTap = () => {
    emit('on-click', list)
}

//如果用了ts可以这样两种方式
// const emit = defineEmits<{
//     (e: "on-click", name: string): void
// }>()

 
</script>

e. 父组件接受子组件的事件

<template>
    <div class="layout">
        <Menu @on-click="getList"></Menu>
    </div>
</template>
 
<script setup lang="ts">
import Menu from './Menu/index.vue'
import { reactive } from 'vue';
 
const data = reactive<number[]>([1, 2, 3])
 
const getList = (list: number[]) => {
    console.log(list,'父组件接受子组件');
}
</script>

我们从 Menu 组件接受子组件派发的事件on-click, 后面是我们自己定义的函数名称getList, 会把参数返回过来

f. 子组件暴露给父组件内部属性和方法

子组件暴露给父组件内部属性 , 通过 defineExpose, 我们从父组件获取子组件实例通过ref

子组件 defineExpose

<template>
  <div class="about">
    <div>子组件</div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
// 给父组件暴露属性及方法
defineExpose({
  height: ref(180),
  sayHello: () => { 
    console.log('hello'); 
    }
})
</script>

父组件调用需要 onMounted 之后 调用

<template>
    <div>
      <AboutView ref="aboutViewRef"  />
      <button @click="getChildAttr">调用子组件的数据及方法</button>
    </div>
</template>

<script setup lang='ts'>
import AboutView from './views/AboutView.vue'
import {ref} from 'vue'


// 父组件调用子组件的方法, 这里的 aboutViewRef 是组件的 ref 的属性值 
const aboutViewRef = ref(null)

const getChildAttr = () => {
  console.log(aboutViewRef.value.height, '----') // 输出 <div></div>
  aboutViewRef.value.sayHello()  // hello
}

</script>

13. 全局组件,局部组件,递归组件

a. 学习地址

文档地址:xiaoman.blog.csdn.net/article/det…

视频地址:www.bilibili.com/video/BV1dS…

b. 全局组件

例如组件使用频率非常高(table,Input,button,等)这些组件 几乎每个页面都在使用便可以封装成全局组件

案例------我这儿封装一个Card组件想在任何地方去使用

<template>
  <div class="card">
     <div class="card-header">
         <div>标题</div>
         <div>副标题</div>
     </div>
     <div v-if='content' class="card-content">
         {{content}}
     </div>
  </div>
</template>
 
<script setup lang="ts">
type Props = {
    content:string
}
defineProps<Props>()
 
</script>
 
<style scoped lang='less'>
@border:#ccc;
.card{
    width: 300px;
    border: 1px solid @border;
    border-radius: 3px;
    &:hover{
        box-shadow:0 0 10px @border;
    }
 
    &-content{
        padding: 10px;
    }
    &-header{
        display: flex;
        justify-content: space-between;
        padding: 10px;
        border-bottom: 1px solid @border;
    }
}
</style>

image.png 使用方法

在main.ts 引入我们的组件跟随在createApp(App) 后面 切记不能放到mount 后面这是一个链式调用用

其次调用 component 第一个参数组件名称 第二个参数组件实例

import { createApp } from 'vue'
import App from './App.vue'
import './assets/css/reset/index.less'
import Card from './components/Card/index.vue'
 
 
createApp(App).component('Card',Card).mount('#app')

使用方法

直接在其他 vue 页面 立即使用即可 无需引入

<template>
 <Card></Card>
</template>

c. 批量注册全局组件

可以参考element ui 其实就是遍历一下然后通过 app.component 注册

image.png

d. 配置递归组件

原理跟我们写js递归是一样的 自己调用自己, 通过一个条件来结束递归 否则导致内存泄漏

父组件:

<template>
    <div>
      <Tree :data="data" />
    </div>
</template>

<script setup lang='ts'>
import Tree from './views/Tree.vue'
import {reactive} from 'vue'

const data = reactive([
    {
      name: '1',
      checked: false,
      children: [
        {
          name: '1-1',
          checked: false,
        },
        {
          name: '1-2',
          checked: false,
        },
      ]
    },
    {
      name: '2',
      checked: true,
      children: [
        {
          name: '2-1',
          checked: false,
        },
        {
          name: '2-2',
          checked: false,
        },
        {
          name: '2-3',
          checked: false,
        },
      ]
    },
    {
      name: '3',
      checked: false,
      children: [
        {
          name: '3-1',
          checked: false,
          children: [
            {
              name: '3-1-1',
              checked: false,
            },
            {
              name: '3-1-2',
              checked: true,
            }
          ]
        },
        {
          name: '3-2',
          checked: false,
        },
      ]
    },
])

</script>

子组件 Tree.vue

<template>
    <div class="tree" v-for="(item, index) in data" :key="index">
        <input type="checkbox" v-model="item.checked">
        <span>{{item.name}}</span>
        <!-- vue3 使用递归组件无需引入组件,直接使用组件名称就可以 -->
        <!--v-if="item?.children?.length" 通过一个条件来结束递归 否则导致内存泄漏 -->
        <agencyTree v-if="item?.children?.length" :data="item?.children"></agencyTree>
    </div>
</template>

<script setup lang='ts'>

defineProps<{
    data: array
}>()
</script>


<script  lang='ts'>
//  再增加一个script 通过 export 添加name
export default {
    name: 'agencyTree'
}
</script>

<style lang='scss' scoped>
.tree{
    margin-left: 20px;
}
</style>

f. 给递归组件定义名称

f-1: 再增加一个script 通过 export 添加 name
<script lang="ts">
export default {
  name:"agencyTree"
}
</script>
f-2: 直接使用文件名当组件名

image.png

f-3: 使用插件[ unplugin-vue-define-options]
import DefineOptions from 'unplugin-vue-define-options/vite'
import Vue from '@vitejs/plugin-vue'
 
export default defineConfig({
  plugins: [Vue(), DefineOptions()],
})

ts支持

 "types": ["unplugin-vue-define-options/macros-global"],

image.png template 

TreeItem 其实就是当前组件 通过import 把自身又引入了一遍 如果他没有children 了就结束

  <div style="margin-left:10px;" class="tree">
    <div :key="index" v-for="(item,index) in data">
      <div @click='clickItem(item)'>{{item.name}}
    </div>
    <TreeItem @on-click='clickItem' v-if='item?.children?.length' :data="item.children"></TreeItem>
  </div>
  </div>

image.png