笔记-学习Vue3第一章(自用)

414 阅读23分钟

一、创建Vue3项目

  1. 创建一个 Vue3 应用程序,使用如下命令:

    npm init vue@latest
    
  2. 项目被创建后,通过以下步骤安装依赖并启动开发服务器:

    npm install
    
    npm run dev
    
  3. 在项目中配置快速生成Vue3模板的命令

    3.1.在项目根目录的.vscode文件夹下新建 vue3.0.code-snippets文件 3.2.在vue3.0.code-snippets中将下面的代码片段复制粘贴进去:

    {
      "Vue3.0快速生成模板": {
        "prefix": "Vue3.0",
        "body": [
          "<template>",
          "\t<div>\n",
          "\t</div>",
          "</template>\n",
          "<script setup lang='ts'>",
          "</script>\n",
          "<style lang='scss' scoped>\n",
          "</style>",
          "$2"
        ],
        "description": "Vue3.0"
      }
    }
    
  4. 解决ts无法识别引入的.vue文件

    在项目根目录下的env.d.ts文件中添加如下代码:

    declare module "*.vue"{
        import { DefineComponent } from "vue"
        // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
        const component: DefineComponent<{}, {}, any>
        export default component
    }
    
  5. 安装sass

    npm install sass --save-dev
    
  6. 配置项目 .prettierrc.json 文件,代码格式化的配置项

    {
      "$schema": "https://json.schemastore.org/prettierrc",
      "semi": false,
      "tabWidth": 2,
      "singleQuote": true,
      "printWidth": 100,
      "trailingComma": "none"
    }
    

    以下是对每个选项的详细说明:

    printWidth:指定每行代码的最大宽度。默认为 80。

    tabWidth:指定一个制表符等于多少个空格。默认为 2。

    useTabs:指定是否使用制表符代替空格缩进。默认为 false。

    semi:指定是否在语句末尾添加分号。默认为 true。

    singleQuote:指定是否使用单引号而不是双引号。默认为 false。

    quoteProps:指定对象属性名称是否使用引号。可以是 “as-needed”、true 或 false。默认为 “as-needed”。

    jsxSingleQuote:指定 JSX 属性是否使用单引号而不是双引号。默认为 false。

    trailingComma:指定是否在数组和对象字面量的末尾添加逗号。可能的值是 “none”、“es5”(在 ES5 中有效)和 “all”。默认为 “es5”。

    bracketSpacing:指定是否在对象字面量中的括号之间添加空格。默认为 true。

    jsxBracketSameLine:指定是否将多行 JSX 元素的末尾括号放在同一行上。默认为 false。

    arrowParens:指定箭头函数参数是否永远使用圆括号。可以是 “always”、“avoid”、或 “as-needed”。默认为 “always”。

    requirePragma:指定是否需要在文件顶部添加 // @format 注释才会格式化。默认为 false。

    insertPragma:指定是否在文件顶部插入 // @format 注释。默认为 false。

    vueIndentScriptAndStyle:指定是否单独缩进 Vue 组件中的

    proseWrap:指定如何处理文本节点的换行。可以是 “preserve”、“always” 或 “never”。默认为 “preserve”。

二、ref全家桶

1. 声明响应式变量

ref()

ref(): 用来声明一个响应式变量,支持所有类型。

    import { ref } from 'vue
    const name = ref('张三')

ref()函数接收一个参数,这个参数作为声明变量时的初始值,返回一个Ref对象,如下图:

image.png

ref()函数会自动推断响应式变量值的类型,也可以接收一个泛型,显式的给响应式变量的值添加类型:

    import { ref } from 'vue
    const name = ref<string>('张三')

Ref接口类型

当响应式变量的值的类型比较复杂时,可以通过 Vue 提供的 Ref 接口类型显式的给响应式变量的值添加类型:

    import { ref } from 'vue'
    import type { Ref } from 'vue
       
    const name: Ref<string> = ref('张三')

获取/修改ref()声明的响应式对象的值

ref()返回一个es6的class类,所以我们需要通过变量名.value的形式,来获取使用ref()定义的响应式变量的值:

    import { ref } from 'vue'
    
    const name = ref('张三')
    console.log(name.value) // 张三
    name.value = '李四'
    console.log(name.value) // 李四

isRef()判断是否为ref对象

isRef()函数,顾名思义就是用来判断一个变量是否为ref对象的。

    import { ref, Ref } from 'vue'
    
    const name = ref('张三')
    const age = 18
    
    console.log(isRef(name)) // true
    console.log(isRef(age)) // false

响应式变量与非响应式变量的区别

通过代码示例可以看出,非响应式变量的值被更改后,再模板中是不会进行同步更新的:

    <div>响应式变量--name:{{ name }}--再模板中使用不需要加.value,vue会自动解包</div>
    <div>非响应式变量--age:{{ age }}</div> <!-- 18 -->
    <button @click="updateAge">修改非响应式变量age的值</button>
    <button @click="updateName">修改响应式变量name的值</button>
    import { ref } from 'vue'

    // 声明一个非响应式对象,非响应式变量的值被更改后,在模板中是不会进行同步更新的
    let age = 18
    // 修改age的值
    function updateAge () {
      age = 20
      console.log(age) // 20
    }

    // 声明一个响应式变量
    const name = ref('张三')
    // 修改name的值
    function updateName () {
      name.value = '李四'
      console.log(name.value) // 李四
    }

image.png 点击第一个修改非响应式变量age的值按钮,控制台打印的age的值发生了改变,但页面展示的age的值没有发生改变。

点击第二个修改响应式变量name的值按钮,控制台打印的name的值发生了改变,同时页面展示的name的值也发生了改变。

shallowRef

shallowRef()浅层次的响应式监听

ref()的区别:ref()是深层次的响应式监听

     <div>ref: {{ refObject }}</div>
     <div>shallowRef: {{ shallowRefObject }}</div>
     <button @click="updateRefObject">修改ref对象的值</button>
     <button @click="updateShallowRefObject">修改shallowRef对象的值</button>
    import { ref, shallowRef } from 'vue'
    
    // 声明一个ref对象
    const refObject = ref({
      name: '张三',
      age: 18
    })
    // 修改ref对象的值
    function updateRefObject () {
      refObject.value.name = '王五'
      refObject.value.age = 22
      console.log('refObject', refObject.value)
    }

    // 声明一个shallowRef对象
    const shallowRefObject = shallowRef({
      name: '李四',
      age: 19
    })

    function updateShallowRefObject () {
      shallowRefObject.value.name = '六六'
      shallowRefObject.value.age = 11
      console.log('shallowRefObject', shallowRefObject.value)
    }

image.png shallowRef()只能响应式监听到.value层,也就是说.value往下的层面就监听不了了。

如果是给.value赋值,那么shallowRef()就能够监听到:

    import { shallowRef } from 'vue'
    
    const name = shallowRef('张三')
    
    name.value = '李四'

shallowRef() 和 ref() 不能混用

shallowRef()ref() 不能混用,意思是说不能混着去写,一个组件内既有shallowRef()又有ref()。 因为当在同一个 方法 内同时修改 refshallowRef 对象时,会影响到shallowRef的响应式监听。

     <div>ref: {{ refObject }}</div>
     <div>shallowRef: {{ shallowRefObject }}</div>
     <button @click="updateRefObject">修改ref对象的值</button>
     <button @click="updateShallowRefObject">修改shallowRef对象的值</button>
    import { ref, shallowRef } from 'vue'
    
    // 声明一个ref对象
    const refObject = ref({
      name: '张三',
      age: 18
    })

    // 声明一个shallowRef对象
    const shallowRefObject = shallowRef({
      name: '李四',
      age: 19
    })

    function updateShallowRefObject () {
      refObject.value = '李四'
      shallowRefObject.value.name = '六六'
      console.log('shallowRefObject', shallowRefObject.value)
    }

如果触发updateShallowRefObject方法,此时,shallowRefObject.value 下面层次的属性也会被监听,就是 name 属性也会被监听到,页面视图也会更新。

image.png

treggerRef

为什么 上面的方式 会造成shallowRef的视图更新呢?

因为在 ref() 底层更新的逻辑中,会调用 triggerRef()这个函数

triggerRef()会强制更新收集的依赖

<div>shallowRef: {{ shallowRefObject }}</div>
<button @click="updateShallowRefObject">修改shallowRef对象的值</button>
   // 声明一个shallowRef对象
const shallowRefObject = shallowRef({
 name: '李四',
 age: 19
})

function updateShallowRefObject () {
 triggerRef(shallowRefObject)
 shallowRefObject.value.name = '六六'
 shallowRefObject.value.age = 11
 console.log('shallowRefObject', shallowRefObject.value)
}

image.png 我们在修改shallowRef对象的值按钮的事件处理函数中添加了triggerRef(),并将shallowRefObject对象作为参数传给了triggerRef(),这样triggerRef()就会强制更新视图中shallowRefObject的值。

ref获取dom元素对象

  1. 给需要获取的元素标签添加ref属性,值自定义

    <div ref="dom">我是divDOM元素</div>
    
  2. 声明一个响应式变量,初始值为空

    注意:这个变量的名字必须跟 标签上面ref属性的值的名字一样

    const dom = ref<HTMLDivElement>()
    console.dir(dom)
    

    image.png

reactive()

reactive()将一个引用类型,例如:Object、Array、Map、Set等,变成响应式的对象。

import { reactive } from 'vue'
    
const form = reactive({
    name: '张三',
    age: 22
})

reactive()ref()一样会自动推断类型,也可以接收泛型,显式的给响应式对象定义类型。

import { reactive } from 'vue'

interface Form {
    name: string
    age: number
}
    
const form = reactive<Form>({
    name: '张三',
    age: 22
}) 

获取/修改reactive()声明的响应式对象的值

ref()声明的响应式对象需要通过.value的形式获取/修改值

reactive()声明的响应式对象则不需要通过.value的形式获取/修改值

const form = reactive({
    name: '张三',
    age: 20
})
    
console.log(form.name) // 张三
form.age = 30
console.log(form.age) // 30

直接给reactive()声明的响应式对象赋值,会造成响应式的丢失

<!-- 调用接口获取数据 -->
<button @click="getList">获取用户列表</button>
<div>list: {{ list }}</div>
import { reactive } from 'vue'
// 直接给reactive()声明的响应式对象的响应式丢失,就是不再是响应式的
let list = reactive<string[]>([])

// 定时器标识符
let timer: number | null
function getList () {
  clearTimeout(timer as number)
  // 模拟发送请求
  timer = setTimeout(() => {
    let res = ['张三', '李四', '王五']
    list = res
    console.log(list) // ['张三', '李四', '王五']
    timer = null
  }, 2000)
}

image.png

解决方案(使用展开运算符)

数组可以使用push()方法结合...展开运算符可以解决 对象的话

### 直接给reactive()声明的响应式对象赋值,会造成响应式的丢失
```html
<!-- 调用接口获取数据 -->
<button @click="getList">获取用户列表</button>
<div>list: {{ list }}</div>
import { reactive } from 'vue'
// 直接给reactive()声明的响应式对象的响应式丢失,就是不再是响应式的
let list = reactive<string[]>([])

// 定时器标识符
let timer: number | null
function getList () {
  clearTimeout(timer as number)
  // 模拟发送请求
  timer = setTimeout(() => {
    let res = ['张三', '李四', '王五']
    list.push(...res)
    console.log(list) // ['张三', '李四', '王五']
    timer = null
  }, 2000)
}

对象可以使用for in 循环解决这个问题

<button @click="getList">获取用户信息</button>
<div>obj: {{ obj }}</div>
import { reactive } from 'vue'
    
let obj = reactive<Obj>({})

// 定时器标识符
let timer: number | null
function getList () {
  clearTimeout(timer as number)
  // 模拟发送请求
  timer = setTimeout(() => {
    let map: { [key: string]: any } = {
      name: '带土',
      age: 16
    }
    for (let key in map) {
      obj[key] = map[key]
    }
    console.log(obj)
    timer = null
  }, 2000)
}

readonly()

readonly()接收一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理。

只读代理是深层的:对任何嵌套属性的访问都将是只读的。它的 ref 解包行为与 reactive() 相同,但解包得到的值是只读的。 image.png

shallowReactive()

shallowReactive(),只对引用类型的第一层属性进行响应式监听,更深层次的属性不会进行响应式监听,就是视图不会进行更新。

shallowRactive() 和 reactive() 不能混用,意思是说不能混着去写,一个组件内既有shallowReactive()又有reactive()。 因为当在同一个 方法 内同时修改 reactiveshallowRactive 对象时,会影响到shallowReactiv的响应式监听。

toRef()、toRefs()、toRaw()

toRef()

toRef():基于响应式对象上的一个属性,创建一个对应的 ref。接受两个参数,第一个参数是响应式对象,第二个参数是这个响应式对象上的key 正确示例:

<div>响应式对象:{{ daitu }}</div>
<div>响应式对象中取出的:{{ like }}</div>
<button @click="setDaituLike">修改响应式对象取出来的值</button>
import { reactive, toRef } from 'vue'

// 响应式对象
const daitu = reactive({
  name: '带土',
  age: 16,
  like: '琳'
})

// 从响应式对象里面去除like的值,并响应式化
const like = toRef(daitu, 'like')

// 修改响应式对象中取出来的值
function setDaituLike () {
  like.value = '野原琳'
}

通过toRef()从响应式取出来的ref,和ref()声明的响应式数据一样,通过.value取值/修改。

修改响应式对象的值,从响应式对象取出来的ref的值也会随着改变

修改从响应式对象取出来的ref的值,响应式对象对应属性的值也会随着改变 非响应式示例:

<div>非响应式对象:{{ you }}</div>
<div>非响应式对象取出的:{{ youLike }}</div>
<button @click="setYouLike">修改非响应式对象取出来的值</button>
// 非响应式对象
const you = {
  name: '宇智波鼬',
  age: 20,
  like: '佐助'
}

// 从非响应式对象中取出来的值
const youLike = toRef(you, 'like')

// 修改非响应式对象取出来的值
function setYouLike () {
  youLike.value = '二柱子'
}

使用toRef()取非响应式对象中某个属性的值,取出来的是一个非响应式的,意思就是视图不会进行同步更新。

总结:toRef()只能将响应式对象里的属性的值提出来化为响应式ref,对非响应式对象毫无卵用。

toRefs()

直接给响应式对象赋值,或者直从响应式对象中解构出来的属性会丢失响应式。

toRefs()接收一个响应式对象为参数,toRef()是从响应式对象中单个提取属性的值,而toRefs()则可以从响应式对象中解构出多个属性。

示例:

<div>响应式对象:{{ kakaxi }}</div>
<div>响应式对象中通过toRefs()解构出来的属性:{{ name }}---{{ age }}---{{ kakaxiLike }}</div>
<button @click="updateKaKaXi">修改响应式对象的属性的值</button>
// toRefs(),从响应式对象中解构出多个属性,取出来的属性是响应式的(注意:不使用toRefs(),直接解构出来的属性会丢失响应式)
const kakaxi = reactive({
  name: '卡卡西',
  age: 20,
  like: 'lin'
})

// 从响应式对象中解构属性出来
const { name, age, like: kakaxiLike } = toRefs(kakaxi)

// 修改从响应式对象中解构出来的属性的值
function updateKaKaXi () {
  name.value = '旗木卡卡西'
  age.value = 22
  kakaxiLike.value = '樱'
}

toRaw()

toRaw()根据一个 Vue 创建的代理返回其原始对象。意思就是将一个响应式的对象转为非响应式的对象。

// toRaw():响应式对象转换为非响应式对象
const mingren = reactive({
  name: '旋涡',
  age: 10,
  like: '撒库拉'
})

// 打印转为非响应式前和转为响应式后的mingren对象
console.log(mingren)
console.log(toRaw(mingren)) 

打印的结果:

image.png

三、计算属性

如果在模板中写太多逻辑,会让模板变得臃肿,难以维护,这个时候我们可以通过计算属性来完成复杂的逻辑

computed()计算属性,当它里面所依赖的值发生改变的时候,就会触发computed()去更新。

计算属性的两种写法

选项式

选项式支持传入一个对象,该对象包含getter函数以及setter函数,默认计算属性是只读的,当计算属性为一个选项式时,可以通过同时提供 getset函数,进行读写操作。

语法:

import { computed } from 'vue'

const fullName = computed({
    get () {
    },
    set () {
    }
})

读示例:

import { ref, computed } from 'vue'
    
const firstName = ref('宇智波') 
const lastName = ref('带土') 
const fullName = computed({ 
    // getter 
    get() { 
        return firstName.value + ' ' + lastName.value 
    }, 
    // setter 
    set() {} 
})

结果:

image.png

写示例:

<div>fullName:{{ fullName }}</div>
<button @click="setComputed">修改计算属性的值</button>
import { ref, computed } from 'vue'
    
const firstName = ref('宇智波') 
const lastName = ref('带土') 
const fullName = computed({ 
    // getter 
    get() { 
        return firstName.value + ' ' + lastName.value 
    }, 
    // setter 
    set(newValue) { // newValue:漩涡-鸣人
        // 注意:我们这里使用的是解构赋值语法
        [firstName.value, lastName.value] = newValue.split('-') // 使用-分割字符串,返回一个数组,然后从数组中结构赋值给firstName.value和lastName.value
    } 
})
// 修改计算属性的值
function setComputed () {
  fullName.value = '旋涡-鸣人'
}

image.png

计算属性返回的是一个计算属性 ref

可以通过 fullName.value 访问/修改计算结果,在模板表达式中引用时无需添加 .value

set(newValue)函数的参数是接收赋值给计算属性的.value属性的值

函数式写法

计算属性的函数式写法只支持一个getter函数,只可以读取,不允许修改计算属性的值

示例:

<div>name:{{ fullName }}</div>
import { ref, computed } from 'vue'

const firstName = ref('宇智波')
const lastName = ref('带土')

// 只允许读取,不允许修改
const fullName = computed(() => firstName.value + '-' + lastName.value)

结果:

image.png

四、侦听器

watch(),接收两个参数,第一个是数据源,第二个是一个回调函数。当watch()函数侦听的响应式数据发生变化时,就会触发回调函数,watch()默认是懒执行的:仅当数据源变化时,才会执行回调。

示例:

<div>{{ name }}</div>
<button @click="updateName">修改name的值</button>
import { ref, watch } from 'vue'

const name = ref('宇智波带土')

function updateName () {
  name.value = '野原琳'
}

// 侦听响应式数据 name
watch(name, () => {
  console.log('name响应式数据发生了变化!!!')
})

image.png

点击修改name的值按钮,可以看到name的值,从宇智波带土变为了野原琳,同时侦听器也被触发了。

侦听的类型

watch() 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组。

示例侦听多个数据源:

<div>{{ name }}</div>
<div>{{ age }}</div>
<button @click="updateName">修改name的值</button>
<button @click="updateAge">修改age的值</button>
import { ref, watch } from 'vue'

const name = ref('宇智波带土')
const age = ref(18)

// 修改name的值
function updateName () {
  name.value = '野原琳'
}
// 修改age的值
function updateAge () {
  age.value = 20
}

// 侦听响应式数据 name
watch([name, age], () => {
  console.log('name响应式数据发生了变化!!!')
})

使用watch()侦听多个数据源,第一个参数为一个数组,将需要侦听的数据源放到数组中。

watch()侦听多个数据源时,只要任意一个数据源发生了变化,就会触发回调函数。

侦听响应式对象

使用watch()侦听一个使用ref()定义的响应式对象时,深层的属性发生变化是无法侦听到的,需要添加第三个参数,它是一个对象,在对象里面添加一个属性:deep: true,这样就可以侦听到对象深层次的属性值发生变化了。

示例:

ref()声明的响应式对象,没加{ deep: true }

<div>{{ obj }}</div>
<button @click="updateobj">修改obj中first的值</button>
import { ref, watch } from 'vue'
 
interface Like {
  first: string
  two: string
}
const obj = ref<Obj>({
  name: '带土',
  age: 19,
  like: {
    first: 'jk',
    two: '嗨丝'
  }
})
// 修改obj的值
function updateobj () {
  obj.value.like.first = '蕾丝'
}

// 侦听响应式数据 name
watch(obj, () => {
  console.log('name响应式数据发生了变化!!!')
})

结果: image.png 使用ref()声明的响应式对象,加了{ deep: true }

<div>{{ obj }}</div>
<button @click="updateobj">修改obj中first的值</button>
import { ref, watch } from 'vue'
 
interface Like {
  first: string
  two: string
}
const obj = ref<Obj>({
  name: '带土',
  age: 19,
  like: {
    first: 'jk',
    two: '嗨丝'
  }
})
// 修改obj的值
function updateobj () {
  obj.value.like.first = '蕾丝'
}

// 侦听响应式数据 name
watch(
    obj, 
    () => {
      console.log('name响应式数据发生了变化!!!')
    },
    {
        deep: true
    }
)

image.png

watch()侦听一个使用reactive()声明的响应式对象,不用添加 { deep: true },就可以侦听对象深层次的属性变化

示例:

<div>{{ obj }}</div>
<button @click="updateobj">修改obj中first的值</button>
import { ref, reactive, watch } from 'vue'
    
interface Obj {
  name: string,
  age: number,
  like: Like
}

interface Like {
  first: string
  two: string
}
const obj = reactive<Obj>({
  name: '带土',
  age: 19,
  like: {
    first: 'jk',
    two: '嗨丝'
  }
})
// 修改obj的值
function updateobj () {
  obj.like.first = '蕾丝'
}

// 侦听响应式数据 name
watch(obj, () => {
  console.log('name响应式数据发生了变化!!!')
})

结果: image.png

侦听响应式对象的属性

当使用watch()响应式对象的属性时,不能直接侦听响应式对象的属性,需要用一个返回该属性的 getter 函数。

<div>{{ obj }}</div>
<button @click="updateobj">修改obj中name的值</button>
import { ref, reactive, watch } from 'vue'
    
interface Obj {
  name: string,
  age: number
}
const obj = reactive<Obj>({
  name: '带土',
  age: 19
})
// 修改obj的值
function updateobj () {
  obj.name = '宇智波带土'
}

// 侦听响应式数据 name
watch(() => obj.name, () => {
  console.log('name响应式数据发生了变化!!!')
})

立马执行watch侦听器的回调函数

watch()侦听器的回调函数,默认是仅当数据源变化时,才会执行回调。如果需要一进入页面就执行一次回调函数,可以在watch()函数的第三个参数中添加:immediate: true

示例:

import { ref, reactive, watch } from 'vue'
    
interface Obj {
  name: string,
  age: number,
  like: Like
}

interface Like {
  first: string
  two: string
}
const obj = reactive<Obj>({
  name: '带土',
  age: 19,
  like: {
    first: 'jk',
    two: '嗨丝'
  }
})
// 侦听响应式数据 name
watch(
  obj, 
  () => {
  console.log('name响应式数据发生了变化!!!')
  },
  { 
    immediate: true 
  }
)

结果:

image.png

watchEffact()

watchEffact()立即执行传入的一个函数,不需要指定 immediate: true,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数,在函数中用到几个就监听几个。

示例:

<input type="text" v-model="message" />
<input type="text" v-model="message2" />
import { ref, watchEffect } from 'vue'

const message = ref('message')
const message2 = ref('message2')

// 侦听器
watchEffect(() => {
  console.log('message====>', message.value)
})

image.png

watchEffact()如果侦听一个嵌套数据结构ref声明的响应式中的几个属性不用添加{ deep: true }watchEffact()只跟踪回调中被使用到的属性。

示例:

<input type="text" v-model="refObj.children.children.name">
const refObj = ref({
  name: '张三',
  age: 20,
  children: {
    children: {
      name: '李四'
    }
  }
})


watchEffect(() => {
  console.log('refObj.value.name====>', refObj.value.children.children.name)
})

image.png

回调函数的触发时机

默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。

如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,你需要指明 flush: 'post' 选项。

<div id="app">我是dom</div>
watchEffect(
    () => {
      let app = document.querySelector('#app')
      console.log('dom=====>', app)
    }, 
    { flush: 'post' }
)

image.png

停止侦听器

如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏,例如:

import { watchEffect } from 'vue' 
// 它会自动停止 
watchEffect(() => {}) 
// ...这个则不会! 
setTimeout(() => { watchEffect(() => {}) }, 100)

要手动停止一个侦听器,请调用 watch 或 watchEffect 返回的函数,示例:

const stop = watchEffect(() => {}) 
// 该侦听器不再需要时调用
stop()

五、生命周期

组件基础

每一个.vue文件都可以充当组件来使用,每一个组件都可以复用。

setup语法糖中,是没有 beforeCreate组件创建之前和created组件创建完毕这两个生命周期函数的,setup语法糖代替了这两个生命周期函数。

onBeforeMount()

onBeforeMount():组件挂载到页面之前,组件已经完成了其响应式状态的设置,但还没有创建 DOM 节点

示例:

<template>
    <div id="div">{{ content }}</div>
</template>
<script setup lang='ts'>
import { ref, onBeforeMount } from 'vue'
/**
 * 在vue3的`setup`语法糖中,setup代替了`beforeCreate`和`created`两个声明周期函数
 */
const content = ref('宇智波带土')

// 获取dom节点
const div = ref<HTMLDivElement>()

// onBeforeMuont:组件挂载到页面之前,此时还获取不到页面中的dom节点
onBeforeMount(() => {
  console.log('onBeforeMount===>', div.value)
})
</script>

结果:

image.png

onMounted()

onMounted():组件在页面中挂载渲染完毕,此时可以获取到页面组件中的DOM节点

组件在以下情况下被视为已挂载:

  • 所有同步子组件都已经被挂载 (不包含异步组件或 <Suspense> 树内的组件)。
  • 自身的 DOM 树已经创建完成并插入了父容器中。注意仅当根容器在文档中时,才可以保证组件 DOM 树也在文档中。
<template>
    <div ref="div">{{ content }}</div>
</template>
<script setup lang='ts'>
import { ref, onBeforeMount, onMounted } from 'vue'
/**
 * 在vue3的`setup`语法糖中,setup代替了`beforeCreate`和`created`两个声明周期函数
 */
const content = ref('宇智波带土')

// 获取dom节点
const div = ref<HTMLDivElement>()

// onMounted:组件在页面中挂载完毕,此时可以获取到页面中的DOM节点
onMounted(() => {
  console.log('onMounted===>', div.value)
})
</script>

结果:

image.png

onBeforeUpdate()

onBeforeUpdate():在组件即将因为响应式状态变更而更新其 DOM 树之前调用,此时还拿不到更新后的dom,拿到的还是更新前的dom,这个钩子可以用来在 Vue 更新 DOM 之前访问 DOM 状态。

<template>
    <div ref="div">{{ content }}</div>
    <button @click="updateContent">修改响应式状态</button>
</template>
<script setup lang='ts'>
import { ref, onBeforeMount, onMounted, onBeforeUpdate } from 'vue'
/**
 * 在vue3的`setup`语法糖中,setup代替了`beforeCreate`和`created`两个声明周期函数
 */
const content = ref('宇智波带土')

// 获取dom节点
const div = ref<HTMLDivElement>()

// 修改content的值
function updateContent() {
  content.value = '野原琳'
}

// onBeforeUpdate:在组件即将`因为响应式状态变更而更新其 DOM 树之前调用`,此时还拿不到更新后的dom里面的内容,拿到的还是更新前的dom里面的内容
onBeforeUpdate(() => {
  console.log('onBeforeUpdate====>', div.value?.innerText)
})
</script>

点击修改响应式状态按钮后的结果:

image.png

onUpdated()

onUpdated():在组件因为响应式状态变更而更新其 DOM 树之后调用,这个钩子会在组件的任意 DOM 更新后被调用,在此可以获取到更新后的dom里面的内容。

注意:父组件的更新钩子将在其子组件的更新钩子之后调用

如果你需要在某个特定的状态更改后访问更新后的 DOM,请使用 nextTick() 作为替代。

<template>
    <div ref="div">{{ content }}</div>
    <button @click="updateContent">修改响应式状态</button>
</template>
<script setup lang='ts'>
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated } from 'vue'
/**
 * 在vue3的`setup`语法糖中,setup代替了`beforeCreate`和`created`两个声明周期函数
 */
const content = ref('宇智波带土')

// 获取dom节点
const div = ref<HTMLDivElement>()

// 修改content的值
function updateContent() {
  content.value = '野原琳'
}

// onUpdated:在组件因为响应式状态变更而更新其 DOM 树之后调用,在此可以获取到更新后的dom里面的内容
onUpdated(() => {
  console.log('onUpdated====>', div.value?.innerText)
})
</script>

点击修改响应式状态按钮后的结果:

image.png

onBeforeUnmount()

onBeforeUnmount():在组件实例被卸载之前调用,当这个钩子被调用时,组件实例依然还保有全部的功能,组件中的响应式状态和方法都还可用。

父组件:

<template>
  <div>
    <Son v-if="isShow" />
    <button @click="isShow = !isShow">切换子组件显示隐藏的状态</button>
  </div>
</template>
import { ref } from 'vue'
// 控制子组件是否显示
const isShow = ref<boolean>(true)

子组件:

<template>
  <div>
    <div ref="div">{{ content }}</div>
    <button @click="updateContent">修改响应式状态</button>
  </div>
</template>
<script setup lang='ts'>
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount } from 'vue'
/**
 * 在vue3的`setup`语法糖中,setup代替了`beforeCreate`和`created`两个声明周期函数
 */
const content = ref('宇智波带土')

// 获取dom节点
const div = ref<HTMLDivElement>()

// onBeforeUnmount:在组件被现在之前被调用一次,组件实例依然还保有全部的功能,组件中的响应式状态和方法都还可用。
onBeforeUnmount(() => {
  console.log('onBeforeUnmount====>组件卸载之前', '组件的响应式状态:' + content.value, '拿到的DOM节点:', div.value)
})

</script>

结果:

image.png

onUnmounted()

onUnmounted():在组件实例被卸载之后调用一次,可以在这个钩子中手动清理一些副作用,例如计时器DOM 事件监听器或者与服务器的连接

一个组件在以下情况下被视为已卸载:

  • 其所有子组件都已经被卸载
  • 所有相关的响应式作用 (渲染作用以及 setup() 时创建的计算属性侦听器) 都已经停止。 父组件:
<template>
  <div>
    <Son v-if="isShow" />
    <button @click="isShow = !isShow">切换子组件显示隐藏的状态</button>
  </div>
</template>
import { ref } from 'vue'
// 控制子组件是否显示
const isShow = ref<boolean>(true)

子组件:

<template>
  <div>
    <div ref="div">{{ content }}</div>
    <button @click="updateContent">修改响应式状态</button>
  </div>
</template>
<script setup lang='ts'>
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount } from 'vue'
/**
 * 在vue3的`setup`语法糖中,setup代替了`beforeCreate`和`created`两个声明周期函数
 */
const content = ref('宇智波带土')

// 获取dom节点
const div = ref<HTMLDivElement>()

// onUnmounted:在组件实例被卸载之后调用
onUnmounted(() => {
  console.log('onUnmounted====>组件卸载之前', '组件的响应式状态:' + content.value, '拿到的DOM节点:', div.value)
})

</script>

结果:

image.png

六、BEM架构+layout布局

BEM架构

BEM 是一个分层系统,它把我们的网站分为三层,这三层正好对应着 BEM 三个英文单词的简写 block, element, modifier,分为 块层、元素层、修饰符层

Block:您希望将样式范围限定到的 HTML 块

.block {
        
 }

Element:该块内的任何元素,名称与您的块名称隔开

.block__elementOne {
    
 } 
    
.block__elementTwo {
    
}

Modifier:用于修改现有元素样式的标志,无需创建单独的 CSS 类

.block__elementOne--modifier {

}
<section class="block"> 
    <p class="block__elementOne">This is an element inside a block.</p> 
    <p class="block__elementOne block__elementOne--modifier">This is an element inside a block, with a modifier.</p> 
</section>
  • 使用__两个下划线将块名称与元素名称分开
  • 使用--两个破折号分隔元素名称及其修饰符
  • 一切样式都是一个类,不能嵌套

layout布局

BEM样式文件

./src/style/bem.scss

/** 
* bem架构,块、元素、修饰
*/
// 声明变量

// 块名称,!default:表示没有被重新赋值,就是用'block'这个值。
$namespace: 'dt' !default;
// 命名空间连接块元素使用'-'
$block-select: '-' !default;
// 块连接元素使用'__'
$element-select: '__' !default;
// 元素连接修饰使用'--'
$modify-select: '--' !default;

// 块的混入,传进来的参数,块的名字
// #{},插值表达式,可以在括号里使用变量
// @content:样式内容,.dt-main { color: #000000; font-size: 14px; }
// color、font-size会替换@content,@content相当于是样式的占位符
@mixin block ($block) {
  $B: #{$namespace + $block-select + $block};
  .#{$B} {
    @content;
  }
}

// 元素
@mixin element ($element) {
  $parent: &;
  $E: #{$parent + $element-select + $element};
  @at-root {
    #{$E} {
      @content
    }
  }
}

//  修饰
@mixin modify ($modify) {
  $parent: &;
  $M: #{ $parent + $modify-select + $modify };
  @at-root {
    #{$M} {
      @content;
    }
  }
}

// bfc
@mixin bfc {
  height: 100%;
  overflow: hidden;
}

vite配置文件:

添加一个全局样式表 ./vite.config.js

export default defineConfig({
  plugins: [vue(), vueJsx()],
  css: {
    // css 预处理器的配置
    preprocessorOptions: {
      // scss预处理器的配置
      scss: {
        // 增加一个全局的样式表
        additionalData: `@import "./src/style/bem.scss";`
      }
    }
  },
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

组件: ./src/App.vue

<template>
  <div id="app">
    <Layout />
  </div>
</template>

<script setup lang='ts'>
import Layout from './components/layout/index.vue'
</script>

<style lang='scss' scoped></style>

./src/components/layout/index.vue

<template>
  <div class="dt-layout">
    <div class="dt-layout__sider">
      <Sider />
    </div>
    <div class="dt-layout__content">
      <Header />
      <Main />
    </div>
  </div>
</template>

<script setup lang='ts'>
import Header from './Header/index.vue'
import Main from './Main/index.vue'
import Sider from './Sider/index.vue'
</script>

<style lang='scss' scoped>
@include block (layout) {
  display: flex;
  height: 100%;

  @include element (sider) {
    width: 200px;

  }

  @include element (content) {
    display: flex;
    flex: 1;
    flex-direction: column;
  }
}
</style>

./src/components/layout/sider/index.vue

<template>
  <div class="dt-sider">
    <h1>Sider</h1>
  </div>
</template>

<script setup lang='ts'>
</script>

<style lang='scss' scoped>
@include block (sider) {
  height: 100%;
  border-right: solid 1px #ccc;
}
</style>

./src/components/layout/header/index.vue

<template>
  <div class="dt-header">
    <h1>Header</h1>
  </div>
</template>

<script setup lang='ts'>
</script>

<style lang='scss' scoped>
@include block (header) {
  height: 50px;
  border-bottom: solid 1px #ccc;
}
</style>

./src/components/main/index.vue

<template>
  <div class="dt-main">
    <div class="dt-main__items" v-for="item in 100" :key="item">{{ item }}</div>
  </div>
</template>

<script setup lang='ts'>
</script>

<style lang='scss' scoped>
@include block (main) {
  flex: 1;
  overflow: auto;

  @include element (items) {
    padding: 15px;
    margin: 15px;
    border: 1px solid #ccc;
    border-radius: 5px;
  }
}
</style>

七、父子组件传值

父组件传值给子组件

父组件传值给子组件通过子组件声明props,父组件绑定并且传值给子组件声明的props

子组件声明Prpos

声明Props可以通过defineProps()宏函数进行定义,它支持传入一个配置对象以及支持给定一个泛型,它返回一个对象,其中包含了定义用来接收父组件啊传过来的值得所有Props。

import { definedProps } from 'vue'

const props = defineProps<T>({
    // 定义接收父组件传值的Prop
})
    
console.log(props) // { // 定义接收父组件传值的Prop }

父组件绑定子组件声明的Props并传值

父组件在子组件标签上使用:(v-bind)指令绑定子组件声明的Props并传值:

<template>
    <Son :声明的Props="值" />
</template>

示例:

在不使用ts的情况下,我们可以这样:

子组件

<template>
  <div class="son">
    <h1>son</h1>
    <div>{{ title }}</div>
  </div>
</template>
<script setup>
import { defineProps } from 'vue'

defineProps({
  title: {
    type: String,
    default: 'title默认值'
  }
})
</script>

父组件

<template>
  <div class="father">
    <h1>father</h1>
    <Son :title="'父组件传过来的title'" />
  </div>
</template>
<script setup>
import Son from './Son.vue';
</script>

template模板中使用props直接拿我们定义的名称来用即可,那在script标签中怎么获取呢?

上面说到过,defineProps()宏函数有一个返回值,是一个对象,而这个对象里面,就是我们定义的用来接收父组件传过来的值的props,所以,在script标签中,我们可以通过这个返回值进而获取到定义的props

子组件:

<template>
  <div class="son">
    <h1>son</h1>
    <div>{{ title }}</div>
  </div>
</template>
<script setup lang='ts'>
import { defineProps } from 'vue'

const props = defineProps({
  title: {
    type: String,
    default: 'title默认值'
  }
})

console.log(props.title) // 父组件传过来的title
</script>

父组件:

<template>
  <div class="father">
    <h1>father</h1>
    <Son :title="'父组件传过来的title'" />
  </div>
</template>
<script setup lang='ts'>
import Son from './Son.vue';
</script>

使用ts的情况下,使用defineProps()宏函数定义props:

defineProps()宏函数可以传入一个泛型,所以,我们在使用ts的情况下可以这么定义props

  1. 定义一个接口interface
  2. 将这个接口作为defineProps()的泛型
<script setup lang='ts'>
    // 定义接口
    interface Props {
        name: string
        age: number
    }
    // 定义props
    const props = defineProps<Props>()
    
    console.log(props) // { name: undefined, age: undefined }
</script>

但是用泛型的方式有个缺点,就是不能直接给props默认值。

Props的默认值

不是用ts泛型版:

<script setup lang='ts'>
    import { defineProps } from 'vue'

    const props = defineProps({
        // 基本类型
        title: {
            type: String,
            default: '标题'
        },
        // 引用类型
        obj: {
            type: Object,
            default: () => ({})
        }
    })
</script>

使用ts泛型版:

使用泛型的方式定义props,需要指定默认值时,需要将整个defineProps<T>()当做参数传给withDefaults()宏函数。

<script setup lang='ts'>
    import { defineProps, widthDefaults } from 'vue'

    interface Props {
        name: string
        age: number
    }
    const props = withDefault(defineProps<Props>(), { name: '张三', age: 18 })
</script>

子组件传值给父组件

子组件传值给父组件通过组件事件实现,子组件定义要触发的事件,通过defineEmits()宏函数来定义要触发的事件:

ts版:

xxx.d.ts:

export interface UserInfo {
    name: string
    age: number
}

子组件:

<script setup lang='ts'>
    import { defineEmits } from 'vue'
    import type { UserInfo } from 'xxx.d.ts'
    // 定义要触发的事件
    interface Emits {
        (e: '要触发的事件名', '传给父组件的值': 类型): void
        (e: 'set-userinfo', userinfo: UserInfo): void
    }
    const emits = defineEmits<Emits>()
    // 触发事件
    emits('set-userinfo', { name: '张三', age: 18 })
</script>

父组件:

<template>
  <div class="father">
    <h1>father</h1>
    <!-- 绑定事件 -->
    <Son @set-userinfo="getUserInfo" />
  </div>
</template>
<script setup lang='ts'>
import Son from './Son.vue';
import type { UserInfo } from 'xxx.d.ts'
// 获取子组件传过来的值
function getUserInfo (userinfo: UserInfo) {
    console.log(userinfo) // { name: '张三', age: 18 }
}
</script>

子组件向父组件暴露属性或方法

子组件向父组件暴露自身的属性或者方法,使用defineExpose()宏函数进行暴露:

子组件:

<script setup> 
    import { defineExpose, ref } from 'vue' 
    const name = '张三'
    const age = ref(18)
    const say = () => {
        console.log('hello!')
    }
    defineExpose({ name, age, say }) 
</script>

父组件:

父组件通过模板引用的方式获取到当前组件的实例,获取到子组件暴露出来的属性或者方法

<template>
  <div class="father">
    <h1>father</h1>
    <!-- <Son :title="'父组件传过来的title'" /> -->
    <Son ref="son" />
  </div>
</template>
<script setup lang='ts'>
    import Son from './Son.vue';
    import { ref } from 'vue'

    // 获取子组件实例
    const son = ref<InstanceType<typeof Son>>()
    // 拿到子组件的属性或方法
    console.log(son.value?.name)
    console.log(son.value?.age)
    son.value?.say()
</script>

八、组件

非路由组件建议放在components目录下,路由组件建议放在views目录下

全局组件

当在项目中出现评率比较高的业务组件,你可以把它封装成一个组件然后全局引入。

components目录下创建一个组件,然后在main.ts文件中引入,使用app.component('自定义组件名称', 全局注册的组件)进行全局注册组件,全局注册过后,在项目中的任意一个组件中无需引入就可以直接使用。

app:Vue的实例对象。

import { createApp } from 'vue'
// 引入组件
import Father from './components/Father.vue'

// 创建vue实例对象
const app = createApp(App)
    
// 全局注册组件
app.component('myFather', Father)

App组件中使用:

<template>
    <myFather />
</template>

局部组件

当一个页面内分好多模块,比如头部、侧边栏、内容区域...并且在项目中出现频率不是很高的话,这时我们可以将这些比如头部、侧边栏、内容区域等拆分成组件去局部引入。

创建一个组件,然后在需要用到这个组件的组件中引入直接使用即可。

例如我要将Father.vueApp.vue中局部引入使用:

image.png

App.vue:

<template>
    <Father />
</template>
<script>
    import Father from './components/Father.vue'
</script>

递归组件

递归组件:就是组件自身使用自身

示例:

index.d.ts:

export interface TreeData {
  title: string
  checked: boolean
  children?: Tree[]
}

App.vue:

<template>
    <Tree :tree-data="treeData" />
</template>
<script setup lang='ts'>
import { ref, reactive } from 'vue'
import type { TreeData } from '@/types'
import Tree from '@/components/Tree.vue'


// 树形结构
const treeData = reactive<TreeData[]>([
  {
    title: '1',
    checked: false
  },
  {
    title: '2',
    checked: false,
    children: [
      {
        title: '2-1',
        checked: true
      }
    ]
  },
  {
    title: '3',
    checked: false,
    children: [
      {
        title: '3-1',
        checked: true,
        children: [
          {
            title: '3-1-1',
            checked: false
          },
          {
            title: '3-1-2',
            checked: false
          }
        ]
      }
    ]
  }
])
</script>

Tree.vue:

<template>
  <div class="tree" v-for="item in treeData" :key="item.title">
    <input v-model="item.checked" type="checkbox"><span>{{ item.title }}</span>
    <Tree v-if="item.children?.length" :tree-data="item.children" />
  </div>
</template>
<script setup lang='ts'>
import { defineProps } from 'vue'
import type { TreeData } from '@/types/index'

export interface Props {
  treeData: TreeData[]
}

const props = defineProps<Props>()
</script>
<style lang='scss' scoped>
.tree {
  margin-left: 10px;
}
</style>

递归组件绑定事件

递归组件绑定事件会触发事件冒泡,就是比如说我触发的是子元素的点击事件,因为事件冒泡,父元素的点击事件也被触发。

解决方法:给事件加上.stop修饰符,阻止事件冒泡。

不加.stop示例:

<template>
  <div class="tree" @click="treeClickHandler(item)" v-for="item in treeData" :key="item.title">
    <input v-model="item.checked" type="checkbox"><span>{{ item.title }}</span>
    <Tree v-if="item.children?.length" :tree-data="item.children" />
  </div>
</template>
<script setup lang='ts'>
import { defineProps } from 'vue'
import type { TreeData } from '@/types/index'

export interface Props {
  treeData: TreeData[]
}

const props = defineProps<Props>()
    
function treeClickHandler (treeNode: TreeData) {
    console.log(treeNode)
}
</script>

image.png

加上.stop示例:

<template>
  <div class="tree" @click.stop="treeClickHandler(item)" v-for="item in treeData" :key="item.title">
    <input v-model="item.checked" type="checkbox"><span>{{ item.title }}</span>
    <Tree v-if="item.children?.length" :tree-data="item.children" />
  </div>
</template>
<script setup lang='ts'>
import { defineProps } from 'vue'
import type { TreeData } from '@/types/index'

export interface Props {
  treeData: TreeData[]
}

const props = defineProps<Props>()
    
function treeClickHandler (treeNode: TreeData) {
    console.log(treeNode)
}
</script>

image.png

九、动态组件

动态组件就是:让多个组件使用同一个挂载点,并动态切换,这就是动态组件。

挂载点就是<Compoent />标签,使用v-bind / :绑定<Component>标签的is属性,实现动态切换组件。

is属性可以接收一个字符串或者是一个组件

示例:

<template>
  <div>
    <ul class="tabs">
      <li class="tabs-item" :class="[activeIndex === index ? 'tabs-item--active' : '']" v-for="(item, index) in tabsList"
        :key="item.name" @click="changeComponent(index, item.component)">{{ item.name }}</li>
    </ul>
    <component :is="componentId"></component>
  </div>
</template>
<script setup lang='ts'>
import { reactive, ref, type Component } from 'vue'
import A from './A.vue';
import B from './B.vue';
import C from './C.vue';

// 显示的组件,默认为A组件
const componentId = ref<Component>(A)

// 默认显示的tab页下标
const activeIndex = ref(0)

// 循环的tabs列表
const tabsList = reactive([
  {
    name: 'AVue',
    component: A
  },
  {
    name: 'BVue',
    component: B
  },
  {
    name: 'CVue',
    component: C
  }
])

// 切换tab页,切换组件
const changeComponent = (index: number, component: Component) => {
  activeIndex.value = index
  componentId.value = component
}
</script>
<style lang='scss' scoped>
.tabs {
  display: flex;
  list-style: none;

  .tabs-item {
    padding: 10px;
    margin-right: 15px;
    border: solid 1px #5f5e5e;
    cursor: pointer;
  }

  .tabs-item--active {
    background-color: skyblue;
  }
}
</style>

效果:

动态组件式.gif

性能优化

image.png 上图的警告意思是:我们收到了一个组件,它被做成了一个响应对象。这可能会导致不必要的性能开销,应该通过将组件标记为' markRaw '或使用' shallowRef '而不是' ref '来避免。 组件被作为响应式对象,意味着组件里面的属性也会被劫持,但是这是没有必要的,所以Vue给出了这个警告。

image.png 就是上图的组件中的属性没有必要去做一个劫持。

Vue提示我们可以使用 shallowRef()代替ref(),因为shallowRef()只代理到.value,更深层次的属性是不会进行代理的。

如果组件作为一个对象的属性值,那么Vue提示我们使用markRaw()将组件包裹起来,被markRaw()包裹的属性会被加上一个__v_skip:true,当reactive碰到这个属性,它就会跳过这个Proxy代理。

优化的代码:

<script setup lang="ts">
import { reactive, shallowRef, ref, markRaw, type Component } from 'vue'
// 显示的组件,默认为A组件
const componentId = shallowRef<Component>(A)


// 循环的tabs列表
const tabsList = reactive([
  {
    name: 'AVue',
    component: markRaw(A)
  },
  {
    name: 'BVue',
    component: markRaw(B)
  },
  {
    name: 'CVue',
    component: markRaw(C)
  }
])
</script>

十、插槽(slot)

插槽就是子组件提供给父组件使用的一个占位符,用<slot></slot>,父组件可以在这个占位符中填充任何模板代码,如html组件等,填充的内容会替换子组件的<slot></slot>标签。

匿名插槽

在子组件中放置一个插槽: Children.vue:

<template>
  <div>
    <slot></slot>
  </div>
</template>

![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/21e0ac345f67401d9724700ebda2aff9~tplv-k3u1fbpfcp-watermark.image?)

在父组件中使用插槽,并给这个插槽填充内容: Father.vue:

<template>
  <div>
    <Children>
      <template v-slot>
        123123123
      </template>
    </Children>
  </div>
</template>
<script setup lang='ts'>
import Children from './Children.vue';
</script>

具名插槽

具名插槽其实就是给插槽取个名字。一个子组件可以放多个插槽,而且可以放在不同的地方,而父组件填充内容时,可以根据这个名字把内容填充到对应插槽中。

通过v-slot:插槽名称,将内容填充到子组件的指定插槽中。 在子组件中放置一个插槽,并给定一个name属性,属性值自定义: Children.vue:

<template>
  <div>
    <slot name="content"></slot>
  </div>
</template>

在父组件中使用插槽,绑定指定插槽slot:name属性值,并给这个插槽填充内容: Father.vue:

<template>
  <div>
    <Children>
      <template v-slot:content>
        123123123
      </template>
    </Children>
  </div>
</template>
<script setup lang='ts'>
import Children from './Children.vue';
</script>

插槽简写

v-slot:插槽名称可以简写为#插槽名称

在子组件中放置一个插槽,并给定一个name属性,值自定义: Children.vue:

<template>
  <div>
    <slot name="content"></slot>
  </div>
</template>

在父组件中使用插槽,指定插槽:slot:name属性值/#name属性值,并给这个插槽填充内容: Father.vue:

<template>
  <div>
    <Children>
      <template #content>
        123123123
      </template>
    </Children>
  </div>
</template>
<script setup lang='ts'>
import Children from './Children.vue';
</script>

作用域插槽

在子组件使用v-bind/:指令动态绑定参数,参数名自定义,然后将要给父组件的slot使用的数据赋值给这个参数,

派发给父组件的slot去使用,在父组件slot指令后面接收的是一个对象,传过来的数据(自定义参数)就在这个对象中,所以我们可以把它解构出来进行使用:

在子组件中放置一个插槽,并绑定一个数据: Children.vue:

<template>
  <div>
    <slot name="content" :data="obj"></slot>
  </div>
</template>
<script setup lang='ts'>
import { reactive } from 'vue'

export interface Obj {
  name: string
  slot: string
}

const obj = reactive<Obj>({
  name: 'Children',
  slot: 'content'
})
</script>

在父组件中使用插槽,并将子组件传过来的数据填充到这个插槽中: Father.vue:

<template>
  <div>
    <Children>
      <template v-slot:content="{ data }">
        {{ data }}
      </template>
    </Children>
  </div>
</template>
<script setup lang='ts'>
import Children from './Children.vue';
</script>

动态插槽

插槽可以是一个动态的 在子组件中放置一个插槽,并绑定一个数据: Children.vue:

<template>
  <div>
    <slot name="content" :data="obj"></slot>
  </div>
</template>
<script setup lang='ts'>
import { reactive } from 'vue'

export interface Obj {
  name: string
  slot: string
}

const obj = reactive<Obj>({
  name: 'Children',
  slot: 'content'
})
</script>

在父组件中使用插槽,并将子组件传过来的数据填充到这个插槽中: Father.vue:

<template>
  <div>
    <Children>
      <template v-slot:[slotName]="{ data }">
        {{ data }}
      </template>
    </Children>
  </div>
</template>
<script setup lang='ts'>
<script setup lang='ts'>
import { ref } from 'vue'
import Children from './Children.vue';

const slotName = ref('content')
</script>

</script>

十一、异步组件

异步组件的应用场景

就想element-plus组件库的骨架屏组件, 比如我们有一个数据请求,那在请求之前我们可以先展示一个骨架屏,数据请求完成了再去渲染请求回来的真实数据。

骨架屏.gif

定义一个异步组件

要想组件成为一个异步组件,我们需要使用到顶层await技术,使用 <script setup> 时有顶层 await 表达式的组件。

Async.vue:

<template>
  <div>

  </div>
</template>

<script setup lang='ts'>
// 在setup语法糖中使用了顶层await技术,也称为异步组件
// 模拟一个请求
const { data } = await getDataAPI()
</script>

<style lang='scss' scoped>

</style>

此时Async.vue就是一个异步组件,因为在这个组件中使用到了顶层await技术 。

如何使用异步组件

异步组件与普通组件的使用方法有些区别,普通组件直接引入就可以使用了,但异步组件不是,使用异步组件首先就是引入,然后使用Vue自带的defineAsyncComponent()宏函数进行定义,

defineAsyncComponent()

defineAsyncComponent()用来定义一个异步组件,返回的类型是一个组件Component defineAsyncComponent()有两种书写方式:

  1. 直接传一个回调函数:
    <script setup lang="ts">
    import { defineAsyncComponent } from 'vue'
    
    const AsyncVue = defineAsyncComponent(() => import('异步组件的路径'))
    </script>
    
    import()函数模式是可以写在代码逻辑里面的,它返回一个 Promise,所以多数情况下会将它和 defineAsyncComponent 搭配使用。

使用/展示组件

使用/展示异步组件必须使用Suspense

Suspense

Suspense是Vue新增的一个内置组件,它默认提供两个插槽:

  1. 第一个插槽的名字为default,是给需要展示的异步组件的一个占位。
  2. 第二个插槽的名字为fallback,是给加载时候展示的组件的一个占位。

示例:

mock数据:public/data.json:

{
  "data": {
    "avatar": "https://img1.baidu.com/it/u=1960110688,1786190632&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=281",
    "info": "哒哒哒哒哒哒哒哒哒哒哒哒哒哒",
    "description": "哒哒哒哒哒哒哒哒哒哒哒哒哒哒哒哒哒哒哒哒哒哒哒哒哒哒哒哒"
  }
}

请求工具:utils/server.ts:

export const axios = {
  get <T>(url: string): Promise<T> {
    return new Promise((resolve) => {
      const xhr = new XMLHttpRequest()

      xhr.open('GET', url)

      xhr.onreadystatechange = () => {
        if (xhr.readyState === 4 && xhr.status === 200) {
          setTimeout(() => {
            resolve(JSON.parse(xhr.responseText))
          }, 2000)
        }
      }
      xhr.send(null)
    })
  }
}

骨架屏组件:asyncComponent/Skeleton.vue:

<template>
  <div class="skeleton">
    <div class="skeleton-avatar"></div>
    <div class="skeleton-info"></div>
    <div class="skeleton-description"></div>
  </div>
</template>

<script setup lang='ts'>
</script>

<style lang='scss' scoped>
.skeleton {
  display: inline-block;
  padding: 20px;
  border: solid 1px #eeeeee;
  border-radius: 5px;

  .skeleton-avatar {
    width: 50px;
    height: 50px;
    border-radius: 50%;
    background-color: #eeeeee;
  }

  .skeleton-info {
    width: 250px;
    height: 20px;
    margin: 20px 0;
    background-color: #eeeeee;
  }

  .skeleton-description {
    width: 250px;
    height: 50px;
    background-color: #eeeeee;
  }
}
</style>

异步组件:asyncComponent/Async.vue:

<template>
  <div class="skeleton">
    <div class="skeleton-avatar">
      <img :src="data.avatar" alt="">
    </div>
    <div class="skeleton-info">{{ data.info }}</div>
    <div class="skeleton-description">{{ data.description }}</div>
  </div>
</template>

<script setup lang='ts'>
import { axios } from '@/utils/server'
// 返回值类型
interface Response {
  data: {
    avatar: string
    info: string
    description: string
  }
}
// 获取数据
const { data } = await axios.get<Response>('./data.json')
</script>

<style lang='scss' scoped>
.skeleton {
  display: inline-block;
  padding: 20px;
  border: solid 1px #eeeeee;
  border-radius: 5px;

  .skeleton-avatar {
    width: 50px;
    height: 50px;
    border-radius: 50%;
    overflow: hidden;
  }

  .skeleton-info {
    width: 250px;
    height: 20px;
    margin: 20px 0;
  }

  .skeleton-description {
    width: 250px;
    height: 50px;
  }
}
</style>

父组件:asyncComponent/index.vue:

<template>
  <Suspense>
    <template #default>
      <Async />
    </template>
    <template #fallback>
      <Skeleton />
    </template>
  </Suspense>
</template>

<script setup lang='ts'>
import Skeleton from './Skeleton.vue';
import { defineAsyncComponent } from 'vue'

const Async = defineAsyncComponent(() => import('@/components/asyncComponent/AS.vue'))
</script>

<style lang='scss' scoped></style>

效果:

异步组件.gif

代码分包

凡是通过import()函数模式引入的东西,都会做一个代码拆解。

在我们打包的时候,会输出一个dist文件夹,默认情况下,会把所有东西放到一个js文件里面,如果说找一个js体积比较大,那么页面首次加载的时候,白屏的时间就会特别长,所以这个时候我们可以使用import()函数模式,将没有用到的包拆出来,后面用到了再去加载,这样首屏加载就会快一些。

十二、Teleport

<Teleport></Teleport>:它可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的指定位置去。

Teleport
属性描述
to支持所有Css选择器,将<Teleport></Teleport>包裹的模板内容传送到指定的css选择器对应的元素内
disabled该属性接收一个布尔值,如果为true<Teleport></Teleport>将不把包裹的模板内容传送,如果为false<Teleport></Teleport>将把包裹的模板内容传送到指定元素内

示例: 创建一个对话框组件,在一个组件中引入对话框组件,这个组件设置相对定位,让对话框相对于body绝对定位。

Dialog.vue组件:

<template>
  <div class="dialog">
    <div class="dialog-header">
      title
    </div>
    <div class="dialog-body">
      body
    </div>
    <div class="dialog-footer">
      footer
    </div>
  </div>
</template>

<script setup lang='ts'>
</script>

<style lang='scss' scoped>
.dialog {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 400px;
  height: 450px;
  background-color: skyblue;
  color: #ffffff;

  .dialog-header {
    height: 45px;
    line-height: 45px;
    border-bottom: solid 1px #efefef;
    text-align: center;
  }

  .dialog-body {
    flex: 1;
    display: flex;
    justify-content: center;
    align-items: center;
  }

  .dialog-footer {
    height: 45px;
    line-height: 45px;
    border-top: solid 1px #ffffff;
    text-align: center;
  }
}
</style>

Dialog.vue的父组件

<template>
  <div class="father">
    <Dialog />
  </div>
</template>

<script setup lang='ts'>
import Dialog from './Dialog.vue';
</script>

<style lang='scss' scoped>
.father {
  height: 50vh;
  background-color: yellow;
}
</style>

没有给对话框Dialog.vue父组件加相对定位时:

image.png 此时因为没有任何父组件添加了定位,所以Dialog.vue组件会相对于body进行绝对定位。

给对话框Dialog.vue父组件加相对定位后: Dialog.vue的父组件的样式:

<style lang='scss' scoped>
.father {
  position: relative;
  height: 50vh;
  background-color: yellow;
}
</style>

image.png 可以看见,给Dialog.vue的父组件加上相对定位后,Dialog.vue组件会相对于其父组件进行绝对定位。

Dialog.vue组件包裹上<Teleport></Teleport>,并指定传送到body时: Dialog.vue的父组件的模板:

<template>
  <div class="father">
    <Teleport to="body">
      <Dialog />
    </Teleport>
  </div>
</template>

image.png 可以看到,Dialog.vue组件又相对于body进行绝对定位。

设置Teleportdisabled属性为true时:

<template>
  <div class="father">
    <Teleport to="body" :disable="true">
      <Dialog />
    </Teleport>
  </div>
</template>

image.png 可以看见,Dialog.vue组件又相对于他的父组件进行绝对定位,因为。

十二、keep-alive缓存组件

<KeepAlive> 是一个内置组件,当我们不希望在切换组件页面时,组件被销毁,页面重新渲。或者考虑性能问题避免多次重复渲染,而是希望把组件缓存起来,保持当前的状态,这时候就需要使用<KeepAlive> 组件。

<KeepAlive>组件会缓存它包裹起来的组件。

示例: 使用<KeepAlive>之前: A组件:

<template>
  <div>
    <label for="A">
      组件A:
    </label>
    <input id="A" type="text" v-model="value">
  </div>
</template>

<script setup lang='ts'>
import { ref } from 'vue'
const value = ref('')
</script>

<style lang='scss' scoped></style>

B组件:

<template>
  <div>
    <label for="B">
      组件B:
    </label>
    <input id="B" type="text" v-model="value1">
  </div>
</template>

<script setup lang='ts'>
import { ref } from 'vue'
const value1 = ref('')
</script>

<style lang='scss' scoped></style>

父组件:

<template>
  <div>
    <button @click="show = !show">切换组件</button>
    <A v-if="show" />
    <B v-else />
  </div>
</template>

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

<style lang='scss' scoped></style>

keep之前.gif 使用<KeepAlive>之后:

父组件:

<template>
  <div>
    <button @click="show = !show">切换组件</button>
    <keep-alive>
      <A v-if="show" />
      <B v-else />
    </keep-alive>
  </div>
</template>

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

<style lang='scss' scoped></style>

keep之后.gif

include属性

include属性,表示需要KeepAlive组件缓存的组件,将需要缓存起来的组件赋值给include属性,include可以接收字符串正则数组,加上以后,只有include属性包含的组件才会被缓存起来。

示例: 只缓存A组件:

父组件:

<template>
  <div>
    <button @click="show = !show">切换组件</button>
    <keep-alive :include="['A']">
      <A v-if="show" />
      <B v-else />
    </keep-alive>
  </div>
</template>

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

<style lang='scss' scoped></style>

include后.gif 可以看见include中只只包含了A组件,所以B组件的数据没有被缓存。

exclude

exclude,表示不需要KeepAlive组件缓存的组件,将不需要缓存的组件赋值给exclude属性,exclude可以接收字符串正则数组,加上以后,只有exclude属性包含的组件就不会被缓存起来。 示例: 不缓存A组件:

父组件:

<template>
  <div>
    <button @click="show = !show">切换组件</button>
    <keep-alive exclude="A">
      <A v-if="show" />
      <B v-else />
    </keep-alive>
  </div>
</template>

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

<style lang='scss' scoped></style>

exclude.gif 可以看到,exclude的值是A,表示不缓存A组件,所以A组件的数据不会被缓存。

max

max指定KeepAlive组件缓存组件的最大数量,被KeepAlive组件包裹的组件超过max的值时,优先缓存活跃的、新的组件不活跃的、旧的组件会被剔除。

被缓存组件新增的两个生命周期

KeepAlive包裹的组件会新增两个生命周期,一个是onActivated,缓存的组件被激活时触发,另一个是onDeactivated离开缓存的组件时触发,离开被缓存的组件时,不会触发onUnmounted,因为组件被缓存起来了,而不是被销毁了,所以触发的是onDeactivated

生命周期函数.gif A组件:

<template>
  <div>
    <label for="A">
      组件A:
    </label>
    <input id="A" type="text" v-model="value">
  </div>
</template>

<script setup lang='ts'>
import { onActivated, onDeactivated, ref } from 'vue'
const value = ref('')

onActivated(() => {
  console.log('A组件被激活啦!')
})

onDeactivated(() => {
  console.log('离开A组件咯!')
})
    
onUnmounted(() =>{
    console.log('A组件销毁咯!')
}
</script>

<style lang='scss' scoped></style>

十三、transition动画组件

transition组件基本使用

Vue提供了transition内置组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡。

  • 条件渲染(使用v-if)
  • 条件展示(使用v-show)
  • 动态组件
  • 组件根节点 自定义transition过渡效果,你需要对transition组件的name属性自定义,并在css中写入对应的样式。

过渡的类名

在进入/离开的过渡中,会有6个类切换。

  1. v-enter-from定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入下一帧移除。

  2. v-enter-active定义过渡生效时的状态。在整个进入过渡的阶段中应用,在元素在元素被插入之前生效,在过渡/动画完成后移除。这个类可以被用来定义进入过渡的过程时间、延迟和曲线函数。

  3. v-enter-to定义过渡的结束状态。在元素被插入之后下一帧生效(与此同时v-enter-from被移除),在过渡/动画完成之后移除。

  4. v-leave-from:定义离开过渡的开始状态。在离开过渡触发时立刻生效,下一帧被移除。

  5. v-leave-active定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立即生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间、延迟和曲线函数。

  6. v-leave-to离开过渡的结束状态。在离开过渡被触发之后下一帧生效(与此同时,v-leavel-from被移除),在过渡/动画完成之后移除。

我们可以给 <Transition> 组件传一个 name prop 来声明一个过渡效果名,对于一个有名字的过渡效果,对它起作用的过渡 class 会以其名字而不是 v 作为前缀:

示例:

<template>
  <div class="transition">
    <transition name="fade">
      <div v-if="flag" class="transition-box"></div>
    </transition>
    <button @click="flag = !flag">switch</button>
  </div>
</template>

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

<style lang='scss' scoped>
.transition-box {
  width: 200px;
  height: 200px;
  background-color: skyblue;
}

// 元素渲染之前的状态
.fade-enter-from {
  width: 0;
  height: 0;
}

// 元素进入时的过渡效果
.fade-enter-active {
  transition: width 3s, height 3s;
}

// 元素渲染完毕后的状态
.fade-enter-to {
  width: 200px;
  height: 200px;
}

// 元素离开之前的状态
.fade-leave-from {
  width: 200px;
  height: 200px;
}

// 元素离开时的效果
.fade-leave-active {
  transition: width 3s, height 3s;
}

// 元素离开之后的状态
.fade-leave-active {
  width: 0;
  height: 0;
}
</style>

效果展示:

transition.gif

animation动画

对于大多数的 CSS 动画,我们可以简单地在 *-enter-active 和 *-leave-active class 下声明它们。

<Transition name="bounce">
  <p v-if="show" style="text-align: center;">
    Hello here is some bouncy text!
  </p>
</Transition>

css

.bounce-enter-active {
  animation: bounce-in 0.5s;
}
.bounce-leave-active {
  animation: bounce-in 0.5s reverse;
}
@keyframes bounce-in {
  0% {
    transform: scale(0);
  }
  50% {
    transform: scale(1.25);
  }
  100% {
    transform: scale(1);
  }
}

自定义过渡class类名

你也可以向 <Transition> 传递以下的 props 来指定自定义的过渡 class:

  • enter-from-class:对应阶段的默认 class 名 v-enter-from
  • enter-active-class:对应阶段的默认 class 名 v-enter-active
  • enter-to-class:对应阶段的默认 class 名 v-enter-to
  • leave-from-class:对应阶段的默认 class 名 v-leave-from
  • leave-active-class:对应阶段的默认 class 名 v-leave-active
  • leave-to-class:对应阶段的默认 class 名 v-leave-to

传入的这些 class 覆盖相应阶段的默认 class 名。这个功能在你想要在 Vue 的动画机制下集成其他的第三方 CSS 动画库时非常有用,比如 Animate.css

示例:

安装animate.css:

pnpm install animate.css
<template>
  <div class="animate">
    <Transition enter-active-class="animate__animated animate__bounceIn"
      leave-active-class="animate__animated animate__bounceOut">
      <div v-if="flag" class="animate-box"></div>
    </Transition>
    <button @click="flag = !flag">switch</button>
  </div>
</template>

<script setup lang='ts'>
import { ref } from 'vue'
// 项目中导入
import 'animate.css'

const flag = ref(true)
</script>

<style lang='scss' scoped>
.animate {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;

  .animate-box {
    width: 400px;
    height: 600px;
    background-color: skyblue;
  }
}
</style>

效果:

animate.gif

duration属性

durationTansition的一个属性,它用来控制整个动画的持续时长。

<Transition :duration="50" enter-active-class="animate__animated animate__bounceIn"
      leave-active-class="animate__animated animate__bounceOut">
      <div v-if="flag" class="animate-box"></div>
</Transition>

animate1.gif 如果有必要的话,你也可以用对象的形式传入,分开指定进入和离开所需的时间:

<Transition :duration="{ enter: 500, leave: 1000 }">...</Transition>

enter表示进入动画的时长,leave表示离开动画的时长。

animate2.gif

transition 生命周期和GSAP

transition有8个声明周期:

  • @before-enter="beforeEnter":对应enter-from
  • @enter="enter":对应enter-active
  • @after-enter="afterEnter":对应enter-to
  • @enter-cancelled="enterCancelled":显示过渡打断
  • @before-leave="beforeLeave":对应leave-from
  • @leave="leave":对应leave-active
  • @after-leave="afterEnter":对应leave-to
  • @leave-cancelled="leaveCancelled":离开过渡打断

每一个钩子函数都会有一个内置参数el就是被transition包裹的元素。enterleave钩子函数会有多一个参数done,它是一个函数,表示过渡/动画完成。

当只使用javascript过渡的时候,在enterleave钩子函数中必须调用done进行回调。

结合gsap动画库使用GreenSock

pnpm add gsap --save

示例:

<template>
  <div class="animate">
    <transition @before-enter="beforeEnter" @enter="onEnter" @leave="onLeave">
      <div v-if="flag" class="animate-box"></div>
    </transition>
    <button @click="flag = !flag">switch</button>
  </div>
</template>

<script setup lang='ts'>
import { ref } from 'vue'
import gsap from 'gsap'

const flag = ref(true)

function beforeEnter(el: HTMLElement) {
  gsap.set(el, {
    width: 0,
    height: 0
  })
}

function onEnter(el: HTMLElement, done: gsap.Callback) {
  gsap.to(el, {
    width: 200,
    height: 200,
    onComplete: done
  })
}

function onLeave(el: HTMLElement, done: gsap.Callback) {
  gsap.to(el, {
    width: 0,
    height: 0,
    onComplete: done
  })
}
</script>

效果: gsap.gif

transition-Appear属性

appear这个属性可以设置初始节点过渡,就是页面加载完成就开始过渡效果/动画。

对应三个状态: appear-active-class:在这个阶段可以设置动画的时长、效果

appear-from-class:在这个阶段设置页面刚开始加载时的一个状态(大小、颜色、背景等)

appear-to-class:在这个阶段可以设置页面加载完毕的一个状态(大小、颜色、背景等)

那么在使用这些属性的时候,得先给Transition组件添加上appear属性:

<template>
  <div class="animate">
    <transition appear appear-from-class="from" appear-active-class="active" appear-to-class="to">
      <div v-if="flag" class="animate-box"></div>
    </transition>
    <button @click="flag = !flag">switch</button>
  </div>
</template>

<script setup lang='ts'>
import { ref } from 'vue'
import 'animate.css'

const flag = ref(true)
</script>

<style>
.from {
  width: 0;
  height: 0;
}

.active {
  transition: width 2s, height 2s;
}

.to {
  width: 100%;
  height: 100%;
}
</style>

效果: 页面一加载或者刷新过渡效果/动画 appear.gif

结合animate.css使用:

<template>
  <div class="animate">
    <transition appear appear-active-class="animate__animated animate__backInDown">
      <div v-if="flag" class="animate-box"></div>
    </transition>
    <button @click="flag = !flag">switch</button>
  </div>
</template>

<script setup lang='ts'>
import { ref } from 'vue'
import 'animate.css'

const flag = ref(true)
</script>

<style>
.from {
  width: 0;
  height: 0;
}

.active {
  transition: width 2s, height 2s;
}

.to {
  width: 100%;
  height: 100%;
}
</style>

transition-group过渡列表

怎么给一个列表添加过渡效果呢?比如使用v-for

在这种场景下,我们会使用<transition-group>组件。

<transition-group>组件默认情况下只包裹一个元素(组件)时不会为其添加过渡/动画效果。但是你可以通过 tag 属性 指定渲染一个元素(组件)。

<transition-group>的过渡模式不可用,因为我们不再切换相互特有的元素。

<transition-group>内部元素总是需要提供唯一的key属性值。

<transition-group>css过渡的类将会应用在内部的元素中,而不是这个组/容器本身。

<transition-group>其他使用方式均和<transition>的使用方式一样。

示例:

<template>
  <div class="animate">
    <button @click="add">add</button>
    <button @click="pop">pop</button>
    <transition-group enter-active-class="animate__animated animate__bounceIn"
      leave-active-class="animate__animated animate__bounceOut">
      <div class="list" v-for="item in list" :key="item">{{ item }}</div>
    </transition-group>
  </div>
</template>

<script setup lang='ts'>
import { ref, reactive } from 'vue'
import 'animate.css'

const list = reactive<number[]>([1, 2, 3, 4, 5, 6])

function add(): void {
  list.push(list.length + 1)
}

function pop() {
  list.pop()
}
</script>

transition-group.gif

列表的移动过渡

transition-group:组件还有一个特殊之处。除了离开和进入,它还可以为定位的改变添加动画。只需要了解新增的v-move类就可以使用这个功能。它会应用在元素改变定位的过程中。像之前的类名一样,它的前缀可以通过 name属性来自定义,也可以通过move-class手动设置。

状态过渡

Vue同样可以给数字、SVG、背景颜色等添加过渡动画,下面以数字为例:

<template>
  <div class="animate">
    <input type="number" v-model="num.current">
    <div>{{ num.tweenedNumber.toFixed(0) }}</div>
  </div>
</template>

<script setup lang='ts'>
import { reactive, watch } from 'vue'
import gsap from 'gsap'

const num = reactive({
  current: 0,
  tweenedNumber: 0
})

watch(() => num.current, (newVal, oldVal) => {
  gsap.to(num, {
    duration: 1,
    tweenedNumber: newVal
  })
})
</script>

效果:

状态过渡.gif

十四、依赖注入(Provide/Inject)

通常情况下,当我们需要从父组件向子组件传递数据时,我们使用props。但是当遇到深度嵌套的组件,而深层的子组件只需要父组件的部分内容。这种情况下如果仍然使用props沿着组件链逐级传递下去,会非常麻烦。

provide可以在祖先组件中指定我们想要提供给后代组件的数据和方法,而在任何后代中,我们都可以使用inject来接收provide提供的数据和方法。

provide() 函数接收两个参数。第一个参数被称为注入名,可以是一个字符串或是一个 Symbol。后代组件会用注入名来查找期望注入的值。一个组件可以多次调用 provide(),使用不同的注入名,注入不同的依赖值。

第二个参数是提供的值,值可以是任意类型,包括响应式的状态,比如一个 ref。

示例:

Father.vue:

<template>
  <div>
    <h1>Father</h1>
    <label for="red">red</label>
    <input id="red" type="radio" value="red" v-model="colorValue">
    <label for="pink">pink</label>
    <input id="pink" type="radio" value="pink" v-model="colorValue">
    <label for="blue">blue</label>
    <input id="blue" type="radio" value="skyblue" v-model="colorValue">
    <div class="box"></div>
    <hr />
    <ProvideA />
  </div>
</template>

<script setup lang='ts'>
import { ref, provide } from 'vue'
import ProvideA from './ProvideA.vue';

const colorValue = ref<string>('red')

// 将需要传给深度嵌套子组件的数据通过provide提供
provide('color', colorValue)
</script>

<style lang='scss' scoped>
.box {
  width: 100px;
  height: 100px;
  background-color: v-bind(colorValue);
  margin-bottom: 20px;
}
</style>

Son.vue:

<template>
  <div>
    <div class="box"></div>
    <hr />
    <ProvideB />
  </div>
</template>

<script setup lang='ts'>
import ProvideB from './ProvideB.vue';
import { inject, type Ref } from 'vue'

const color = inject<Ref<string>>('color')
</script>

<style lang='scss' scoped>
.box {
  width: 100px;
  height: 100px;
  background-color: v-bind(color);
  margin-bottom: 20px;
}
</style>

Grandson.vue

<template>
  <div>
    <div class="box"></div>
    <hr />
  </div>
</template>

<script setup lang='ts'>
import { inject, type Ref } from 'vue'

const color = inject<Ref<string>>('color')
</script>

<style lang='scss' scoped>
.box {
  width: 100px;
  height: 100px;
  background-color: v-bind(color);
  margin-bottom: 20px;
}
</style>

子组件是可以修改父组件提供的注入进来的依赖的:

Grandson.vue

<template>
  <div>
    <div class="box"></div>
    <hr />
    <button @click="updateColor">修改注入进来的值</button>
  </div>
</template>

<script setup lang='ts'>
import { inject, type Ref } from 'vue'

const color = inject<Ref<string>>('color')
console.log(color)
function updateColor() {
  color!.value = 'black'
}
</script>

provide.gif

当提供 / 注入响应式的数据时,建议尽可能将任何对响应式状态的变更都保持在供给方组件中

十五、兄弟组件传参和Bus

借助父组件传参

例如:Father组件为父组件,A和B组件时Father的子组件。

示例:

A组件:

<template>
  <div>
    A
    <span>{{ flag }}</span>
    <button @click="emitB">修改flag的值</button>
  </div>
</template>

<script setup lang='ts'>
import { ref, defineEmits } from 'vue'

// 定义自定义事件
interface Emits {
  (e: 'emitB', flag: boolean): void
}

const emtis = defineEmits<Emits>()

const flag = ref(false)

// 触发自定义事件
function emitB() {
  flag.value = !flag.value
  emtis('emitB', flag.value)
}
</script>

<style lang='scss' scoped></style>

B组件:

<template>
  <div>
    B
    <span>{{ flag }}</span>
  </div>
</template>

<script setup lang='ts'>
import { defineProps } from 'vue'

export interface Props {
  flag: boolean
}

defineProps<Props>()
</script>

<style lang='scss' scoped></style>

Father组件:

<template>
  <div>
    Father
    <hr />
    <A @emitB="getAParams" />
    <hr />
    <B :flag="flag" />
  </div>
</template>

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

const flag = ref(false)

function getAParams(params: boolean) {
  flag.value = params
}
</script>

<style lang='scss' scoped></style>

AtoB.gif

缺点:每次都要经过父组件,需要父组件去绑定子组件的自定义事件。

EventBus

Mitt

在Vue3中,onon、off和$once实例方法已被移除,组件实例不在实现事件触发接口,因此大家熟悉的EventBus便无法使用了。然而我们习惯了使用EventBus,对于这种情况我们可以使用Mitt库(发布订阅模式)。

1、安装

npm install --save mitt

全局使用

  1. main.ts中初始化
// 引入mitt
import mitt from 'mitt'


import { createApp } from 'vue'

// 创建vue实例对象
const app = createApp(App)

// mitt是一个函数,初始化mitt
const Mit = mitt()

// mitt注册为全局属性后,但是vue并不会帮我们去声明mitt的类型
// 所以我们要在vue的全局属性上为mitt扩充ts类型声明,这样我们在使用mitt的时候,才会有ts类型提示。
declare module 'vue' {
  export interface ComponentCustomProperties  {
    $Bus: typeof Mit
  }
}

// 将mitt注册为能够被应用内所有组件实例访问到的全局属性
// app.config.globalProperties 一个用于注册能够被应用内所有组件实例访问到的全局属性的对象, 这是对 Vue 2 中 `Vue.prototype` 使用方式的一种替代。。
app.config.globalProperties.$Bus = Mit

现在我们就可以在项目中使用mitt了。

关于ComponentCustomPropertiesvue官网的介绍:

image.png

使用

A组件:

<template>
  <button @click="emitB">修改flag的值</button>
</template>
<script setup lang="ts">
  import { getCurrentInstance } from 'vue'

  const instance = getCurrentInstance()

  function emitB () {
    instance?.proxy?.$Bus.emit('emit-to', '哈哈哈哈')
  }
</script>

B组件:

<template>
  <div>
    B
  </div>
</template>

<script setup lang='ts'>
import { getCurrentInstance } from 'vue'

const instance = getCurrentInstance()

instance?.proxy?.$Bus.on('emit-to', (value) => {
  console.log(value) // 哈哈哈哈
})

</script>

image.png

十六、tsx&vite插件

我们之前都是使用Template去写我们的模板,现在可以扩展另一种风格:TSX风格。

  1. 安装插件
npm install @vitejs/plugin-vue-jsx --save-dev
  1. 在vue.config.ts中添加配置
import vueJsx from '@vitejs/plugin-vue-jsx'

export default defineConfig({
  plugins: [vue(), vueJsx()]
})
  1. 在项目src目录下创建.tsx文件: 我的是创建在src目录下:App.tsx

tsx书写风格

  1. 直接使用 export default 向外暴露一个渲染函数:
export default function () {
  return (<div>带土</div>)
}

使用的时候就当做它是一个组件去使用,引入tsx文件的时候可以不带后缀名。 比如在App.vue中引入使用:

<template>
  <div>
    <daitu></daitu>
  </div>
</template>

<script setup lang='ts'>
  import daitu from './App.tsx'

</script>

<style lang='scss' scoped></style>

  1. 使用vue内置的defineComponent函数的 选项式API模式:
import { defineComponent } from 'vue'

export default defineComponent({
  data () {
    return {
      name: '张三'
    }
  },
  render () {
    return (<div>{ this.name }</div>)
  }
})

注意:在tsx文件中的插值表达式是单个花括号:{}

  1. 使用vue内置的defineComponent函数的 setup 函数模式
import { defineComponent, ref } from 'vue'

export default defineComponent({
  setup () {
    const name = ref('张三')
    const flag = ref(false)
    return () => (<div v-show="{ flag.value }">{ name.value }</div>)
  }
})

注意:在template中使用ref变量会自动解包,不需要加.value。但是在tsx中是需要添加.value的,不会自动解包。

tsx支持v-show指令,但不支持v-if指令,在tsx中可以使用三元表达式代替v-if指令:

import { defineComponent, ref } from 'vue'

export default defineComponent({
  setup () {
    const name = ref('张三')
    const flag = ref(false)
    return () => (<>
      <div>{ flag.value ? <div>true</div> : <div>false</div> }</div>
    </>)
  }
})

tsx中代替v-for指令的写法:

import { defineComponent, reactive } from 'vue'

export default defineComponent({
  setup () {
    const data = reactive([
      {
        name: '张三'
      },
      {
        name: '李四'
      },
      {
        name: '王五'
      }
    ])
    return () => (<>
      <div>
        { data.map(item => {
          return <div>{ item.name }</div>
        }) }
      </div>
    </>)
  }
})

tsx中代替v-bind指令绑定属性值的写法:

属性名={值}

import { defineComponent, reactive, ref } from 'vue'

export default defineComponent({
  setup () {
    const name = ref('张三')
    const flag = ref(false)
    const data = reactive([
      {
        name: '张三'
      },
      {
        name: '李四'
      },
      {
        name: '王五'
      }
    ])
    return () => (<>
      <div>
        { data.map(item => {
          return <div data-name={v.name}>{ item.name }</div>
        }) }
      </div>
    </>)
  }
})

tsx中使用propsemits

父组件:

<template>
  <daitu :name="带土" @on-click="val => console.log(val)"></daitu>
</template>
import { defineComponent, reactive, ref } from 'vue'

interface Props {
  name?: string
}

export default defineComponent({
  props: {
    name: String
  },
  emits: ['on-click'],
  setup (props: Props, emit) {
    const name = ref('张三')
    const flag = ref(false)
    function fn () {
      emit('on-click', name)
    }
    return () => (<>
      <div onClick={() => fn()}>{ props?.name }</div>
    </>)
  }
})

tsx中的插槽:

import { defineComponent } from 'vue'

const A = (_, { slots }) => (<>
  <div>{ slots.default ? slots.default : '默认值' }</div>
</>)

export default defineComponent({
  setup () {
    const slot = () => {
      default: () => (<div>带土default slots</div>)
    }
    return () => (<>
      <div>
        <A v-slots={slot}></A>
      </div>
    </>)
  }
})

tsx中的数据双向绑定:

import { defineComponent, ref } from 'vue'

export default defineComponent({
  setup () {
    const inputValue = ref<string>('')
    return () => (<>
      <div>
        <input type="text" v-model={inputValue.value} />
      </div>
    </>)
  }
})

十七、v-model指令

vue3v-model是破坏性更新的。

v-model指令支持inputselect表单元素以及自定义组件

v-model其实是一个语法糖,通过propsemit组成而成。

对比vue2的变化

  1. 默认值的改变
  • prop: value -> modelValue;
  • 事件: input -> update:modelValue;
  • v-bind的.sync修饰和组件的model选项已移除;
  • 新增 支持多个v-model;
  • 新增支持自定义修饰符Modifiers;

示例: 父组件

<template>
  <div>
    <button @click="open">显示</button>
    <span>isShow:{{ isShow }}</span>
    <hr />
    <VModel v-model="isShow" />
  </div>
</template>

<script setup lang='ts'>
import { ref } from 'vue'
import VModel from './v-model.vue'

const isShow = ref<boolean>(false)

const open = () => {
  isShow.value = true
}
</script>

<style lang='scss' scoped></style>

子组件

<template>
  <div v-if="modelValue" class="v-model">
    <div>{{ modelValue }}</div>
    <input type="text">
    <button @click="close">关闭</button>
  </div>
</template>

<script setup lang='ts'>
import { defineProps, defineEmits } from 'vue'
// 定义props的接口类型
export interface Props {
  modelValue: boolean
}
// 定义emits的接口类型
export interface Emits {
  (e: 'update:modelValue', modelValue: boolean): void
}
// 定义props
const props = defineProps<Props>()

// 定义emits
const emit = defineEmits<Emits>()
// 点击关闭按钮的点击事件处理函数
const close = () => {
  emit('update:modelValue', false)
}
</script>

<style lang='scss' scoped>
.v-model {
  margin-top: 15px;
  width: 350px;
  height: 200px;
  border: solid 10px #efefef;
}
</style>

v-model.gif

多v-model的用法

Vue3支持绑定多个v-model

组件只双向绑定一个props的话,可以使用默认值modelValue

当绑定多个props的时候,可以通过v-model:props进行双向绑定。

示例: 父组件

<template>
  <div>
    <button @click="open">显示</button>
    <span>isShow:{{ isShow }}</span>
    <hr />
    <div>text: {{ inputValue }}</div>
    <VModel v-model="isShow" v-model:text="inputValue" />
  </div>
</template>

<script setup lang='ts'>
import { ref } from 'vue'
import VModel from './v-model.vue'

// 是否显示对话框
const isShow = ref<boolean>(false)
// 输入框的内容
const inputValue = ref<string>('奥利给')

const open = () => {
  isShow.value = true
}

子组件

<template>
  <div v-if="modelValue" class="v-model">
    <div>{{ modelValue }}</div>
    <input type="text" :value="text" @input="change">
    <button @click="close">关闭</button>
  </div>
</template>

<script setup lang='ts'>
import { defineProps, defineEmits } from 'vue'

export interface Props {
  modelValue: boolean
  text: string
}
export interface Emits {
  (e: 'update:modelValue', modelValue: boolean): void
  (e: 'update:text', text: string): void
}

const props = defineProps<Props>()

const emit = defineEmits<Emits>()

const close = () => {
  emit('update:modelValue', false)
}

const change = (e: Event) => {
  const input = e.target as HTMLInputElement
  emit('update:text', input.value)
}
</script>

<style lang='scss' scoped>
.v-model {
  margin-top: 15px;
  width: 350px;
  height: 200px;
  border: solid 10px #efefef;
}
</style>

v-model1.gif

自定义v-model修饰符

在defineProps中定义一个,比如:textModifiers:

export interface Props {
  text: string
  textModifiers: {
    isSb: boolean
  }
}

export interface Emits {
  ('update:text', text: string): void
}
const props = defineProps<Props>()

const emit = defineEmits<Emits>()

<!-- input事件的时间处理函数 -->
const change = (e: Event) => {
  const input = e.target as HTMLInputElement
  emit('update:text', prpos?.textModifiers?.isSb ? input.value + '傻逼' : input.value)
}

完整示例: 父组件

<template>
  <div>
    <button @click="open">显示</button>
    <span>isShow:{{ isShow }}</span>
    <hr />
    <div>text: {{ inputValue }}</div>
    <VModel v-model="isShow" v-model.isSb:text="inputValue" />
  </div>
</template>

<script setup lang='ts'>
import { ref } from 'vue'
import VModel from './v-model.vue'

// 是否显示对话框
const isShow = ref<boolean>(false)
// 输入框的内容
const inputValue = ref<string>('奥利给')

const open = () => {
  isShow.value = true
}
</script>

<style lang='scss' scoped></style>

`子组件:

<template>
  <div v-if="modelValue" class="v-model">
    <div>{{ modelValue }}</div>
    <input type="text" :value="text" @input="change">
    <button @click="close">关闭</button>
  </div>
</template>

<script setup lang='ts'>
import { defineProps, defineEmits } from 'vue'

export interface Props {
  modelValue: boolean
  text: string
  textModifiers: {
    isSb: boolean
  }
}
export interface Emits {
  (e: 'update:modelValue', modelValue: boolean): void
  (e: 'update:text', text: string): void
}

const props = defineProps<Props>()

const emit = defineEmits<Emits>()

const close = () => {
  emit('update:modelValue', false)
}

const change = (e: Event) => {
  const input = e.target as HTMLInputElement
  emit('update:text', props?.textModifiers?.isSb ? input.value + '傻逼' : input.value)
}
</script>

<style lang='scss' scoped>
.v-model {
  margin-top: 15px;
  width: 350px;
  height: 200px;
  border: solid 10px #efefef;
}
</style>

v-model2.gif

十八、 自定义指令(directive)

自定义指令(directive)属于破坏性更新。

vue中有v-ifv-bindv-forv-showv-model等等一系列方便快捷的指令,除此之外,vue还提供了自定义指令

Vue3指令的钩子函数

  • created:元素初始化的时候。
  • beforeMount:指令绑定到元素后调用,只调用一次。
  • mounted:元素插入父级dom调用。
  • beforeUpdate元素被更新之前调用。
  • updated:元素更新之后调用。
  • beforeUnmount:在元素被移除前调用。
  • unmounted:指令被移除后调用,只调用一次。

Vue2指令的钩子函数:bindinsertedupdatecomponentUpdatedunbind

钩子参数​

指令的钩子会传递以下几种参数:

  • el:指令绑定到的元素。这可以用于直接操作 DOM。

  • binding:一个对象,包含以下属性。

    • value:传递给指令的值。例如在 v-my-directive="1 + 1" 中,值是 2。
    • oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用。
    • arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 "foo"。
    • modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo 中,修饰符对象是 { foo: true }。
    • instance:使用该指令的组件实例。
    • dir:指令的定义对象。
  • vnode:代表绑定元素的底层 VNode。

  • prevNode:之前的渲染中代表指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用。

局部自定义指令

自定义指令的名称,必须以vNameOfDirective的形式来命名自定义指令,以使得它们可以直接在模板中使用。

例如:

<template>
  <input v-focus />
</template>

<script setup>
import { type Directive } from 'vue'
// 在模板中启用 v-focus
const vFocus:Directive = {
  mounted: (el) => el.focus()
}
</script>

上面这个 input 元素应该会被自动聚焦

给自定义指令赋值

给自定义参数赋的值在指令的所有钩子函数中都能够拿到。

<template>
  <div v-focus="baz">
</template>

<script setup>
import { type Directive } from 'vue'

const baz = ref(111)

// 在模板中启用 v-focus
const vFocus:Directive = {
  mounted: (el, binding, vnode, prevVnode) => {
    console.log(binding.value) // 111
  }
}
</script>

自定义指令传参

<template>
  <div v-focus:obj="baz">
</template>

<script setup>
import { type Directive } from 'vue'

const baz = ref('baz')

// 在模板中启用 v-focus
const vFocus:Directive = {
  mounted: (el, binding, vnode, prevVnode) => {
    console.log(binding.arg) // obj
  }
}
</script>

自定义指令修饰符

<template>
  <div v-focus:obj.lazy="baz">
</template>

<script setup>
import { type Directive } from 'vue'

const baz = ref(111)

// 在模板中启用 v-focus
const vFocus:Directive = {
  mounted: (el, binding, vnode, prevVnode) => {
    console.log(binding.modifiers) // '{ lazy: true }'
  }
}
</script>

image.png

指令简写

假设你只想在指令的mountedupdated钩子函数中触发相同的行为,而不关心其他的钩子函数,那么可以通过指令的函数模式实现:

<script>
  import { type Directive } from 'vue'
  const vPermission:Directive = (el, binding) => {}
</script>

Directive可以接收泛型,依次给函数的参数设定类型,比如:

<script>
  import { type Directive } from 'vue'
  const vPermission:Directive<HTMLElement, string> = (el, binding) => {}
</script>

此时,el的类型是HTMLElementbinding.value的类型是string

权限指令示例:

<template>
  <div class="permission">
    <button v-permission="'shop:create'">新增</button>
    <button v-permission="'shop:edit'">编辑</button>
    <button v-permission="'shop:delete'">删除</button>
  </div>
</template>

<script setup lang='ts'>
import { type Directive } from 'vue'

window.localStorage.setItem('userId', 'daitu')

const userId = localStorage.getItem('userId')

const permission = [
  'daitu:shop:create',
  'daitu:shop:edit',
  // 'daitu:shop:delete',
]

const vPermission: Directive<HTMLElement, string> = (el, binding) => {
  console.log(el)
  console.log(binding.value)
  if (permission.indexOf(`${userId}:${binding.value}`) === -1) {
    el.style.display = 'none'
  }
}
</script>

<style lang='scss' scoped>
.permission {
  >button {
    padding: 5px 15px;
    color: #000000;
    border: 0;
    margin-left: 15px;

    &:first-child {
      background-color: skyblue;
    }

    &:nth-child(2) {
      background-color: pink;
    }

    &:last-child {
      background-color: yellow;
    }
  }
}
</style>

image.png

拖拽指令

<template>
  <div>
    <div class="dt-dialog">
      <div v-move class="dt-dialog__header">
      </div>
      <div class="dt-dialog__content">
        内容区域
      </div>
    </div>
  </div>
</template>

<script setup lang='ts'>
import type { Directive } from 'vue'

const vMove: Directive<HTMLElement, void> = (el) => {
  /**
   * 鼠标按下事件的事件处理函数
   * @param e 鼠标事件对象
   */
  const mouseDownHandler = (e: MouseEvent) => {
    // 获取鼠标在dialog中的x坐标
    const X: number = e.clientX - el.parentElement?.offsetLeft!
    // 获取鼠标在dialog中的y坐标
    const Y: number = e.clientY - el.parentElement?.offsetTop!
    /**
     * document文档对象模型鼠标移动事件的事件处理函数
     * @param ele 鼠标事件对象
     */
    const mouseMoveHandler = (ele: MouseEvent) => {
      // 浏览器可视窗口最大宽度
      const clientWidth = document.body.clientWidth
      // 浏览器可是窗口最大高度
      const clientHeight = document.body.clientHeight
      // 获取dialog距html文档可视窗口的左侧距离
      const dialogForDocumentLeft: number = ele.clientX - X
      // 获取dialog距html文档可视窗口的顶部距离
      const dialogForDocumentTop: number = ele.clientY - Y
      // 将计算好的dialog距html文档可视窗口的左侧距离和顶部距离分别赋值给dialog的left和top
      el.parentElement!.style.left = dialogForDocumentLeft + 'px'
      el.parentElement!.style.top = dialogForDocumentTop + 'px'
      // dialog的右边界位置
      const dialogRightBorder = dialogForDocumentLeft + el.parentElement!.offsetWidth
      // dialog的底边界位置
      const dialogBottomBorder = dialogForDocumentTop + el.parentElement!.offsetHeight
      // 判断dialog的右边界是否等于或超过可视窗口的最大宽度
      if (dialogRightBorder >= clientWidth) {
        // 超过就用可视窗口的宽度减去dialog的宽度,作为dialog距离可视窗口左侧的距离
        el.parentElement!.style.left = clientWidth - el.parentElement!.offsetWidth + 'px'
      }
      // 判断dialog的底边界是否等于或超过可视窗口的最大高度
      if (dialogBottomBorder >= clientHeight) {
        // 超过就用可视窗口的高度度减去dialog的高度,作为dialog距离可视窗口顶部的距离
        el.parentElement!.style.top = clientHeight - el.parentElement!.offsetHeight + 'px'
      }
      // 判断dialog的左边界是否等于或超过可视窗口的最小宽度0
      if (el.parentElement!.offsetLeft <= 0) {
        // 是的话dialog距离可视窗口左侧的距离设置为0
        el.parentElement!.style.left = '0px'
      }
      // 判断dialog的左边界是否等于或超过可视窗口的最小高度0
      if (el.parentElement!.offsetTop <= 0) {
        // 是的话dialog距离可视窗口顶部的距离设置为0
        el.parentElement!.style.top = '0px'
      }
    }
    /**
     * document文档对象模型鼠标移动事件的事件处理函数
     */
    const mouseUpHandler = () => {
      // 修改鼠标的样式
      document.removeEventListener('mousemove', mouseMoveHandler)
    }
    // 为document文档对象模型绑定鼠标移动事件
    document.addEventListener('mousemove', mouseMoveHandler)
    // 为document文档对象模型绑定鼠标放开事件
    document.addEventListener('mouseup', mouseUpHandler)
  }
  // 为绑定自定义事件的元素绑定鼠标按下事件
  el.addEventListener('mousedown', mouseDownHandler)
}
</script>

<style lang='scss' scoped>
.dt-dialog {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 350px;
  height: 400px;
  background-color: skyblue;
  color: #333333;
  border: solid 5px #686767;

  .dt-dialog__header {
    height: 35px;
    border-bottom: solid 1px #333333;
    background-color: pink;

    &:hover {
      cursor: move;
    }
  }
}
</style>

拖拽指令.gif

图片懒加载

<template>
  <div>
    <img style="width: 414px; height: 500px;" v-lazy="image" v-for="image in imageList">
  </div>
</template>

<script setup lang='ts'>
import type { Directive } from 'vue'

// 引入图片
const modules: Record<string, { default: string }> = import.meta.glob('../src/assets/images/*.jpg', { eager: true })
console.log(modules)
const imageList = Object.values(modules).map(item => {
  return item.default
})
console.log(imageList)

// 懒加载指令
const vLazy: Directive<HTMLImageElement, string> = async (el, binding) => {
  console.log(el)
  // 真正的图片还未加载时展示的默认图片
  const defaultImage = await import('./assets/vue.svg')
  // 给img绑定默认图片
  el.src = defaultImage.default
  // 给img赋值真正的图片

  // 监测元素是否进入可视区域
  const observer = new IntersectionObserver((entries) => {
    console.log(entries[0])
    // 如果 = 0说明不在可视区域,如果大于0预说明元素进入了可是区域
    if (entries[0].intersectionRatio > 0) {
    // 模拟异步请求 
      setTimeout(() => {
    // 将真正展示的图片地址赋值给元素的src属性
        el.src = binding.value
    // 停止监听
        observer.unobserve(el)
      }, 2000)
    }
  })
    // 监听指定元素与html的交集
  observer.observe(el)

}
</script>

<style lang='scss' scoped></style>

拖拽指令.gif

十九、自定义Hooks

主要用来处理复用代码逻辑的封装

Vue2的Mixins

vue2Mixins,Mixins就是将这些相同的逻辑抽离出来,各个组件只需要引入mixins,就能够实现写一次代码,在多组件复用的的效果。

弊端就是会涉及覆盖的问题,变量来源不明,不利于阅读,使代码变得难以维护。

组件的datamethodsfilters会覆盖mixins里同名的datamethodsfilter

Vue3的自定义hook

  • Vue3的hook函数相当于Vue2的mixin,不同的是,hooks是函数。

  • Vue3的hook函数,可以帮助我们提高代码的复用性,让我们能在不同的组件中都利用hooks函数

  • Vue3 hook 库

    1. Get Started
    2. VueUse

浅试自定义hooks

src下创建一个hooks目录,专门用来存放自定义hooks

src/hooks/index.ts

import { onMounted } from 'vue'

interface Options {
  el: string
}

export default function (options: Options):Promise<{ base64: string }> {
  return new Promise((resolve) => {
    // dom挂载完毕
    onMounted(() => {
      // 获取img元素对象
      const img: HTMLImageElement = document.querySelector(options.el) as HTMLImageElement
      // 元素加载事件,元素加载完毕触发
      img.onload = () => {
        // pedding改为fullfiled,成功态,并返回成功的结果值
        resolve({
          base64: imageToBase64(img)
        })
      }
    })

    // 转base64的函数
    const imageToBase64 = (el: HTMLImageElement) => {
      // 创建一个canvas元素
      const canvas = document.createElement('canvas')
      // 建立一个二维渲染上下文
      const ctx = canvas.getContext('2d')
      // 设置画布宽高
      canvas.width = el.width
      canvas.height = el.height
      // 在画布上绘制图像
      ctx?.drawImage(el, 0, 0, canvas.width, canvas.height)
      // 返回图片的base64
      return canvas.toDataURL('image/jpg')
    }
  })
}

src/App.vue

<template>
  <div>
    <img id="image" src="./assets/images//01.jpg" alt="">
  </div>
</template>

<script setup lang='ts'>
import useBase64 from './hooks'

useBase64({ el: '#image' }).then(res => {
  console.log(res.base64)
})
</script>

<style lang='scss' scoped>
img {
  width: 300px;
  aspect-ratio: 1/2;
}
</style>

image.png

Hooks结合自定义指令使用

功能:监听元素大小位置的变化,获取元素的大小位置信息

示例:

src/hooks/index.ts:

// 导入vue实例类型App

import type { App } from 'vue'
/**
 * 监听元素大小变化
 * @param el 需要监听大小变化的元素
 * @param callback 回调函数
 */

function useResize (el: HTMLElement, callback: (elementSizeInfo: DOMRectReadOnly) => void) {
    // 监听元素大小的变化
    // entries:一个 ResizeObserverEntry 对象数组,可以用于获取每个元素改变后的新尺寸。
    let resizeObserver = new ResizeObserver((entries) => {
        // 调用执行函数
        callback(entries[0].contentRect)
    })
     // 对指定元素进行监听
    resizeObserver.observe(el)
}

// install函数: 插件的install函数,插件是一个拥有install函数的对象,或者是install函数本身
// app: 安装函数会接收到安装它的应用实例和传递给 app.use(插件,额外选项) 的额外选项作为参数
const install = (app: App) => {
  // 使用vue实例对象的directive方法注册一个全局的自定义指令
  app.directive('resize', {
    // 自定义指令的生命周期函数,绑定指令的元素挂在到页面后执行
    mounted: (el: HTMLElement, binding: { value: (elementSizeInfo: DOMRectReadOnly) => void }) => {
      // 调用useResize函数,监听元素大小
      useResize(el, binding.value)
    }
  })
}

// 往useResize函数上添加一个install属性,属性值是一个函数类型(就是给useResize对象上添加install安装函数,让useResize成为一个插件)
// 在js中一切皆为对象,包括函数
// 使用console.dir()打印useResize函数:
/**
 * f useResize(el, callback)
 * install: (app) => {...}
 */
// 可以看到useResize身上多了一个install属性
useResize.install = install

// 向外暴露useResize函数
export default useResize

useResize当作hooks使用

App.vue:

<template>
  <div>
    <div id="resize"></div>
  </div>
</template>

<script setup lang='ts'>
// import useResize from './hooks';
import { onMounted } from 'vue'

onMounted(() => {
  // 获取元素对象
  const element = document.getElementById('resize') as HTMLElement
    useResize(element, (elementSizeInfo) => {
    console.log(elementSizeInfo)
  })
})


</script>

<style lang='scss' scoped>
#resize {
  width: 300px;
  height: 350px;
  background-image: linear-gradient(45deg, red, blue);
  border: solid 1px #ccc;
  resize: both;
  overflow: hidden;
}
</style>

useResize当作自定义指令使用:

main.ts:

import { createApp } from 'vue'
import App from './App.vue'
// 引入useResize
import useResize from './hooks'
console.dir(useResize);

let app = createApp(App)

app.use(useResize)

app.mount('#app')

App.vue:

<template>
  <div>
    <div v-resize="callback" ref="resizeElement" id="resize"></div>
  </div>
</template>

<script setup lang='ts'></script>

<style lang='scss' scoped>
#resize {
  width: 300px;
  height: 350px;
  background-image: linear-gradient(45deg, red, blue);
  border: solid 1px #ccc;
  resize: both;
  overflow: hidden;
}
</style>

hook和自定义指令的效果是一样的 拖拽指令.gif

二十、 Vue3全局函数和变量

由于Vue3没有Prototype属性。使用app.config.globalProperties代替,然后去定义变量额函数。

Vue2定义全局变量:

Vue.prototype.$http = () => {}

Vue3定义全局变量:

app.config.globalProperties为所有组件都安装全局可用的属性

import { createApp } from 'vue'

const app = createApp({})

app.config.globalProperties.$http = 'axios'

app.config.globalProperties.$filter = {
  format<T> (str: T) {
    return `带土-${str}`
  }
}

Vue3全局变量的使用方式:

在模板中使用

<template>
  <!-- 全局变量 -->
  <div>{{ $http }}</div>
  <!-- 全局函数 -->
   <div>
    {{ $filter.format('奥利给') }}
  </div>
</template>

在js中使用

在js中使用全局变量或者函数,需要借助vue内置的一个函数:getCurrentInstance

getCurrentInstance函数返回当前的组件实例

我们可以获取到当前的组件实例,然后去调用全局声明的变量或者是函数。

import { getCurrentInstance } from 'vue'

const app = getCurrentInstance()
// 使用全局变量
console.log(app?.proxy.$http)
// 使用全局函数
console.log(app?.proxy.$filter.format('萨斯给'));

添加模块扩展

如果不添加vue模块扩展的话,Ts无法正确的对我们添加的全局属性/方法进行识别,就会导致报错。

所以我们要对vue进行模块扩展ComponentCustomProperties接口:

interface Filter {
  format<T>: (str: T) => string
}

declare module 'vue' {
  export interface ComponentCustomProperties {
    $http: string,
    $filter: Filter
  }
}

二十一、Vue3插件

插件 (Plugins) 是一种能为 Vue 添加全局功能的工具代码。

一个插件可以是一个拥有 install() 方法的对象,也可以直接是一个安装函数本身。

安装函数会接收到安装它的应用实例和传递给 app.use() 的额外选项作为参数。

import type { App } from 'vue'
const myPlugin = { 
    install(app: App, options) { 
        // 配置此应用 
    } 
}
export default myPlugin

main.ts

import { createApp } from 'vue'
import myPlugin from './plugins/index.ts'
    
const app = createApp()
    
app.use(myPlugin)

仿element加载组件

  • loading组件Loading.vue

    <template>
        <div v-if="flag" class="loading">
            loading......
        </div>
    </template>
    
    <script setup lang='ts'>
    import { ref } from 'vue'
    // 显示/隐藏开关
    const flag = ref(false)
    // 显示
    const show = () => {
      flag.value = true
    }
    // 隐藏
    const hide = () => {
      flag.value = false
    }
    // 向外暴露本组件setup中的方法/变量
    defineExpose({
      show,
      hide,
      flag
    })
    </script>
    
    <style lang='scss' scoped>
    .loading {
        position: absolute;
        top: 0;
        left: 0;
        display: flex;
        justify-content: center;
        align-items: center;
        width: 100%;
        height: 100%;
        background-color: #000000;
        color: #ffffff;
        font-size: 32px;
        font-weight: 700;
    }
    </style>
    
  • 编写插件loading.ts

    import type { App, VNode } from 'vue'
    import { createVNode, render } from 'vue'
    import Loading from './Loading.vue'
    
    /**
     * 插件的安装函数
     * @param app Vue示例对象
     */
    const install = (app: App): void => {
      console.log(app)
      // 创建虚拟DOM
      const Vnode: VNode = createVNode(Loading)
      // 挂载虚拟DOM到body元素
      render(Vnode, document.body)
      // 添加全局属性
      app.config.globalProperties.$loading = {
        show: Vnode.component?.exposed?.show,
        hide: Vnode.component?.exposed?.hide
      }
      console.log(Vnode)
    }
    // 插件可以是一个含install安装函数的对象或者是安装函数本身
    export default {
      install
    }
    
  • createVNode():创建一个虚拟DOM,接收一个参数

  • 扩充Vue的类型vite-env.d.ts:

      declare module 'vue' {
        export interface ComponentCustomProperties {
          $loading: {
            show: () => void
            hide: () => void
          }
        }
      }
    
      export default {}
    
  • main.ts中注册插件main.ts:

    import { createApp } from 'vue'
    import './style.css'
    import App from './App.vue'
    
    import Loading from './plugins/Loading'
    
    const app = createApp(App)
    
    app.use(Loading)
    
    app.mount('#app')
    
  • 在App.vue中使用插件:

    <template>
      <div>
    
      </div>
    </template>
    
    <script setup lang='ts'>
    import { getCurrentInstance } from 'vue'
    
    // 获取本组件的实例对象
    const instance = getCurrentInstance()
    // 使用全局方法
    instance?.proxy?.$loading.show()
    // 五秒后隐藏
    setTimeout(() => {
      instance?.proxy?.$loading.hide()
    }, 5000)
    
    </script>
    
    <style lang='scss' scoped>
    
    </style>
    

二十二、Scoped和样式穿透

主要是用于修改很多Vue常用的第三方组件库(element、ant-desigin)等组件的样式,虽然这些组件库已经配好了样式。但是难免会有需要更改其样式但是又更改不了,这时就需要用到样式穿透

为什么会更改不了呢?

答案是因为我们在组件中的<style scoped lang="scss"></style>标签中添加了scopedscoped起到样式阻隔(样式私有化/模块化)的作用,使得每一个组件内的样式不会影响到其他组件的样式。

那这样做的目的又是什么呢?

因为Vue是SPA(single page web application)单页面应用,打完包之后只有一个index.html文件。如果不做这个css模块化的话,样式就会错乱,影响到其他页面。

scoped的原理

vue中的scoped通过在DOM结构以及css样式上添加唯一不重复的标记(:data-v-hash)的方式,以保证唯一(而这个工作是由PostCSS转译实现的),达到样式私有化/模块化的目的。

scoped三条渲染规则

  1. PostCSS会给每一个.vue文件一个唯一的data自定义属性,并且会在组件中的每一个DOM节点加上这个data自定义属性(形如:data-v-123)来表示它的唯一性。

  2. 在每一句css选择器的末尾(编译后生成的css语句)加上一个属性选择器,通过组件唯一的data自定义属性,来命中对应组件中对应css选择器的标签的样式。

image.png

  1. 如果组件内部包含其他组件,只会给其他组件的最外层标签加上当前组件的唯一data自定义属性。

    image.png

PostCSS会给每一个组件分配一个独一无二的动态属性data-v-xxx,给该组件中的所有DOM添加该属性,然后,给css选择器额外添加一个对应的属性选择器,来选择该组件中的DOM,这种做法使得样式只作用于含有该属性的DOM---该组件内部的DOM,从而达到样式模块化的效果。

样式穿透(:deep(css选择器))

我们知道,在组件内引入了其他组件,postcss只会给其他组件的最外层标签添加上当前组件的唯一data自定义属性,但是我们在当前组件中写得css选择器它还是会添加上以当前组件的唯一data自定义属性为属性选择器,所以这时css选择器就没办法命中对应的标签。那我要是想在当前组件修改其他组件中的深层一点的标签的样式咋办呢?这时就需要用到样式穿透了。

.main {
  :deep (.el-input__wrapper) {}
}

加上样式穿透就可以修改其他组件中的深层的标签的样式了,其实它的原理就是将属性选择器从css选择器的末尾挪到前面去,:deep()的父选择器是谁,postCss生成的属性选择器就挪到谁的后边。

:deep()前:

image.png

image.png

:deep()后:

image.png

image.png