目录

1、核心·-响应式

2、getCurrentInstance

3、5个内置组件

3-1、 Transition组件

基于 CSS 的过渡效果#

CSS 的 animation#

同时使用 transition 和 animation#

深层级过渡与显式过渡时长#

性能考量#

JavaScript 钩子#

可复用过渡效果#

出现时过渡#

 过渡模式

3-2、TransitionGroup组件

和  的区别#

3-3、 KeepAlive组件

基本使用#

在 DOM 模板中使用时,它应该被写为 。

包含/排除#

最大缓存实例数#

缓存实例的生命周期#

3-4、Teleport 组件

3-5、Suspense组件

4、响应式-进阶

5、由于少了 export,没法传参,也不方便暴露接口,所以就增加了三个工具方法

defineProps

defineEmits

defineExpose

语法

defineProps :

1、适用于父组件向子组件传递属性

2、带默认值的defineProps

defineEmits

defineExpose

子组件:detail.vue

父组件:main.vue

6、其他的Composition API

6.1.shallowReactive与shallowRef

2.readonly与shallowReadonly

3.toRaw与markRaw

4.customRef

7.provide与inject

8、TypeScript 与组合式 API

9、Vuex与组合式API


组合式Api要求我们要将响应式的变量声明在setup函数中,setup函数是一个专门用于组合式Api的钩子函数。而且我们需要借助reactive函数来创建响应式的对象。

setup函数的两种返回值:

若返回一个对象,则对象中的属性、方法,在模板中均可以直接使用。(重点关注)
若返回一个渲染函数:则可以自定义渲染内容。

setup不能是一个async函数,因为返回值不再是return的对象,而是promise,模板看不到return对象中的属性

1、核心·-响应式

setup的两个注意点
setup执行的时机:在beforeCreate之前执行一次,this是undefined
setup的参数

props:值为对象,包含: 组件外部传递过来,且组件内部声明接收了属性
context:上下文对象
attrs: 值为对象,包含:组件外部传递过来,但没有在props配置中声明的属性,相当于 this.$attrs
slots:收到插槽的内容,相当于$slots
emit: 分发自定义事件的函数,相当于this.$emit

//父组件
<script setup>
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
import HelloWorld from './components/test3.vue';
const hello = (val) =>{
  console.log('传递的参数是:'+ val);
}
</script>

<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <HelloWorld msg="传递吧" @hello="hello">
    <template v-slot:cacao>
      <span>是插槽吗</span>
    </template>
    <template v-slot:qwe>
      <span>meiyou</span>
    </template>

  </HelloWorld>
</template>


//子组件
export default {
    name: 'test3',
    props: ['msg'],
    emits:['hello'],
    //这里setup接收两个参数,一个是props,一个是上下文context
    setup(props,context){
        /**
         * props就是父组件传来的值,但是他是Porxy类型的对象
         * >Proxy:{msg:'传递吧'}
         * 可以当作我们自定义的reactive定义的数据
         */

        /**
         * context是一个对象 包含以下内容:
         * 1.emit触发自定义事件的 
         * 2.attrs 相当于vue2里面的 $attrs 包含:组件外部传递过来,但没有在props配置中声明的属性
         * 3.slots 相当于vue2里面的 $slots
         * 3.expose 是一个回调函数
         */
        console.log(context.slots);
        let person = reactive({
            name: '张三',
            age: 17,
        })

        

        function changeInfo(){
            context.emit('hello', 666)
        }

        //返回对象
        return {
            person,
            changeInfo
        }

        //返回渲染函数(了解) 这个h是个函数
        //return () => h('name','age')
    }
}
</script>
<template>
    <button @click="add1">点我啊</button>
    {{ datas.num }}
</template>

<script>
    import {reactive} from 'vue'
    export default {

    setup() {
        const datas = reactive({ num: 0 })
        function add1(){
            datas.num++
        }
        return {
            datas,
            add1
        }
    }
 }
</script>

<style>
</style>
如上,即为组合式Api的典型写法。

在组合式Api中一定记得将需要暴露到模板的对象return出去(单独使用setup的时候)。
更多声明响应式对象的方法
前面我们已经了解了data方法(选项式Api中使用)、reactive(组合式Api中使用)两种不同的声明响应式对象的方式,那还有其他方式吗?

ref

因为reactive方法仅仅对,对象,数组、Map、Set类型生效,对于string、number、boolean类型是无效的。

鉴于这种情况,vue3引入了ref方法来帮助开发者创建任意类型的响应式对象。
<script>
    import {reactive,ref} from 'vue'
    export default {

    setup() {
        //const num = reactive(0)    reactive不支持声明number类型的响应式对象
        const num = ref(0)
        function add1(){
            this.num++
        }
        return {
            num,
            add1
        }
    }
 }
</script>
toRef

当我们想要将reactive创建的响应式对象的某个属性单独传递出来时,我们可以使用toRef实现

<template>
    <button @click="add1">点我啊</button>
    {{ tms }} 
    <div>
        姓名:{{ datas.name }}
        年龄:{{ datas.age }}
    </div>
    
</template>

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

        const datas = reactive({name: 'phyger',age: 18, num: 0})
        // 通过toRef将datas中的num转换为了ref对象
        const tms = toRef(datas,'num')
        const add1=()=>{
            // 在此对tms的值进行修改
            tms.value++
        }

</script>
toRefs
类似toRef,toRefs可以将reactive声明的响应式变量的所有元素都转换为ref对象。
<template>
    <button @click="add1">点我啊</button>
    {{ tms.num }} 
    <div>
        姓名:{{ tms.name }}
        年龄:{{ tms.age }}
    </div>
</template>

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

        const datas = reactive({name: 'phyger',age: 18, num: 0})
        // 通过toRef将datas中的num转换为了ref对象
        const tms = toRefs(datas)
        const add1=()=>{
            // 在此对tms的值进行修改
            tms.num.value++
        }
</script>
toRefs的返回值是一个字典,格式为:
{name: ObjectRefImpl, age: ObjectRefImpl, num: ObjectRefImpl}
如上,我们已经将reactive定义的对象元素全部转换为了ref对象,而且属性值的变化会同步更新到reactive的对象中。

isRef
vue3为我们提供了判断时否为ref对象的方法,即isRef。

<script setup>
    import {reactive,toRefs,isRef} from 'vue'

        const datas = reactive({name: 'phyger',age: 18, num: 0})
        // 通过toRef将datas中的num转换为了ref对象
        const tms = toRefs(datas)
        const add1=()=>{
            // 在此对tms的值进行修改
            tms.num.value++
            // 判断datas,datas.name,tms,tms.age是否为ref对象
            console.log(tms) // toRefs的返回值是一个字典{name: ObjectRefImpl, age: ObjectRefImpl, num: ObjectRefImpl}
            console.log(isRef(datas),isRef(datas.name),isRef(tms),isRef(tms.age))
            // 输出是 false false false true
        }
</script>
结果:
简化在setup中的return
前面我们有说,在组合式Api中,我们必须将模板需要的属性暴露出去。通常大多数的属性都是需要暴露出去的,为了方便,vue3支持在单文件组件(SFC)的场景下,使用<script setup>来简化代码(export也不需要了)。其余的工作构建工具会帮我们处理掉。

<template>
    <button @click="add1">点我啊</button>
    {{ num }}
</template>

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

        // const num = reactive(0)    reactive不支持声明number类型的响应式对象
        let num = ref(0)
        
        // 1、普通函数
        // function add1(){
        //     // 默认num是一个RefImpl对象,我们需要使用.value才能对num的值进行操作
        //     console.log(num,num.value) // RefImpl,0
        //     num.value++
        // }
        
        // 2、箭头函数
        const add1=()=>{
            num.value++
        }
</script>
如上,我们直接使用<script setup>方式简化了return。

Vue3.0中响应式原理
先来看一看vue2的响应式原理
对象类型:通过Object.defineProperty()对属性的读取、修改进行拦截(数据劫持)
数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)
Object.defineProperty( data, 'count', {
    get(){},
    set(){}
})

//模拟实现一下
let person = {
    name: '张三',
    age: 15,
}
let p = {}
Object.defineProperty( p, 'name', {
    configurable: true, //配置这个属性表示可删除的,否则delete p.name 是删除不了的 false
    get(){
        //有人读取name属性时调用
        return person.name
    },
    set(value){
        //有人修改时调用
        person.name = value
    }
})
存在问题:
1. 新增属性。删除属性。界面不会更新
2. 直接通过下表修改数组,界面不会自动更新
vue3的响应式
实现原理:
通过Proxy(代理):拦截对象中任意属性的变化,包括:属性值的读写、属性的添加、属性的删除等等。
通过Reflect(反射):对被代理对象的属性进行操作
模拟vue3中实现响应式
let person = {
    name: '张三',
    age: 15,
}
//我们管p叫做代理数据,管person叫源数据
const p = new Proxy(person,{
    //target代表的是person这个源对象,propName代表读取或者写入的属性名
    get(target,propName){
        console.log('有人读取了p上面的propName属性')
        return target[propName]
    },
    //不仅仅是修改调用,增加的时候也会调用
    set(target,propName,value){
        console.log(`有人修改了p身上的${propName}属性,我要去更新界面了`)
        target[propName] = value
    },
    deleteProperty(target,propName){
        console.log(`有人删除了p身上的${propName}属性,我要去更新界面了`)
        return delete target[propName]
    }
})
映射到person上了,捕捉到修改,那就是响应式啊
vue3底层源码不是我们上面写的那么low,实现原理一样,但是用了一个新的方式
window.Reflect
![Reflect的写法](https://img-blog.csdnimg.cn/565f96b1be74435cacbc42e06706791d.png)

let obj = {
    a: 1,
    b:2,
}
//传统的只能通过try catch去捕获异常,如果使用这种那么底层源码将会有一堆try catch
try{
    Object.defineProperty( obj, 'c', {
        get(){ return 3 },
    })
    Object.defineProperty( obj, 'c', {
        get(){ return 4 },
    })
} catch(error) {
    console.log(error)
}

//新的方式: 通过Reflect反射对象去操作,相对来说要舒服一点,不会要那么多的try catch
const x1 = Reflect.defineProperty( obj, 'c', {
        get(){ return 3 },
})
const x2 = Reflect.defineProperty( obj, 'c', {
        get(){ return 3 },
})
//x1,和x2是有返回布尔值的
if(x2){
    console.log('某某操作成功了')
}else {
    console.log('某某操作失败了')
}
所以vue3最终的响应式原理如下:
let person = {
    name: '张三',
    age: 15,
}
//我们管p叫做代理数据,管person叫源数据
const p = new Proxy(person,{
    //target代表的是person这个源对象,propName代表读取或者写入的属性名
    get(target,propName){
        console.log('有人读取了p上面的propName属性')
        return Reflect.get(target, propName)
    },
    //不仅仅是修改调用,增加的时候也会调用
    set(target,propName,value){
        console.log(`有人修改了p身上的${propName}属性,我要去更新界面了`)
        Reflect.set(target, propName, value)
    },
    deleteProperty(target,propName){
        console.log(`有人删除了p身上的${propName}属性,我要去更新界面了`)
        return Reflect.deleteProperty(target,propName) 
    }
})
reactive 函数
reactive定义响应式数据是深层次的,对象中的对象,数组中的数组都能被监视到状态的修改(或叫值的修改)reactive内部是基于ES6的Proxy实现的,通过代理对象操作源对象内部数据进行操作。
<template>
  <h3>{{person.name}}</h3>
  <h3>{{person.age}}</h3>
  <h3>{{person.job.type}}</h3>
  <h3>{{person.job.salary}}</h3>
  <h3>{{person.hobby}}</h3>
  <button @click="change">点我修改数据</button>
</template>

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

    let person = reactive({
      name:'ulrich',
      age:22,
      job:{
        type:'front-end engineer',
        salary:'30k',
      },
      hobby:['学习','骑行','写博客']
    })
	
	// 修改person中的数据
    let change = function(){
      person.name = 'vichien'
      person.job.type = 'back-end engineer'
      person.hobby[2] = '和产品经理对线'
    }



</script>

reactive对比ref

从定义数据角度对比:

ref用来定义: 基本数据类型
reactive用来定义: 对象(或数组)类型数据
备注: ref也可以用来定义对象(或数组)类型数据,它内部会自动通过reactive转为代理对象

从原理角度对比:

ref通过Object.defineProperty()的get和set来实现响应式(数据劫持)
reactive通过Proxy来实现响应式(数据劫持),并通过Reflect操作源对象内部的数据

从使用角度对比:

ref定义数据:操作数据需要 .value ,读取数据时模板中直接读取不需要 .value
reactive 定义的数据: 操作数据和读取数据均不需要 .value

箭头函数

箭头函数也叫匿名函数,将function关键字省略掉了。 一个简单例子区分普通函数和箭头函数。

// 普通函数
var f1=function(arg1,agr2){
    return arg1+arg2
}

// 箭头函数-多参数
var f2 = (arg1,arg2)=>{
    return arg1+arg2
}

// 箭头函数-无参数:不能省略括号
var f3 = ()=>{
    return true
}

// 箭头函数-单参数:可以省略括号
var f3 = a =>{
    return a
}

computed()
computed() 用来创建计算属性,computed() 函数的返回值是一个 ref 的实例。这个值模式是只读的: 

import { ref, computed } from 'vue'
 
export default {
  setup() {
    const count = ref(0)
    const plusCount = computed(() => count.value + 1)
    plusCount.value = 10
    console.log('plusCount:',plusCount.value)
    console.log('count:', count.value)
  }
}
// 打印结果:
// plusCount.value: 1
// count.value: 0

 或者传入一个拥有 get 和 set 函数的对象,创建一个可手动修改的计算状态:

import { ref, computed } from 'vue'
 
export default {
  setup() {
    const count = ref(0)
    const plusCount = computed({
      get: () => count.value + 1,
      set: val => {
        count.value = val - 1
      }
    })
    plusCount.value = 10
    console.log('plusOne:',plusCount.value)
    console.log('count:', count.value)
  }
}
// 打印结果:
// plusOne.value: 10
// count.value: 9

计算属性与监视
computed函数
与vue2.x中的写法一致、需要引入computed

<template>
  <h1>一个人的信息</h1>
  <div>
      姓: <input type="text" v-model="person.firstName">
      名:<input type="text" v-model="person.lastName">
      <div>
          <span>简名:{{person.smallName}}</span> <br>
          <span>全名:{{person.fullName}}</span>
      </div>
  </div>
</template>
<script>
import { computed,reactive } from 'vue'

    export default {
        name: 'test4',
        props: ['msg'],
        emits:['hello'],
        setup(){
            let person = reactive({
                firstName: '张',
                lastName: '三'
            })
            //简写形式
            person.smallName = computed(()=>{
                return person.firstName + '-' + person.lastName
            })

            //完全形态
            person.fullName = computed({
                get(){
                    console.log('调用get');
                    return person.firstName + '*' + person.lastName
                },
                set(value){
                    console.log('调用set');
                    const nameArr = value.split('*')
                    person.firstName = nameArr[0]
                    person.firstName = nameArr[1]
                },
            })
            return {
                person,
            }
        },
        
    }
 </script>


watch函数
和computed一样,需要引入api
有两个小坑:

1.监视reactive定义的响应式数据的时候:oldValue无法获取到正确的值,强制开启了深度
监视(deep配置无效)
2.监视reactive定义的响应式数据中某个属性的时候:deep配置有效

具体请看下面代码以及注释

<template>
  <h1>当前求和为: {{sum}}</h1>
  <button @click="sum++">点我+1</button>
  <hr>
  <h1>当前信息为: {{msg}}</h1>
  <button @click="msg+='!' ">修改信息</button>
  <hr>
  <h2>姓名: {{person.name}}</h2>
  <h2>年龄: {{person.age}}</h2>
  <button @click="person.name += '~' ">修改姓名</button> <button @click="person.age++">增长年龄</button>
</template>

<script>
    //使用setup的注意事项
    import { watch,ref,reactive } from 'vue'

    export default {
        name: 'test5',
        props: ['msg'],
        emits:['hello'],
        setup(){
            let sum  = ref(0)
            let msg = ref('你好啊')
            let person = reactive({
                name: '张三',
                age: 18,
                job:{
                    salary: '15k'
                },
            })
            //由于这里的this是指的是undefined,所以使用箭头函数
            //情况一:监视ref所定义的一个响应式数据
            // watch(sum, (newValue,oldValue)=>{
            //     console.log('新的值',newValue);
            //     console.log('旧的值',oldValue);
            // })

            //情况二:监视ref所定义的多个响应式数据
            watch([sum,msg], (newValue,oldValue)=>{
                console.log('新的值',newValue); //['sum的newValue', 'msg的newValue']
                console.log('旧的值',oldValue); //['sum的oldValue', 'msg的oldValue']
            },{immediate: true,deep:true}) //这里vue3的deep是有点小问题的,可以不用deep,(隐式强制deep)

            //情况三:监视reactive定义的所有响应式数据,
            //1.此处无法获取正确的oldValue(newValue与oldValue是一致值),且目前无法解决
            //2.强制开启了深度监视(deep配置无效)
            /**
            * 受到码友热心评论解释: 此处附上码友的解释供大家参考:
            * 1. 当你监听一个响应式对象的时候,这里的newVal和oldVal是一样的,因为他们是同一个对象【引用地址一样】,
            *    即使里面的属性值会发生变化,但主体对象引用地址不变。这不是一个bug。要想不一样除非这里把对象都换了
            * 
            * 2. 当你监听一个响应式对象的时候,vue3会隐式的创建一个深层监听,即对象里只要有变化就会被调用。
            *    这也解释了你说的deep配置无效,这里是强制的。
            */
            watch(person, (newValue,oldValue)=>{
                console.log('新的值',newValue); 
                console.log('旧的值',oldValue);
            })

            //情况四:监视reactive对象中某一个属性的值,
            //注意: 这里监视某一个属性的时候可以监听到oldValue
            watch(()=>person.name, (newValue,oldValue)=>{
                console.log('新的值',newValue);  
                console.log('旧的值',oldValue);
            })

            //情况五:监视reactive对象中某一些属性的值
            watch([()=>person.name,()=>person.age], (newValue,oldValue)=>{
                console.log('新的值',newValue);  
                console.log('旧的值',oldValue);
            })

            //特殊情况: 监视reactive响应式数据中深层次的对象,此时deep的配置奏效了
            watch(()=>person.job, (newValue,oldValue)=>{
                console.log('新的值',newValue);  
                console.log('旧的值',oldValue);
            },{deep:true}) //此时deep有用

            return {
                sum,
                msg,
                person,
            }
        },
        
    }
</script>

watchEffect函数
watch的套路是:既要指明监视的属性,也要指明监视的回调
watchEffect的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性
watchEffect有点像computed:

但computed注重的计算出来的值(回调函数的返回值),所以必须要写返回值
而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值
<script>
    //使用setup的注意事项
    import { ref,reactive,watchEffect } from 'vue'

    export default {
        name: 'test5',
        props: ['msg'],
        emits:['hello'],
        setup(){
            let sum  = ref(0)
            let msg = ref('你好啊')
            let person = reactive({
                name: '张三',
                age: 18,
                job:{
                    salary: '15k'
                },
            })
            
            //用处: 如果是比较复杂的业务,发票报销等,那就不许需要去监听其他依赖,只要发生变化,立马重新回调
            //注重逻辑过程,你发生改变了我就重新执行回调,不用就不执行,只执行一次
            watchEffect(()=>{
                //这里面你用到了谁就监视谁,里面就发生回调
                const x1 = sum.value
                console.log('我调用了');
            })

            return {
                sum,
                msg,
                person,
            }
        },
        
    }
</script>

2、getCurrentInstance

Vue2中,可以通过this来获取当前组件实例; 

Vue3中,在setup中无法通过this获取组件实例,console.log(this)打印出来的值是undefined。

在Vue3中,getCurrentInstance()可以用来获取当前组件实例

let {proxy} =getCurrentInstance();

在setup中分别打印下面3个值,结果如下: 

可以看到,getCurrentInstance是一个function方法,getCurrentInstance()是一个对象,proxy也是一个对象。proxy是getCurrentInstance()对象中的一个属性,通过对象的解构赋值方式拿到proxy。 

getCurrentInstance只能在setup生命周期钩子中使用。 

1.在onMunted生命周期中打印getCurrentInstance

2.定义一个handClick方法,通过click事件触发方法

可以看到在function中是无法获取该实例的。

ctx和proxy都是getCurrentInstance()对象中的属性,通过解构赋值的方式拿到。可以看到,2者有所区别。ctx是普通对象,proxy是Proxy对象。

补充:Vue3中关于getCurrentInstance的大坑

应用场景1:如果开发中只适用于调试! 不要用于线上环境,否则会有问题!

解决方案:

方案1.获取挂载到全局中的方法

方案2.使用proxy线上也不会出现问题


应用场景2:全局挂载变量或方法

方法1:

main.ts定义:

在页面中使用:

 方法2:

通过provide和inject挂载

在src文件夹下创建symbol文件夹,并创建index.ts文件

 main.ts定义:

在页面中使用:

3、5个内置组件

3-1、 Transition组件

<Transition> 是一个内置组件,这意味着它在任意别的组件中都可以被使用,无需注册。它可以将进入和离开动画应用到通过默认插槽传递给它的元素或组件上。进入或离开可以由以下的条件之一触发:

  • 由 v-if 所触发的切换
  • 由 v-show 所触发的切换
  • 由特殊元素 <component> 切换的动态组件

以下是最基本用法的示例:

当一个 <Transition> 组件中的元素被插入或移除时,会发生下面这些事情:

  1. Vue 会自动检测目标元素是否应用了 CSS 过渡或动画。如果是,则一些 CSS 过渡 class 会在适当的时机被添加和移除。

  2. 如果有作为监听器的 JavaScript 钩子,这些钩子函数会在适当时机被调用。

  3. 如果没有探测到 CSS 过渡或动画、也没有提供 JavaScript 钩子,那么 DOM 的插入、删除操作将在浏览器的下一个动画帧后执行。

基于 CSS 的过渡效果#

CSS 过渡 class#

一共有 6 个应用于进入与离开过渡效果的 CSS class。

  1. v-enter-from:进入动画的起始状态。在元素插入之前添加,在元素插入完成后的下一帧移除。

  2. v-enter-active:进入动画的生效状态。应用于整个进入动画阶段。在元素被插入之前添加,在过渡或动画完成之后移除。这个 class 可以被用来定义进入动画的持续时间、延迟与速度曲线类型。

  3. v-enter-to:进入动画的结束状态。在元素插入完成后的下一帧被添加 (也就是 v-enter-from 被移除的同时),在过渡或动画完成之后移除。

  4. v-leave-from:离开动画的起始状态。在离开过渡效果被触发时立即添加,在一帧后被移除。

  5. v-leave-active:离开动画的生效状态。应用于整个离开动画阶段。在离开过渡效果被触发时立即添加,在过渡或动画完成之后移除。这个 class 可以被用来定义离开动画的持续时间、延迟与速度曲线类型。

  6. v-leave-to:离开动画的结束状态。在一个离开动画被触发后的下一帧被添加 (也就是 v-leave-from 被移除的同时),在过渡或动画完成之后移除。

v-enter-active 和 v-leave-active 给我们提供了为进入和离开动画指定不同速度曲线的能力,我们将在下面的小节中看到一个示例。

我们可以给 <Transition> 组件传一个 name prop 来声明一个过渡效果名:

template

<Transition name="fade">
  ...
</Transition>

对于一个有名字的过渡效果,对它起作用的过渡 class 会以其名字而不是 v 作为前缀。比如,上方例子中被应用的 class 将会是 fade-enter-active 而不是 v-enter-active。这个“fade”过渡的 class 应该是这样:

css

CSS 的 animation#

原生 CSS 动画和 CSS transition 的应用方式基本上是相同的,只有一点不同,那就是 *-enter-from 不是在元素插入后立即移除,而是在一个 animationend 事件触发时被移除。

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

同时使用 transition 和 animation#

Vue 需要附加事件监听器,以便知道过渡何时结束。可以是 transitionend 或 animationend,这取决于你所应用的 CSS 规则。如果你仅仅使用二者的其中之一,Vue 可以自动探测到正确的类型。

然而在某些场景中,你或许想要在同一个元素上同时使用它们两个。举例来说,Vue 触发了一个 CSS 动画,同时鼠标悬停触发另一个 CSS 过渡。此时你需要显式地传入 type prop 来声明,告诉 Vue 需要关心哪种类型,传入的值是 animation 或 transition

深层级过渡与显式过渡时长#

尽管过渡 class 仅能应用在 <Transition> 的直接子元素上,我们还是可以使用深层级的 CSS 选择器,在深层级的元素上触发过渡效果。

 

性能考量#

你可能注意到我们上面例子中展示的动画所用到的 CSS 属性大多是 transform 和 opacity 之类的。用这些属性制作动画非常高效,因为:

  1. 他们在动画过程中不会影响到 DOM 结构,因此不会每一帧都触发昂贵的 CSS 布局重新计算。

  2. 大多数的现代浏览器都可以在执行 transform 动画时利用 GPU 进行硬件加速。

相比之下,像 height 或者 margin 这样的属性会触发 CSS 布局变动,因此执行它们的动画效果更昂贵,需要谨慎使用。我们可以在 CSS-Triggers 这类的网站查询哪些属性会在执行动画时触发 CSS 布局变动。

JavaScript 钩子#

你可以通过监听 <Transition> 组件事件的方式在过渡过程中挂上钩子函数:

 

 

这些钩子可以与 CSS 过渡或动画结合使用,也可以单独使用。

在使用仅由 JavaScript 执行的动画时,最好是添加一个 :css="false" prop。这显式地向 Vue 表明可以跳过对 CSS 过渡的自动探测。除了性能稍好一些之外,还可以防止 CSS 规则意外地干扰过渡效果。在有了 :css="false" 后,我们就自己全权负责控制什么时候过渡结束了。这种情况下对于 @enter 和 @leave 钩子来说,回调函数 done 就是必须的。否则,钩子将被同步调用,过渡将立即完成。

可复用过渡效果#

得益于 Vue 的组件系统,过渡效果是可以被封装复用的。要创建一个可被复用的过渡,我们需要为 <Transition> 组件创建一个包装组件,并向内传入插槽内容:

出现时过渡#

如果你想在某个节点初次渲染时应用一个过渡效果,你可以添加 appear prop:

 过渡模式

在之前的例子中,进入和离开的元素都是在同时开始动画的,因此我们不得不将它们设为 position: absolute 以避免二者同时存在时出现的布局问题。

然而,很多情况下这可能并不符合需求。我们可能想要先执行离开动画,然后在其完成之后再执行元素的进入动画。手动编排这样的动画是非常复杂的,好在我们可以通过向 <Transition> 传入一个 mode prop 来实现这个行为:

3-2、TransitionGroup组件

Senior Frontend Engineer, CoreSenior Frontend Engineer, Core

<TransitionGroup> 是一个内置组件,用于对 v-for 列表中的元素或组件的插入、移除和顺序改变添加动画效果。

和 <Transition> 的区别#

<TransitionGroup> 支持和 <Transition> 基本相同的 props、CSS 过渡 class 和 JavaScript 钩子监听器,但有以下几点区别:

  • 默认情况下,它不会渲染一个容器元素。但你可以通过传入 tag prop 来指定一个元素作为容器元素来渲染。

  • 过渡模式在这里不可用,因为我们不再是在互斥的元素之间进行切换。

  • 列表中的每个元素都必须有一个独一无二的 key attribute。

  • CSS 过渡 class 会被应用在列表内的元素上,而不是容器元素上。

  • Senior Frontend Developer

    Nordhealth Oy · Remote

    Front End Web Developer (Manager)

    Hudson Creative · New York City, NY, USA

    Jobs by vuejobs.com

  • 3-3 KeepAlive组件

  • <KeepAlive> 是一个内置组件,它的功能是在多个组件间动态切换时缓存被移除的组件实例。

    基本使用#

    在组件基础章节中,我们已经介绍了通过特殊的 <component> 元素来实现动态组件的用法:

    template

    <component :is="activeComponent" />
    

    默认情况下,一个组件实例在被替换掉后会被销毁。这会导致它丢失其中所有已变化的状态 —— 当这个组件再一次被显示时,会创建一个只带有初始状态的新实例。

    在下面的例子中,你会看到两个有状态的组件——A 有一个计数器,而 B 有一个通过 v-model 同步 input 框输入内容的文字展示。尝试先更改一下任意一个组件的状态,然后切走,再切回来:

    你会发现在切回来之后,之前已更改的状态都被重置了。

    在切换时创建新的组件实例通常是有意义的,但在这个例子中,我们的确想要组件能在被“切走”的时候保留它们的状态。要解决这个问题,我们可以用 <KeepAlive> 内置组件将这些动态组件包装起来:

    template

    <!-- 非活跃的组件将会被缓存! -->
    <KeepAlive>
      <component :is="activeComponent" />
    </KeepAlive>
    

    现在,在组件切换时状态也能被保留了:

    在演练场中尝试一下

    TIP

    在 DOM 模板中使用时,它应该被写为 <keep-alive>

    包含/排除#

    <KeepAlive> 默认会缓存内部的所有组件实例,但我们可以通过 include 和 exclude prop 来定制该行为。这两个 prop 的值都可以是一个以英文逗号分隔的字符串、一个正则表达式,或是包含这两种类型的一个数组:

    template

    <!-- 以英文逗号分隔的字符串 -->
    <KeepAlive include="a,b">
      <component :is="view" />
    </KeepAlive>
    
    <!-- 正则表达式 (需使用 `v-bind`) -->
    <KeepAlive :include="/a|b/">
      <component :is="view" />
    </KeepAlive>
    
    <!-- 数组 (需使用 `v-bind`) -->
    <KeepAlive :include="['a', 'b']">
      <component :is="view" />
    </KeepAlive>
    

    它会根据组件的 name 选项进行匹配,所以组件如果想要条件性地被 KeepAlive 缓存,就必须显式声明一个 name 选项。

    TIP

    在 3.2.34 或以上的版本中,使用 <script setup> 的单文件组件会自动根据文件名生成对应的 name 选项,无需再手动声明。

    最大缓存实例数#

    我们可以通过传入 max prop 来限制可被缓存的最大组件实例数。<KeepAlive> 的行为在指定了 max 后类似一个 LRU 缓存:如果缓存的实例数量即将超过指定的那个最大数量,则最久没有被访问的缓存实例将被销毁,以便为新的实例腾出空间。

    template

    <KeepAlive :max="10">
      <component :is="activeComponent" />
    </KeepAlive>
    

    缓存实例的生命周期#

    当一个组件实例从 DOM 上移除但因为被 <KeepAlive> 缓存而仍作为组件树的一部分时,它将变为不活跃状态而不是被卸载。当一个组件实例作为缓存树的一部分插入到 DOM 中时,它将重新被激活

    一个持续存在的组件可以通过 onActivated() 和 onDeactivated() 注册相应的两个状态的生命周期钩子:

    vue

    <script setup>
    import { onActivated, onDeactivated } from 'vue'
    
    onActivated(() => {
      // 调用时机为首次挂载
      // 以及每次从缓存中被重新插入时
    })
    
    onDeactivated(() => {
      // 在从 DOM 上移除、进入缓存
      // 以及组件卸载时调用
    })
    </script>
    

    请注意:

  • onActivated 在组件挂载时也会调用,并且 onDeactivated 在组件卸载时也会调用。

  • 这两个钩子不仅适用于 <KeepAlive> 缓存的根组件,也适用于缓存树中的后代组件。

3-4、Teleport 组件

<style>
.modal-mask {
  position: fixed;
  z-index: 9998;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: table;
  transition: opacity 0.3s ease;
}

.modal-wrapper {
  display: table-cell;
  vertical-align: middle;
}

.modal-container {
  width: 300px;
  margin: 0px auto;
  padding: 20px 30px;
  background-color: #fff;
  border-radius: 2px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
  transition: all 0.3s ease;
}

.modal-header h3 {
  margin-top: 0;
  color: #42b983;
}

.modal-body {
  margin: 20px 0;
}

.modal-default-button {
  float: right;
}

/*
 * 对于 transition="modal" 的元素来说
 * 当通过 Vue.js 切换它们的可见性时
 * 以下样式会被自动应用。
 *
 * 你可以简单地通过编辑这些样式
 * 来体验该模态框的过渡效果。
 */

.modal-enter-from {
  opacity: 0;
}

.modal-leave-to {
  opacity: 0;
}

.modal-enter-from .modal-container,
.modal-leave-to .modal-container {
  -webkit-transform: scale(1.1);
  transform: scale(1.1);
}
</style>

3-5、Suspense组件

4、响应式-进阶

<template>
  <!-- 和 ref() 不同,浅层 ref 的内部值将会原样存储和暴露,并且不会被深层递归地转为响应式。
只有对 .value 的访问是响应式的。
shallowRef() 常常用于对大型数据结构的性能优化或是与外部的状态管理系统集成。 -->
  <!-- shallowRef-->
  <div>
    <div>{{ data.count }}</div>
    <button @click="clickBtnNoChange">非响应</button>
    <button @click="clickBtnChange">响应</button>
  </div>

  <!-- customRef -->
  <div>
    <p>1s之后更新</p>
    <p>{{ text }}</p>
    <input type="text" v-model="text" />
  </div>

  <!-- shallowReactive -->
  <div>
    <p>shallowReactive</p>
    <div>{{ stateReactive.foo }}</div>
    <button @click="clickBtnNoChange1">非响应</button>
    <button @click="clickBtnChange1">响应</button>
  </div>

  <!-- effectScope() -->
  <div>
    <p>effectScope</p>
    <p>{{ counter}}</p>
   
  </div>
  


</template>

<script lang="ts" setup>
import { useDebouncedRef } from "./debounceRef";
import {
  shallowRef,
  reactive,
  ref,
  watch,
  shallowReactive,
  getCurrentInstance,
  triggerRef,
  watchEffect,
  isReactive,
  effectScope,
  computed,
} from "vue-demi";

//shallowRef

const text = useDebouncedRef("hello", 1000);
const data = shallowRef({ count: 1 });
//不会触发响应式-修改数据
const clickBtnNoChange = () => {
  data.value.count++;
  console.log(data.value.count++, " data.value.count++===========");
};
//会修改数据源
const clickBtnChange = () => {
  const random = Math.floor(Math.random() * 10 - 1);
  data.value = { count: random };
};
const state = shallowRef({ count: 1 });
state.value = { count: 2 };
const shallow = shallowRef({
  greet: "Hello world",
});
// 触发该副作用第一次应该会打印 "Hello, world"
watchEffect(() => {
  console.log(shallow.value.greet);
});
// 这次变更不应触发副作用,因为这个 ref 是浅层的
shallow.value.greet = "hello universe";
// 打印 "hello, universe"
triggerRef(shallow);

// shallowReactive

const stateReactive = shallowReactive({
  foo: 1,
  nested: {
    bar: 2,
  },
});

//不会触发响应式-修改数据
const clickBtnNoChange1 = () => {
  //但下层嵌套对象不会被转化为响应式
  isReactive(stateReactive.nested); //false
  //不是响应式的
  stateReactive.nested.bar++;
};
//会修改数据源
const clickBtnChange1 = () => {
  stateReactive.foo++;
  console.log(stateReactive.foo++, "stateReactive.foo++===========");
};
//更改状态自身的属性是响应式的
stateReactive.foo++;
//但下层嵌套对象不会被转化为响应式
isReactive(stateReactive.nested); //false
//不是响应式的
stateReactive.nested.bar++;

// effectScope()

const disposables=[]
const counter=ref(1)
const scope=effectScope()
scope.run(()=>{
    const doubled=computed(()=>counter.value*2)
    console.log(doubled,'doubled===============')
    watch(doubled,()=>  {
     console.log(doubled.value)
    }
 )
    watchEffect(()=>console.log("Count:",doubled.value))
})
//处理掉当前作用域内的所有 effect
scope.stop()
console.log(scope.stop(),'scope.stop()=============')
disposables.push(()=>scope.stop())
console.log(disposables,'disposables=============')

</script>
import {customRef} from 'vue'

export function useDebouncedRef(value:any,delay=200){
    let timeout:any
    return customRef((track,trigger)=>{
        return {
            get(){
                track()
                return value
            },
            set(newValue){
                clearTimeout(timeout)
                timeout=setTimeout(()=>{
                    value=newValue
                    trigger()
                },delay)
            }
        }
    })
}

5、由于少了 export,没法传参,也不方便暴露接口,所以就增加了三个工具方法

  • defineProps

  • defineEmits

  • defineExpose

注意,这三个工具方法只是帮助 Vue 编译器构建组件,它们不会出现在最终代码里,我们也不能预期它们会像普通函数那样工作。比如下面这段代码,就得不到常见的结果:

const props = defineProps({
  userMenu: {
    type: Array,
    default() {
      return []
    }
  }
})
console.log(props) // 该对象中的 userName 总是有值
console.log(props.userMenu) // 该对象始终是一个空数据
因为 Vue 是 MVVM 框架,它的视图会在数据变化后自动渲染,于是通常情况下,
props 里的值什么时候被填充并不重要,Vue 开发团队也不想追求 defineProps 工作的一般化。
所以使用目前版本,上面这段代码,访问到的 props 是 reactive 对象,数据被填充后就能看到
 userName 里有值;
而 props.userMenu 在访问时还没有被填充,所以得到的是 default() 返回的默认值,一直是空的。

同时大家还要知道,console.log() 输出的是对象的指针,而非快照。
所以里面的值只跟你展开时有关,跟运行时关系不大

语法

在 script setup 中必须使用 defineProps 和 defineEmits API 来声明 props 和 emits ,它们具备完整的类型推断并且在 script setup 中是直接可用的

defineProps :

1、适用于父组件向子组件传递属性

子组件

   // 定义Props
    const props = defineProps<{
        result: number,
        name: string
    }>()

父组件 

<Detail name="结果" :result="1"></Detail>

2、带默认值的defineProps

适用于带默认值的Props,经测试不能与defineProps在同一组件同时定义。

子组件 

 interface IProps {
        labels?: string[]
        result: number,
        name:string     
    }

    // 定义带默认值的Props
    const defaultProps = withDefaults(defineProps<IProps>(), {
        name: 'hello',
        result:0,
        labels: () => ['one', 'two']
    })

父组件

<!-- 没传的Props会使用子组件的默认值 -->
 <Detail name="结果"></Detail>         

defineEmits

适用于父组件向子组件传递方法

子组件 

  <button @click="btnAdd">添加</button>
    <hr />
  <button @click="btnReset">重置</button>
   <hr />

// 定义Emits
    const emits = defineEmits<{
        (e: 'add', id: number): void
        (e: 'reset', value: number): void
    }>()

    const btnAdd = () => {
        emits('add',2)
    }
    const btnReset = () => {
        emits("reset",0)
    }

夫组件

<!-- 传入Emits -->
<Detail @add="add" @reset="reset"></Detail>

const result = ref<number>(0);

const add = (num:number)=>{
    result.value+=num
}

const reset = (num:number)=>{
    result.value = num
}

defineExpose

适用于子组件向父组件暴露方法和属性,父组件通过子组件示例进行调用。

子组件 

// 定义
Expose const exposeStr = ref<string>("") defineExpose({ exposeStr })

父组件 

 <!-- 传入Props和Emits -->
<Detail ref="detail"></Detail>

// 必须跟组件ref保持一致
const detail = ref()

setTimeout (() => {
    detail.value.exposeStr = "exposeStr"
},1000)

完整代码 

子组件:detail.vue

<template>
  <div>
    <!-- {{props.name}} - {{props.result}} -->
    <hr />
    {{defaultProps.name}} - {{defaultProps.result}} - {{defaultProps.labels}}
    <hr />
    <button @click="btnAdd">添加</button>
    <hr />
    <button @click="btnReset">重置</button>
    <hr />
    {{exposeStr}}
  </div>
</template>

<script setup lang="ts">
    import { ref,defineProps ,defineEmits,defineExpose} from "vue"

    // 定义Props
    // const props = defineProps<{
    //     result: number,
    //     name: string
    // }>()

    interface IProps {
        labels?: string[]
        result: number,
        name:string     
    }

    // 定义带默认值的Props
    const defaultProps = withDefaults(defineProps<IProps>(), {
        name: 'hello',
        result:0,
        labels: () => ['one', 'two']
    })

    // 定义Emits
    const emits = defineEmits<{
        (e: 'add', id: number): void
        (e: 'reset', value: number): void
    }>()

    const btnAdd = () => {
        emits('add',2)
    }
    const btnReset = () => {
        emits("reset",0)
    }

    // 定义Expose
    const exposeStr = ref<string>("")
    defineExpose({
        exposeStr
    })

</script>
<style lang="scss" scoped></style>

父组件:main.vue

<template>
    <div>
        <!-- 传入Props和Emits -->
        <Detail ref="detail" name="结果" :result="result" @add="add" @reset="reset"></Detail>
    </div>
</template>

<script setup lang="ts">
import Detail from './detail.vue'

import {ref} from 'vue'

const result = ref<number>(0);

const add = (num:number)=>{
    result.value+=num
}

const reset = (num:number)=>{
    result.value = num
}

// 必须跟子组件ref名保持一致
const detail = ref()
setTimeout (() => {
    // 调用子组件属性并修改值
    detail.value.exposeStr = "exposeStr"
},1000)
</script>

6、其他的Composition API

6.1.shallowReactive与shallowRef

shallowReactive:只处理对象最外层属性的响应式(浅响应式)只考虑第一层数据的响应式。
shallowRef:只处理基本数据类型的响应式,不进行对象的响应式处理,传递基本数据类型的话跟ref没有任何区别,ref是可以进行对象的响应式处理的
我们正常的ref创建的数据,里面的.value是一个proxy,而shallowRef创建的数据 .value里面是一个object数据类型,所以不会响应式数据

什么时候使用?:
如果有一个对象数据,结构比较深,但变化时只是外层属性变化 ===> shallowReactive
如果有一个对象数据,后续功能不会修改对象中的属性,而是生新的对象来替换 ===> shallowRef

2.readonly与shallowReadonly

readonly:让一个响应式的数据变成只读的(深只读)
shallowReadonly: 让一个响应式数据变成只读的(浅只读)
应用场景:不希望数据被修改的时候

<script>
    import { reactive,readonly,shallowReadonly } from 'vue'
    export default {
        name: 'test9',
        setup(){
            let person = reactive({
                name: '张三',
                job:{
                    salary: '20k',
                }
            })
            person = readonly(person) //这个时候修改人的信息就不会改变了,所有的都不能改
            /**
            * 页面不进行响应式的改变,一般存在两种情况:
            * 1.setup里面定义的数据改变了,但是vue没有检测到,这个时候是不会改变的
            * 2.setup里面定义的数据压根儿就不让你改,这个时候也没法响应式
            */
            person = shallowReadonly(person) //只有最外层不能修改是只读的,但是job还是可以改的
            return {
                person
            }
        },
    }
</script>

3.toRaw与markRaw

toRaw
作用:将一个由reactive生成的响应式对象转换为普通对象
使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新
markRaw:
作用:标记一个对象,使其永远不会再成为响应式对象
使用场景:

1.有些值不应被设置成响应式的,例如复杂的第三方类库等
2.当渲染具有不可变数据的大列表时候,跳过响应式转换可以提高性能
import {reactive,toRaw,markRaw} from 'vue'
setup(){
    let person = reactive({
        name: '张三',
    })
    function showRawPerson(){
        const p = toRaw(person)
        p.age++
        console.log(p)
    }
    function addCar(){
        let car = {name: '奔驰'}
        person.car = markRaw(car) //一旦这么做时候,他就永远不能当成响应式数据去做了
    }
}


4.customRef

创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显示控制
实现防抖效果:

<template>
    <input type="text" v-model="keyword">
    <h3>{{keyword}}</h3>
</template>

<script>
    import { customRef, ref } from 'vue'

    export default {
        name: 'test10',
        setup(){
            let timer;
            //自定义一个ref——名为: myRef
            function myRef(value){
                return customRef((track,trigger)=>{
                    return {
                        get(){
                            console.log(`有人读取我的值了,要把${value}给他`);  //两次输出: v-model读取  h3里面的插值语法调了一次
                            track()  //追踪一下改变的数据(提前跟get商量一下,让他认为是有用的)
                            return value
                        },
                        set(newValue){
                            console.log(`有人把myRef这个容器中数据改了:${newValue}`);
                            clearTimeout(timer)
                            timer = setTimeout(()=>{
                                value = newValue
                                trigger() //通知vue去重新解析模板,重新再一次调用get()
                            },500)
                        }
                    }
                })
            }

            // let keyword = ref('hello')  //使用内置提供的ref
            let keyword = myRef('hello')  //使用自定义的ref
            return {
                keyword,
            }
        },
        
    }
</script>


7.provide与inject

作用:实现祖孙组件间的通信
套路:父组件有一个provide选项提供数据,子组件有一个inject选项来开始使用这些数据
具体写法:

//父组件
<script setup>

import { ref,reactive,toRefs,provide } from 'vue';
import ChildVue from './components/Child.vue';

let car = reactive({
  name: '奔驰',
  price: '40w'
})
provide('car',car) //给自己的后代组件传递数据
const {name, price} = toRefs(car)
</script>
<template>
  <div class="app">
    <h3>我是父组件, {{name}}--{{price}}</h3>
    <ChildVue></ChildVue>
  </div>
</template>
<style>
.app{
  background-color: gray;
  padding: 10px;
  box-sizing: border-box;
}
</style>

//子组件
<script setup>
import { ref } from '@vue/reactivity';
import SonVue from './Son.vue';
</script>

<template>
  <div class="app2">
    <h3>我是子组件</h3>
    <SonVue></SonVue>
  </div>
</template>

<style>
.app2{
  background-color: rgb(82, 150, 214);
  padding: 10px;
  box-sizing: border-box;
}
</style>
//孙组件
<script setup>

import { ref,inject } from 'vue';
let car = inject('car') //拿到父组件的数据
const {name, price} = car
</script>

<template>
  <div class="app3">
    <h3>我是孙组件</h3>
    <p>{{name}}-{{price}}</p>
  </div>
</template>
<style>
.app3{
  background-color: rgb(231, 184, 56);
  padding: 10px;
  box-sizing: border-box;
}
</style>

8、TypeScript 与组合式 API

1.为组件的 props 标注类型

// 场景一: 使用<script setup >

<script setup lang="ts">

const props = defineProps({

  foo: { type: String, required: true },

  bar: Number

})

props.foo // string

props.bar // number | undefined

</script>

//也可以将 props 的类型移入一个单独的接口中

<script setup lang="ts">

interface Props {

  foo: string

  bar?: number

}

const props = defineProps<Props>()

</script>
//场景二: 不使用<script setup>

import { defineComponent } from 'vue'

export default defineComponent({

  props: {

    message: String

  },

  setup(props) {

    props.message // <-- 类型:string

  }

})


注意点:为了生成正确的运行时代码,传给 defineProps() 的泛型参数必须是以下之一:

//1.一个类型字面量:

defineProps<{ /*... */ }>()

//2.对同一个文件中的一个接口或对象类型字面量的引用

interface Props {/* ... */}

defineProps<Props>()

//3.接口或对象字面类型可以包含从其他文件导入的类型引用,但是,传递给 defineProps 的泛型参数本身不能是一个导入的类型:

import { Props } from './other-file'

// 不支持!

defineProps<Props>()

Props 解构默认值

//当使用基于类型的声明时,失去了对 props 定义默认值的能力。通过目前实验性的响应性语法糖来解决:

<script setup lang="ts">

interface Props {

  foo: string

  bar?: number

}

// 对 defineProps() 的响应性解构

// 默认值会被编译为等价的运行时选项

const { foo, bar = 100 } = defineProps<Props>()

</script>

2.为组件的 emits 标注类型

//场景一: 使用<script setup>

<script setup lang="ts">

const emit = defineEmits(['change', 'update'])

// 基于类型

const emit = defineEmits<{

  (e: 'change', id: number): void

  (e: 'update', value: string): void

}>()

</script>




//场景二: 不使用<script setup>

import { defineComponent } from 'vue'



export default defineComponent({

  emits: ['change'],

  setup(props, { emit }) {

    emit('change') // <-- 类型检查 / 自动补全

  }

})

3.为 ref() 标注类型

import { ref } from 'vue'

import type { Ref } from 'vue'

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

// 推导出的类型:Ref<number>

const year = ref(2020)

// => TS Error: Type 'string' is not assignable to type 'number'.

year.value = '2020'



//2.指定一个更复杂的类型,可以通过使用 Ref 这个类型:

const year: Ref<string | number> = ref('2020')

year.value = 2020 // 成功!



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

// 得到的类型:Ref<string | number>

const year = ref<string | number>('2020')

year.value = 2020 // 成功!



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

// 推导得到的类型:Ref<number | undefined>

const n = ref<number>()

4.为reactive() 标注类型

import { reactive } from 'vue'

//1.reactive() 也会隐式地从它的参数中推导类型:

// 推导得到的类型:{ title: string }

const book = reactive({ title: 'Vue 3 指引' })



//2.要显式地标注一个 reactive 变量的类型,我们可以使用接口:

interface Book {

  title: string

  year?: number

}

const book: Book = reactive({ title: 'Vue 3 指引' })

5.为 computed() 标注类型

import { ref, computed } from 'vue'

//1.computed() 会自动从其计算函数的返回值上推导出类型:

const count = ref(0)



// 推导得到的类型:ComputedRef<number>

const double = computed(() => count.value * 2)



// => TS Error: Property 'split' does not exist on type 'number'

const result = double.value.split('')



//2.通过泛型参数显式指定类型:

const double = computed<number>(() => {

  // 若返回值不是 number 类型则会报错

})


6.为事件处理函数标注类型

//在处理原生 DOM 事件时,应该为我们传递给事件处理函数的参数正确地标注类型

<script setup lang="ts">

function handleChange(event) {

  // 没有类型标注时 `event` 隐式地标注为 `any` 类型,

  // 这也会在 tsconfig.json 中配置了 "strict": true 或 "noImplicitAny": true 时报出一个 TS 错误。

  console.log(event.target.value)

}

</script>



<template>

  <input type="text" @change="handleChange" />

</template>



//因此,建议显式地为事件处理函数的参数标注类型,需要显式地强制转换 event 上的属性:

function handleChange(event: Event) {

  console.log((event.target as HTMLInputElement).value)

}

7.为 provide / inject 标注类型

/*

provide 和 inject 通常会在不同的组件中运行。要正确地为注入的值标记类型,

Vue 提供了一个 InjectionKey 接口,它是一个继承自 Symbol 的泛型类型,

可以用来在提供者和消费者之间同步注入值的类型:

*/

import { provide, inject } from 'vue'

import type { InjectionKey } from 'vue'



const key = Symbol() as InjectionKey<string>



provide(key, 'foo') // 若提供的是非字符串值会导致错误



const foo = inject(key) // foo 的类型:string | undefined



//建议将注入 key 的类型放在一个单独的文件中,这样它就可以被多个组件导入。

//当使用字符串注入 key 时,注入值的类型是 unknown,需要通过泛型参数显式声明:

const foo = inject<string>('foo') // 类型:string | undefined



//注意注入的值仍然可以是 undefined,因为无法保证提供者一定会在运行时 provide 这个值。

//当提供了一个默认值后,这个 undefined 类型就可以被移除:

const foo = inject<string>('foo', 'bar') // 类型:string



//如果你确定该值将始终被提供,则还可以强制转换该值:

const foo = inject('foo') as string

8.为模板引用标注类型

//模板引用需要通过一个显式指定的泛型参数和一个初始值 null 来创建:

<script setup lang="ts">

import { ref, onMounted } from 'vue'



const el = ref<HTMLInputElement | null>(null)



onMounted(() => {

  el.value?.focus()

})

</script>

/**

    注意为了严格的类型安全,有必要在访问 el.value 时使用可选链或类型守卫。这是因为直到组件被挂载前,

    这个 ref 的值都是初始的 null,并且在由于 v-if 的行为将引用的元素卸载时也可以被设置为 null。

*/

<template>

  <input ref="el" />

</template>

9.为组件模板引用标注类型

//有时,你可能需要为一个子组件添加一个模板引用,以便调用它公开的方法。举例来说,我们有一个 MyModal 子组件,它有一个打开模态框的方法

<!-- MyModal.vue -->

<script setup lang="ts">

import { ref } from 'vue'

const isContentShown = ref(false)

const open = () => (isContentShown.value = true)

defineExpose({

  open

})

</script>

//为了获取 MyModal 的类型,我们首先需要通过 typeof 得到其类型,再使用 TypeScript 内置的 InstanceType 工具类型来获取其实例类型:

<!-- App.vue -->

<script setup lang="ts">

import MyModal from './MyModal.vue'

const modal = ref<InstanceType<typeof MyModal> | null>(null)

const openModal = () => {

  modal.value?.open()

}

</script>

注意,如果你想在 TypeScript 文件而不是在 Vue SFC 中使用这种技巧,需要开启 Volar 的Takeover 模式。

9、Vuex与组合式API

组合式API 可以通过调用 useStore 函数,来在 setup 钩子函数中访问 store。这与在组件中使用选项式 API 访问 this.$store 是等效的。

import { useStore } from 'vuex'
export default {
  setup () {
    const store = useStore()
  }
}

1.访问 state 和 getter

为了访问 state 和 getter,需要创建 computed 引用以保留响应性,这与在选项式 API 中创建计算属性等效。

import { computed } from 'vue'

import { useStore } from 'vuex'

export default {

  setup () {

    const store = useStore()

    return {

      // 在 computed 函数中访问 state

      count: computed(() => store.state.count),

      // 在 computed 函数中访问 getter

      double: computed(() => store.getters.double)

    }

  }

}

2.访问 Mutation 和 Action

要使用 mutation 和 action 时,只需要在 setup 钩子函数中调用 commit 和 dispatch 函数。

import { useStore } from 'vuex'

export default {

  setup () {

    const store = useStore()

    return {

      // 使用 mutation

      increment: () => store.commit('increment'),

      // 使用 action

      asyncIncrement: () => store.dispatch('asyncIncrement')

    }

  }

}

后续更新中。。。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐