likes
comments
collection
share

Vue3 入门

作者站长头像
站长
· 阅读数 11

前言

最近边看官方文档边查资料学习Vue3, 融入了一些自己的理解,如有错误,感谢指出

基础部分

组合式API 和 响应式API

Vue3 和 Vue2有一些相同也有一些区别,比如,Vue3可以使用组合式API,一个单文件组件的API会写在<script setup>中,这与Vue2有差异。另外,Vue3的this与Vue2中的this也有区别,比如,this没了,请注意。

选项式API组合式API

  • 选项式API: 类似Vue2中我们熟悉的风格,选项式API是基于组合式API
<script>
  export default{
       data(){
           return:{}
       },
       methods:{
           //这里写各种api
       },
       setup(params){},
       props:[],
       emits:[]
  } 
</script>
  • 组合式API: 在不同的组件里,在<script setup>中引入某个模块中的API进行使用
<script setup>
import {func1, func2, hooks} from 'module' 
   //例如: import {ref, mounted} from 'vue' 
   const cnt = ref() //获取响应式状态    
   function add(){    
       cnt.value++   //这里的完整cnt是?
   }    
      
   //生命周期钩子 
   mounted(){
       console.log('open')
   }
} 
</script>

<template>
   <button @click="add">cnt: {{ cnt }}</button>
</template>

响应式声明 —— ref()、reactive()

Vue3中的声明式渲染与Vue2不同,在Vue3中,你需要将 响应式状态 用ref()或是reactive()这两个API声明并初始化

ref()reactive() 都可以对响应式状态进行声明式渲染,但是他们有所区别

ref():

可以接受任何值类型,ref()声明后会返回一个包裹好的对象,修改其值需要访问其对象的.value属性。多次使用ref声明同一个响应式状态时,最新的会覆盖前面的

  • 基础

格式:

// 使用ref()声明响应式状态的格式
import {ref} from 'vue'
const d1 = ref(初始值)

const dd1 = {
    data1: ref('也可以在对象中选择性对某个属性进行响应式声明'),
    data2: 630
}

//模板语法中调用值: 注意这里不是直接调用 value, 否则会报 NaN
{{ d1 }}

//修改值:
d1.value = newValue

示例:

<script setup>
    import {ref} from 'vue'
    const cnt = ref(0)
    const type = typeof(cnt)

    //直接对cnt进行操作
    function add_1(){
        cnt.value++
    }

    //对cnt的value属性进行操作
    function add_2(){
        cnt++
    }
</script>

<template>
    <button @click="add_1"> 对cnt的value属性修改, 修改后的cnt为: {{ cnt }}</button>
    <button @click="add_2"> 直接对cnt修改, 修改后的cnt为: {{ cnt }}</button>
    <br/>
    <p>使用ref()响应式声明的cnt的类型为: {{ type }}</p>
</template>

Vue3 入门

可以看到无法通过cnt++修改cnt的值,因为使用ref()声明的cnt是一个object

  • 响应式替换

当一个ref包含对象类型的值时,可以使用属性相同的对象对其响应式替换

const test = ref({ num,: 1 })
test.cnt = { num: 60 }
//替换后的 test.cnt 是 60
  • 被解构时不丢失响应性
const { num } = test
//这个 num 依然是响应式的
  • 顶层属性 和 解包

顶层属性: 用上面的例子,在test中,test是一个顶层属性,而其中的 num(即: test.num是顶层属性。

解包: 模板语法中,如果一个ref类型数据是顶层对象,那么会自动提取它的value使用, 形如上面{{ cnt }},cnt是一个顶层对象,在{{ cnt }}中会对 cnt 自动调用 cnt.value 进行渲染,顶层对象可以在模板语法中直接被用于四则运算

像上面 test.num不是一个顶层对象,那么就无法被用于四则运算,那么对它进行解包就需要先解构使其中的num成为顶层对象,再使用顶层对象num进行四则运算。注意:数组和集合不会被解包

<script setup>
    import {ref} from 'vue'
    const test = {
        num: ref(60)
    }
    
    //解构
    const {num} = test
</script>

<template>
    <h1>直接使用test.num: {{ test.num }}</h1>
    <h1>对test.num进行加法运算: {{ test.num + 1}}, 乘法运算 {{ test.num*2 }}</h1>
    <h1>对test.num进行解构后使用顶层对象num进行加法运算: {{ num+2 }}</h1>
</template>

Vue3 入门

原因: 对非顶层对象进行解包得到的是一个object,而对顶层对象解包得到的是它的value

  • 在响应式对象中自动解包

将ref嵌套在深层响应式对象中时,发生ref解包:

在 Vue 中,状态都是默认深层响应式的。这意味着即使在更改深层次的对象或数组,你的改动也能被检测到。

const num1 = ref(850)
const o = reactive({
    num1
})
console.log(o.num1) //850

如果要声明浅层响应式,要使用shallowReactive()

使用ref声明的数组和集合,要调用其值需要手动调用.value

const arr = reactive([ref(1,2,3,4)])
console.log(arr[0].value) //1
console.log(arr[0]) //{"__v_isShallow":false,"__v_isRef":true,"_rawValue":1,"_value":1}

reactive():

只适用于对对象(包括数组、Map。Set等) 的响应式声明

格式:

import {reactive} from 'vue'
const d2 = reactive({
    cnt: 初始值
})
//声明的是一个object

//使用其中的属性:
d2.cnt++

//模板语法中使用
{{ d2.cnt }}

//如果直接写入对象而不是调用属性,那么会出现其JSON格式
{{ d2 }}

Vue3 入门

其他参照ref

动态绑定

大体同Vue2,但注意如果我们要动态绑定一个class类名呢?

动态绑定样式

<script setup>
    import { reactive } from 'vue'

    const styleArr = reactive({
        red: 'title_id',
        pink: 'title_class',
        purple: 'purple'
    })
</script>

<template>
    <h1 :id="styleArr.red">动态绑定id名</h1>
    <h1 :class="[1 == 1? styleArr.pink : '' ]">动态绑定类名</h1>
    <h1 :style="{color: styleArr.purple}">动态绑定Style</h1>
</template>

<style>
    #title_id {
      color: blue;
    }
    .title_class{
      color: pink;
    }
</style>

Vue3 入门

可以发现基本就是动态值的声明有点区别

事件监听 - v-on

同Vue2的v-on, 简写为 @,这里列出修饰符

事件修饰符:

  • .stop: 阻止点击事件传递,可用于阻止点击穿透
  • .prevent: 阻止元素中该操作的默认事件发生
  • .self: 仅当event.target为元素自身时触发
  • .capture: 捕获元素内部事件,优先在该外部元素处理
  • .once: 该事件只会被触发一次
  • .passive: 事件的默认行为立即发生而不等待设置的事件完成

按键修饰符(都是见文知意的): 借助按键修饰符可以完成组合动作触发方法( 如ctrl+[ )

  • 系统按键修饰符: .ctrl.alt.shift.meta
  • 鼠标按键修饰符:.left.right.middle
  • 按键状态修饰符: keyupkeydown
  • 键盘按键别名: .enter.tab.delete.esc.space.up.downleft.right
  • exact: 写在链式修饰符最后一个,用于严格控制触发某个事件所需要的组合修饰符的类别,换而言之多按了别的就触发不了

单向绑定 - v-bind

同Vue2的v-bind, 简写为 :

双向绑定 - v-model

基本用法同Vue2的v-model,但Vue3获取事件的修饰符的方法有变化。

v-model内置修饰符 (input输入中) :

  • .lazy: 在每次change事件后再更新数据
  • .number: 使用户的输入自动变为数字
  • .trim: 自动除去用户输入内容中两端的空格

在子组件中,我们用defineProps('状态')向父组件传递了一个状态,父组件里我们给这个状态传值时给v-model加了修饰符,这时在子组件里如果我们想要有一个回调,可以根据那个修饰符进行一些操作,该怎么做呢?

如下情景:

parent.vue

<child v-model:info.number="info" />

child.vue

<script setup>
    const props = defineProps({
        info: String
    })
    defineEmits('update:info')
</script>

<template>
    <input 
        type="text"
        :value="info"
        @input="$emit('update:info', $event.target.value)"
    />
</template>

现在我们想要实现当父组件v-model的修饰符是.number时,将输入的内容中的数字9都变为数字0

官方文档说,对于又有参数arg又有修饰符的 v-model 绑定,生成的 prop 名将是 arg + "Modifiers"

const props = defineProps(['info', 'infoModifiers'])
//或者
const props = defineProps({
    info: String,
    infoModifiers: { default: ()=>({}) }
})

其中,infoModifiers中存储的是父组件给info传值时使用的修饰符,你可以这样访问修饰符:

console.log(props.infoModifiers) //输出 { number: true }

条件渲染 - v-if / v-show

同Vue2的v-if、v-else-if、v-elsev-show,注意二者的区别,v-show只是display属性的变化,仍可以获取到DOM

列表渲染 - v-for

同Vue2的v-for

计算属性 - computed

与Vue2有较大差异 Vue3中,需要响应式声明计算属性, 这和其他hooks一样的, 都是需要引入 请注意,计算属性必须监听一个响应式状态, 在getter中,不可以做异步操作和修改DOM

<script setup>
    import { ref, computed } from 'vue'
    const  data1 = ref(-2)
    
    //声明一个计算属性msg的ref, 监听data1的变化,当data1值发生变化时,会自动计算对应的计算属性msg的值
    const msg = computed(() => {
        //各种有关处理,最后return结果, 这里和Vue2一样
        return data1.value>0 ? 'Ok' : 'No'
        
        //上面直接return相当于:
        get(){
            return data1.value>0 ? 'Ok' : 'No'
        }
    }) 
</script>

<template>
    <!--使用计算属性-->
    <p>{{ msg }}</p>
    <button @click="data1++">data1++</button>
    <p>data1: {{ data1 }}</p>
</template>

上面这种直接return结果的计算属性是只读的,它相当于只写了一个getter,如果需要使用其他方式更改计算属性的值,你需要再写setter

set(newValue){
    // 根据接受到的值对该计算属性监听的响应式状态进行更改
}

生命周期

Vue3 相比 Vue2, 生命周期多了setup,但它是围绕beforeCreatecreated运行,所以没必要显示定义出来。另外,Vue3 的其他几个生命周期钩子类型和 Vue2 基本一样(因为destroy两个钩子名字变了),但名字前面都多了一个on, 可以见以下:

Vue2 Vue3 说明
beforeCreate setup 组件创建时运行
created
beforeMount onBeforeMount DOM即将挂载前
mounted onMounted DOM挂载完毕
beforeUpdate onBeforeUpdate 响应数据即将修改前
updated onUpdated 响应数据修改完毕
请注意: 销毁周期的两个钩子名字有变
beforeDestroy onBeforeUnmount 组件即将销毁前
destoryed onUnmounted 组件销毁完毕

Vue3 使用组合式API时,需要先引入生命周期钩子,生命周期钩子函数也可以写在外部方法里调用

import { onMounted, onUpdated } from 'vue'
// 直接调用
onMounted(() => {
    console.log('组件已完成挂载')
})

//在外部函数中调用
function test(){
    //其他操作
    onUpdated(() => {
        console.log('数据已完成更新')
    })
}

监视属性 - watch

同样的,需要先引入

import { watch } from 'vue'

作用同Vue2中一样,监视某个响应式状态的变化,根据新旧值调用回调。但Vue3中需要传入监听的状态,如下:

//注意传入的是监听状态不是它的value
//还要注意,如果传入的状态是一个对象,那么newValue和oldValue是相同的,都是那个对象,这时候需要转为深层监视器
watch(监听的状态, async(newValue, oldValue) => {
        //回调
    },
    { deep: false }, //是否转为深层监视器, 要监听的状态的数据结构很大时慎用
    { immediate: true }, //是否创建时立即执行 
    { flush: 'post' }    //设置次配置项可以让监听器中能够访问组件更新后的DOM, 如果不设置,那么只能访问更新前的旧DOM
                         // watchEffect() 中同样
)

//监听状态还可以这样传入:
watch(
    () => 监听的状态,
    (newValue, oldValue) => {
        //回调
    },
    // 配置项相同
)

watchEffect()

当监听器会使用与监听源相同的响应式状态时,可以使用watchEffect()来简化代码,让监听器自动跟踪回调的响应式依赖

Vue3 入门 官方文档给出的例子中,该监听器监听的状态todoId在监听的回调中被使用,这时可以使用watchEffect()简化这种监听的写法,并且这种写法不需要声明immediate: truewatchEffevt()只会在同步执行时自动追踪他监听的所有的状态

Vue3 入门

  • 关闭监听器:
const closeWatch = watchEffect(() => {})

//关闭监听器
closeWatch()

模板引用 - ref

也就是Vue2中,我们通过给一个元素设置ref="xxx",然后可以使用this.$refs.xxx获取那个元素的实例

获取并使用单个元素的DOM

在Vue3中,因为没有this, 所以同样的功能用法有些区别,举个栗子:

我们现在要获取一个ref="back"的元素的实例:

<div ref="back" @click="goBack">&lt; 返回</div>

不同于Vue2的是,Vue3中我们要使用ref()响应式声明一个同名的ref,并且在挂载完毕后通过value属性访问我们想要获取的DOM

import { ref, onMounted } from 'vue'

//使用ref声明一个同名的ref,注意此时初始值是null
const back = ref(null)

function goBack(){
  if(back){ //这里注意,应当先判断back是否为null,在没有完成挂载时获取DOM是没有用的
    /* 很重要的一点,因为ref()声明得到的是一个包裹的对象,想要获取其值(这里是DOM),需要访问value属性 */
    back.value.style.backgroundColor = "red"
  }
}  

onMounted(() => {
  console.log('获取到了DOM: ', back.value) //组件挂载完毕后,back获取到了DOM并在控制台输出
})

Vue3 入门

Vue3 入门

在 v-for 中获取指定DOM

出于程序执行逻辑的原因,ref数组中的DOM的顺序不一定和源数组相同

ref绑定数组

<button v-for="(item, index) in list" :key="index" ref="listItem" @click="getItem(index)">
    {{ item }}
</button>
import { ref, onMounted } from 'vue'
//响应式声明要被遍历的list
const list = ref( [1,2,3] )

//我们要获取的是一个被v-for遍历的数组,所以声明的同名ref也应该是一个数组
const listItem = ref( [] )

function getItem(index){
    //请注意,value才是我们想要的东西,DOM数组存在value之中
    listItem.value[index].style.color = 'purple'
    listItem.value[index].style.fontSize = 18 * (index+1) + 'px'
}

onMounted(() => {
    console.log('DOM获取成功: \n', listItem)
})

Vue3 入门

Vue3 入门

ref绑定函数

这种方法绑定的函数会在每次组件更新时被调用

<button :ref="(el) => { /* 收到了该元素的引用el */ }">
    绑定一个内联函数
</button>

ref获取子组件实例

获取的步骤和获取DOM是一致的,只是要先引入子组件,但是应当注意两个区别:

  • 当子组件使用的是选项式API(即Vue2那种)或者子组件没有使用<script setup>,被引用的组件实例和该子组件的 this 完全一致,这意味着父组件对子组件的每一个属性和方法都有完全的访问权

  • 当子组件使用了<script setup>,这时候父组件默认无法访问子组件里所有的东西,而只能访问子组件中通过defineExpose()暴露的内容。

子组件对外暴露内容供父组件访问:

<script setup>
 import { ref } from 'vue' 
 const a = 1, b = ref(2) 
 //选择性对外暴露内容  
 defineExpose({
   a,
   b  
 })  
</script>    

组件

注册组件

在Vue2中,我们一般将其默认暴露,然后在需要引用它的组件内使用import引入并写在components内,这是局部引入。如果全局引入,那么写在App.vue的components内。Vue3中有一点点变化

全局注册

假设组件testA.vuetestB.vue,我们想要全局引入,可以这样:

App.vue

import { createApp } from 'vue'
const app = createApp({})

//全局注册非单文件组件
app.component(
    'testA',
    {
        /* 组件的实现 */
    }
)

//全局注册单文件组件
import testA from './testA.vue' //引入组件文件
import testB from './testB.vue'
app.component('testA', testA)
   .component('testB', testB) //可以链式调用app.component()方法

于是你可以在其他组件里直接使用被全局注册的组件

局部注册

局部注册的组件仅可用于注册时的当前组件

父组件中:

<!-- 若使用 <script setup>  -->
<script setup>
    import testA from './testA'
</script>

<template>
    <testA />
</template>

<!-- 若不使用 <script setup>,那么需要像VUE2一样在components选项中显示注册 -->
<script>
    import testB from './testB.vue'
    export default {
        setup(){
            //...
        },
        components: {
            testB
        },
        data(){
            return {
                //...
            }
        }
    }
</script>

父子组件数据交互有差异。在vue2中我们可以直接使用props来接收父组件传来的数据,但是在Vue3中,若我们使用组合式API并在 <script setup> 中,那么我们要使用 defineProps 宏声明它,这个宏不需要显示导入。为了方便,一般都命名为props

父组件向子组件通信 - defineProps()

在<script setup>中,defineProps()不需要显示引入

请注意:父组件通过props向子组件传值是浅拷贝这意味着,如果子组件改变了从props拿到的值,父组件中对应的值也会相应改变, 想要深拷贝就需要自己处理

const propsA = defineProps(['title', 'info']) //使用数组
const propsB = defineProps({ //使用对象,格式为 数据名:类型
    id: String,
    list: Array,
    baseInfo: Object,
    way: Function,
    
    //完整的配置
    completeData: {
        type: [Array, String], //若只能是一种数据则不适用数组
        required: true, //是否必需
        default(){      //没有传值获值为 undefined 时的默认值
            return ''   //也可以直接 default: xxx
        },
        validator(value){ //设置校验规则,唯有满足规则的值才可以成功传入使用
            return xxx 
        }
    }
})

type可选值为原生构造函数( Number、String、Boolean、Array、Object、Date、Function、Symbol )或者自定义构造函数或者构造类的实例

defineProps()会返回一个对象,里面包含了可以传递给组件的所有props

//调用props
console.log(propsA.title)

给组件的props传值、传动态值:

<myTitle title="JavaScript is the best language in the world"></myTitle>
<myTitle :title="titleOne"></myTitle>
<myTitle :base-info="info"></myTitle>

传递Boolean应该使用v-bind,不给值会被隐式转换为true,不写则为false

<component :like="true" />
<component like />
<!-- 上面两个等效 -->

传入Object,不论是否为常量,都应该使用v-bind

<!-- 传常量 -->
<component :baseInfo="{
    nickName: 'Rory',
    tagList: ['A','B']
 }" />
 
<!-- 传变量 -->
<component :baseInfo="user.baseInfo" />

子组件向父组件通信 - defineEmits()

<script setup>中使用defineEmits来声明一个emit函数,等同于$emit方法,然后使用emit即可实现子组件向父组件通信。组件触发的事件不会冒泡

// ...还没搞清除声明emit的,后面补上

也可以不声明emit,使用$emit('事件名', '传递出的参数')来对外传递参数, 如果emit定义的事件与元素的某个原生事件同名(比如click),那么emit定义的事件将会覆盖原生的事件

child.vue

<script setup>
</script>

<template>
    <!-- 在子组件的该方法触发位置,定义使用什么事件来触发该方法,比如这里是用click来触发 response 方法,并给父组件传递一个参数 'hello world' -->
    <button @click="$emit('response', 'hello world')">Child component</button>
</template>

parent.vue

<script setup>
    import { ref } from 'vue'
    import ChildComp from './ChildComp.vue'

    const childMsg = ref('No child msg yet')
</script>

<template>
    <!-- 父组件中,直接v-on="子组件暴露的事件名", 即可调用子组件传递出的方法,但注意触发该方法的事件仍然是子组件中触发该方法的事件 -->
    <ChildComp @response="(msg) => childMsg = msg"/>
    <p>{{ childMsg }}</p>
</template>

Vue3 入门

  • 事件校验 - 声明时把时间赋值为一个函数

对抛出某事件时所传入emit的参数进行校验,返回值为boolean,为true则事件合法,可正常进行,否则被拦截

<script setup>
   const emit = defineEmits({
       // 对 click 事件进行校验
       click: ({ params } => {
             if(check(params)){ //对传入的参数进行校验,check()是对参数的某种校验规则
                 return true
             }else{ return false }
       }),
       //不对response事件校验,任何情况都合法,都可以触发
       response: null
   })
</script>    

有些时候我们可能需要在声明$emit方法的子组件的<scrip setup>中使用$emit方法,直接用方法名是不可以的,应当emit('方法名')来调用该方法

子组件中的插槽 - <slot/>

和VUE2差不多,插槽内容可以访问到父组件的作用域但不可访问到子组件的作用域

child.vue

<template> 
    <div style="display: flex; flex-direction: column;"> 
        <strong>我是子组件,下面是父组件传来的插槽内容</strong> 
        <slot /> 
    </div> 
</template>

parent.vue

<template> 
    <child>
        父组件内的子组件标签内的东西会被渲染到子组件对应位置的插槽中
        <img src="https://img1.baidu.com/it/u=413643897,2296924942&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500"/>
    </child>
</template>

Vue3 入门

组件动态切换 - :is=""

这个主要用于在同一位置动态切换不同的组件显示,比较突出的一个表现就是移动端使用tabbar来进行首页页面切换。

通过:is=""的值控制哪个组件进行显示,他可以接收以下值:

  • 注册组件的组件名
  • 导入的组件对象
<component :is="tabs[currentComponent]"></component>

这里注意,组件切换掉时,被切换掉组件会被卸载,这意味着你无法获取到被切换掉组件的DOM,如果你需要让被切换的组件不被卸载,你需要使用<KeepAlive>将其包裹

如果is作用于原生的html元素上,那么其值需要加上前缀vue:才可以被解析为一个Vue组件

<p is="vue:myComponent"></p>

透传Attributes

指的是传递给一个组件,却没有被该组件声明为 props或 emits 的 attribute 或者 v-on 事件监听器,如果该组件以单个元素为根进行渲染,那么这些透传的attributes将会被自动添加到根元素,比如:

设父组件为parent,子组件为child

<!-- child.vue -->
<div class="test"> 
    <p>我是子组件,没有设置任何emits或者props </p>
</div>

<!-- parent.vue -->
<child class="childClass" id="childId" @click="choose" />

<!-- 于是在子组件渲染得到 -->
<div class="test childClass" id="childId" @click="choose">
    <p>我是子组件,没有设置任何emits或者props </p>
</div>

禁用attribute继承,当想要attribute应用于根节点外的其他元素时

<script>
//如果使用了 <script setup>,需要新开一个 <script>
export default{
    inheritAttrs: false
}
</script>

<template>
    <div class="test">
        <p :class="$attrs.childClass">我是子组件,没有设置任何emits或者props </p>
    </div>
</template>

子组件中,通过模板表达式{{ $attrs }}访问到透传进来的除了组件声明的propsemits之外的所有attribute

<!-- child.vue -->
<div> all other attribute: {{ $attrs }} </div>

通过$attrs访问其中的属性和函数

  • 访问属性: $attrs.data1$attrs.fooBar (访问'foo-bar')
  • 访问函数: $attrs.func_1

在JavaScript中访问透传Attributes

需要使用useAttrs()

<script setup>
    import { useAttrs } from 'vue'
    const attrs = useAttrs()
    //获取到了透传进来的 attribute 对象(总是最新的,非响应式),然后就可以访问其中的内容
</script>

props依赖注入 - Provide & Inject

当出现组件套组件层层套,最顶层的组件想把一个props给比较深层的组件时,固然可以层层props,但并不好,这时候为了提高效率和减少不必要的消耗,需要使用到provide(提供)inject(注入)

提供 - provide

用于某个父组件为其子组件后代提供数据,这种提供是选择性的一对多,而不是走一条链路层层无脑层层给的,接收数据的组件使用inject接收,形成一条点对点的接收

某个父组件

<script setup>
    import { provide } from 'vue'
    provide('注入名1', 注入值1) //注入名可以是一个String, 也可以是一个Symbol
    provide('注入名2', 注入值2)  //注入值可以是一个响应式状态
    provide('obj', new Object)  //注入值可以是一个对象
    provide('func', ()=>{xxx})  //注入值也可以是一个函数
</script>

使用readonly()包裹的提供的值,不可以被接收方更改

import { ref, provide, readonly } from 'vue'
const cnt = ref(22)
provide('value_readonly', readonly(cnt))

App.vue中的注入

//app层不需要引入provide,直接通过app调用
app.provide('注入名3', 注入值3)

注入 - inject

需要接收的某个子组件中

<script setup>
    import { inject } from 'vue'
    const msg = inject('注入名1')
    console.log(msg)
    
    //如果没有没有祖先组件provide 'data',但子组件却要注入,那么应该设置默认值,如下:
    const value = inject('data', '默认值')
    const spe = inject( 'special', () => new Date() ) //也可初始化一个类
    const { wantKey, wantFunc } = inject('info')  //解构赋值也是支持的
</script>

异步组件 - defineAsyncComponent()

用于大型项目中,把应用拆分为许多个小组件,并在需要时再从服务器加载相关组件,异步组件使用defineAsyncComponent()加载器函数加载

加载组件并注册

局部注册

defineAsyncComponent()接收一个返回Promise的函数

import { defineAsyncComponent } from 'vue' //该方法需要引入
const AsyncComponent = defineAsyncComponent(() => {
    return new Promise( (resolve, reject) => {
        //访问服务器,获取组件
        resolve(  /* 获取成功,把组件抛出 */  )
        reject( /* 获取失败,抛出信息,也可以是抛出专门在获取失败时候显示的组件 */ )
    } )
})

也可以使用ES模块动态导入

<script setup>
    import { defineAsyncComponent } from 'vue'
    const AsyncComponent = defineAsyncComponent(() => {
        import('某个组件的路径')
    })
</script>

<template>
    <AsyncComponent />
</template>

全局注册

App.vue

app.component('comp', defineAsyncComponent(() => {
    import('组件路径')
}))

注册时的选项

要设置高级选项则传入一个Object

const myComponent = defineAsyncComponent({
    //加载组件
    loader: () => import('组件路径'), 
    
    //加载异步组件中使用的组件
    loadingComponent: compA,
    //加载异步组件失败显示的组件
    errorComponent: errComp,
    //超时时间吗,如果加载时间超过了这个时间,也会展示 errorComponent
    timeout: 5000
    
    //展示加载的组件之前的延迟时间, ms为单位
    delay: 100
})

异步组件和Suspense的交互

逻辑复用

组合式函数

其实就是把一些按照某个标准可以归为一类的函数封装到一个@/api/xxx.js文件里,声明时将它们暴露export function funcName(){ /* xxx */ },然后按需在组件或者.js文件中引入即可

官方文档约定: 组合式函数用驼峰法命名并以use作为开头

// @/api/user.js
import { xxx, yyy } from 'zzz'
export function useLogin( account, password ){
    //...
    return new Promise( (res, rej) => {
        //...
    } )
}

// @/components/login/index.vue
<script setup>
    import { useLogin } from '@/api/user.js'
    //在你需要的地方使用引入的 login 方法
    function checkAndLogin( account, password ){
        if(check( account, password )){
            useLogin( account, password )
            .then(res => {
                //...
            })
            .catch(err => {
                //...
            })
        }
    }
</script>

自定义指令

自定义一个v-xxx指令,在<script setup>,以v开头的驼峰命名变量vXxx都可被作为一个自定义指令,在模板中以v-Xxx的形式使用

//创建自定义指令
const vXxx = { //自定义指令 v-Xxx
    // 参数传入挂载点
    mounted: (el) => {
        // 这个指令的功能
    }
}
<template>
    <!-- 使用自定义指定 -->
    <div v-Xxx />
</template>

全局注册自定义指令

App.vue

// 注册v-Xxx
app.directive('xxx', {
    // 该组件的功能
})

自定义指令的生命周期钩子

每个生命周期钩子都可以传入四个参数: el-挂载点元素(只有这个不是只读)、bindingvnodepreVnode

  • el: 挂载点,即该指令绑定到的元素,可通过这个直接操作DOM
  • binding: 数据绑定对象,格式为:
binding = {
    value: "传递给该指令的值", //相当于newValue
    oldValue: "传递给指令的值之前的值", //只在beforeUpdate、updated两个钩子里可用
    arg: "传递给指令的参数",
    midifiers: { //指令修饰符对象,为true则使用了该修饰符
        stop: true,
    },
    instance: { /* 使用该指令的组件的实例对象 */ },
    dir: {} //指令的定义对象
}
  • vnode: 绑定元素的底层vue节点
  • preNode: 之前的渲染中代表指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用

插件 - Plugins

插件是一种可以为Vue添加全局功能的工具,他可以是某种功能模块,也可以是某个自定义指令

安装与使用插件 - install() & use()

//安装与编写插件
const pluginName = {
    install(app, options){
        // 插件的功能
        return xxx
    }
}

//全局使用插件
import pluginName from '@/plugin/myPlugin.js'
app.use(pluginName, {
    // 插件的可选项
})

编写插件

一般插件会专门写在@/plugin文件夹下,并且插件中可以使用provideinject

@/plugin/myPlugin.js

export default{
    install(app, options){
        // 这里写插件的功能
        return xxx
    }
    
    app.provide('name', options)
}

过渡组件 - <Transition>

和Vue2的很像,被<Transition>标签环绕的部分,一旦发生状态、或是DOM元素发生改变,那么这部分就会触发<Transition>设置的过渡动画。这里动画实现的方式是: 在合适的时机给元素动态添加样式类名,动画的播出阶段和对应的类名如下图:

Vue3 入门

可以看到和Vue2相比有一些区别,主要是进入一个阶段时从v-enter变为了v-enter-from

设置过渡效果

首先需要给过渡效果命名——通过设置<Transition>标签的name属性实现,你也可以设置动画播放时间,使用:duration="time",或者分别设置开始阶段和结束阶段的时间: :duration="{ enter: 300, leave: 500 }"。如果我们希望元素初次渲染时候使用一次过渡效果,那么使用传入appear,name也可传入一个动态值

<Transition name="animationName" appear>
    <div >
        <p class="inner"> {{ msg }} </p>
        <!-- 内容 -->
    <div/>
<Transition/>

设置命名后,上面各阶段类名中的v就变为自己设置的过渡效果名,比如: v-enter-to --> animationName-enter-to

然后我们在CSS中设置各阶段过渡效果

.animationName-enter-active{
    transition: all 1s ease-in;
}
.animationName-enter-from{
    transform: rotate(180deg)
    opacity: 0;
}
.animationName-leave-active{
    transition: all 1s ease-out;
}
.animationName-leave-to{
    transform: rotate(180deg);
    opacity: 1;
}

也可以对<Transition>内的某个特定元素设置过渡效果,这被称为深层过渡

.animationName-enter-active .inner{
    transition: all 1s ease-in;
}

这里基本还是和Vue2一样的。同样的你可以在其中使用CSS3提供的animation - @keyframes动画,来定义<Transition>的过渡动画,实现方法和平常的animation一样

  • 通过传给props传类名赋予动画效果

可以用这种方法使用第三方动画库

<Transition
    name="ani"]
    enter-active-class="ani_enter"
    leave-active-class="ani_leave"
>
    <!-- 内容 -->
</Transition>
  • 触发<Transition>过渡的同时再触发设置在其上的另一个独立的animation动画

有时候我们需要在同一个元素上,触发了animation动画时,再触发<Transition>设置的过渡动画。我们需要设置<Transition>type属性来让<Transition>关注animation类型的动画

<Transition type="animation">
    <!-- 内容 -->
</Transition>

js钩子

通过@钩子名="函数名"使用

<Transition
    @before-enter="onBeforeEnter(el)" 
    @enter="onEnter(el, done)" 
    @after-enter="onAfterEnter(el)" 
    @enter-cancelled="onEnterCancelled(el)" 
    @before-leave="onBeforeLeave(el)"
    @leave="onLeave(el, done)" 
    @after-leave="onAfterLeave(el)" 
    @leave-cancelled="onLeaveCancelled(el)"
>
</Transition>
钩子说明
before-enter在元素被插入到 DOM 之前被调用
enter在元素被插入到 DOM 之后的下一帧被调用,使用done()表示过渡结束
after-enter当进入过渡完成时调用
enter-cancelled当进入过渡完成时调用
before-leave大多数时候使用,在 leave 钩子之前调用
leave在离开过渡开始时调用,使用done()表示过渡结束
after-leave离开过渡完成,元素从DOM移除
leave-cancelled只在v-show中调用

是否使用CSS动画

设置:css="false",则不启用CSS动画,<Transition>会采用仅由js实现的动画

可复用<Transition>

就是写个xxx.vue文件,template中只放<Transition>,然后里面放插槽,接着当平常组件使用就行了

<template>
    <Transition 
     name="name"
     @enter=""
     @xxx="xxx">
        <!-- 其他内容 -->
        <slot></slot>
    </Transition>
</template>

<script>
    //...
</script>

<style>
    /* 如果想要应用到插槽,那么这里不适用 scoped */
</style>

过渡模式 - mode

通过给modeprop传入值来设置过渡动画的模式,诸如out-inin-out

<Transition mode="out-in">
     <!--  内容 -->
</Transition>

<TransitionGroup>

用于里面要用v-for渲染的情景, 需要传入tag="标签名"指定一个标签作为外部容器来渲染,比如tag="div",如果里面需要对<li />使用v-for渲染,则应该tag="ul"

<TransitionGroup>不可以使用过渡模式mode,设置的CSS过渡动画演示直接作用于v-for渲染出的列表元素上

其他的和<Transition>差不多

<Teleport> - 传送门

<TelePort>用于解决这样的问题: 一个组件的一部分逻辑上属于该组件,但从整个应用获页面来看,他被渲染在整个VUE应用到的外部区域。一个明显例子是全屏模态框还有弹出层。

<Teleport>的功能是: 将一个组件的一部分模板传送到组件的DOM结构外去,它只改变渲染的DOM的结构,不会影响组件间的逻辑关系。那么可以联想到的是,我们可以在一个组件里写一个页面,然后把他送到组件外渲染,让他属于组件外的节点。这样可以在一些情况下更方便的地设置层级关系

<Teleport to="body">
    <!-- 内容 -->
</Teleport>

我们使用to="destination"指定将<Teleport>传送到哪里去渲染, 其值可以是一个DOM对象,也可以是一个CSS选择器字符串

传入disabledprop将其禁用

<Teleport :disabled="false"> ... </Teleport>
<!-- 你也可以传入一个动态值,或者isMobile,用于检测设备是否是移动设备  -->

多个<Teleport>传入同一个目标时,按照传入时的先后顺序在目标DOM中顺序排列渲染

<Suspense>

单文件组件

创建、全局与局部使用和Vue2是一样的