花十分钟了解Vue3-Composition API的特性

331 阅读4分钟
相比较于Vue2的Options API (选项式API) Vue3中转而使用了灵活性更高且组织和重用逻辑更强的Composition API(组合式API)
  • 选项式API从字面意思可以理解为使用包含多个选项的对象来描述组件的逻辑(data、methods、computed...),更适用于一些简单的构建场景
  • 组合式API则是使用函数来描述组件的逻辑,将所有需要使用到的api动态导入组件,通过直接在函数作用域中直接定义响应式状态并将多个函数中得到的状态组合起来处理复杂的逻辑,更合适需要构建复杂且完整的应用场景时使用

1. 两种API的初始化代码示例

//组合式API
<script>
import { ref, onMounted, computed } from "vue"
export default{
    setup(){
        const foo = ref(0)
        //需要主动暴露属性给模板以及其它钩子
        return{
            foo
        }
    }
}
</script>
//选项式API
<script>
export default {
    data (){
        return {
            foo: 0
        }
    },
    //mounted() {},
    //methods:{},
    //computed:{},
    ....
}
</script>

2. setup( )

  • setup( )函数是组合式API中一个特殊的钩子,它是使用组合式API的入口,如果要在模板中使用响应式状态都需要在setup函数中定义并返回
  • 但由于需要不断的手动暴露方法和状态过于繁琐,vue提供了一个<script setup> 构建工具来简化操作,它是一个告知使用组合式API编译的语法糖,在单文件组件中,模板内可以直接使用<script setup>顶层的导入和声明的变量,不需要再频繁的返回定义
//未使用<script setup>
<script>
import { ref } from "vue"
export default{
    setup(){
        const foo = ref(0)
        function handleUpdateFoo(){ 
            foo.value++
        }
        //需要主动暴露属性、函数给模板以及其它钩子
        return{
            foo,
            handleUpdateFoo
        }
    }
}
</script>

//使用了<script setup>
<script setup>
import { ref, onMounted, computed } from "vue"

const foo = ref(0)
onMounted(()=>{})
function handleUpdateFoo(){ 
    foo.value++
}
const sum = computed(()=>{})
</script>

3. 响应式状态声明

3.1 ref

  • ref会将传入的参数包装成一个带value属性的 ref 对象,value属性是响应式的;
  • ref可以定义任何值类型的响应式状态
  • ref定义的响应式变量在传递给函数时不会丢失响应性,但对ref定义的响应式对象直接解构赋值会丢失响应性,需要使用一般对象对ref进行分组之后再解构就不会丢失响应性
<script setup>
import {ref} from 'vue'

const count = ref(0)  //相当于count:{ value: 0 },函数中通过count.value访问
const objRef = ref({ count: 0 })  //函数中通过objRef.value.count访问count属性

const {value : foo} = count //响应性丢失
const newCount = count.value //响应性丢失

const object = { //使用一般对象对ref进行分组
    foo: ref(0),
    bar: ref(1)
}
const { foo, bar } = object //保持响应性
</script>

3.2 reactive

  • reactive定义一个响应式的对象或者数组
  • reactive仅适用于定义对象类型的数据,原始类型无效
  • reactive定义的响应式对象不等于原对象,其返回的是一个原对象的proxy,是一个代理对象,只有代理对象才具备响应性,修改原对象并不会触发更新
  • reactive定义的响应式对象的属性被赋值或者被解构至本地变量时,或是将该属性传入一个函数时均会丢失响应性
<script setup>
import {reactive} from 'vue'
const obj = reactive({ count: 0 })  //直接使用obj.count访问count属性

const state = {}
const proxy = reactive(state)
console.log(proxy === state) //false

const state = reactive({ count: 0 })
let foo = state.count  //响应性丢失
foo++ // 不影响原始的 state

let { count } = state  //响应性丢失
count++ // 不会影响原始的state

// 将属性传入一个函数,将无法跟踪 state.count 的变化
function callSomeFunction(state.count){}
</script>

4. computed 计算属性

  • computed()方法的返回值为一个计算属性ref, 与一般的ref对象相似可以使用value属性获取计算的结果
const computedFoo = computed(()=>{
    return a > 1 ? true : false 
})
//computedFoo.value 获取计算结果

5. watch 侦听器

  • 由于组合式API侦听器的第一个参数只接收ref/reactive对象、getter/effect函数和数组,因此无法直接侦听响应式对象的某一个属性,需要使用一个getter函数返回该属性进行侦听,而在选项式API中则支持使用字符串形式用.分隔属性的方式进行侦听
watch(foo, (newVal, oldVal)=>{
    ....
},
{deep: ture},      //强制开启深度侦听
{immediate: ture}, //立刻执行回调
{flush: 'post'}    //开启在回调中访问组件更新后的DOM
)
//侦听响应式对象某个属性时需要使用getter返回该属性
watch(
        ()=>obj.attribute,
        (newVal)=>{
            ....
        }
)

5.1 watchEffect

组合式API中新增了watchEffect()函数,它与watch()的区别在于使用时可以不向它传递第一个参数,它会自动追踪回调中的响应式参数,只要参数发生变化watchEffect()就会执行,并且它是立刻执行的一旦创建就会立刻执行一次

//watchEffect不需要传递源参数,自动追踪回调中的依赖
const obj = reactive({count: 1})
function handleClick(){
    obj.count++
}
watchEffect(()=>{
  alert(`I'm ${obj.count} now`)})

5.2 flush: 'post'

默认情况下侦听器的回调会在Vue的组件更新之前被调用,因此在回调里访问到的都是更新之前的DOM,如果需要在回调中使用更新之后的DOM节点就需要设置flush: 'post'选项,而在组合式API中可以直接使用watchPostEffect来默认开启这个选项

watchPostEffect(()=>{
    //回调默认在组件更新之后执行
})

6. 组件 ref 引用

  • 组合式API与选项式API一样都可以在模板中直接使用ref属性来引用DOM节点,但写法不相同,在组合式API中需要先声明一个与模板中定义的ref同名的变量来表示引用
<template>
    <input ref="input">
</template>

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

const input = ref(null)  //声明一个与模板中定义的ref同名的变量
onMounted(()=>{
    input.value.focus()
})
</script>
  • 在组合式API中,使用ref引用一个组件时需要注意的是,如果组件使用了<script setup>,那么这个组件是默认私有的,组件内的任何属性无法被引用了它的其他组件访问到,此时需要使用到defineExpose主动将需要被其它组件访问的状态暴露出来
//当使用ref引用了一个组件,并且组件使用了<script setup>时
//父组件
<template>
    <Child/ ref="child">   //<script setup>中导入的组件可以在模板直接使用
</template>

<script setup>
import {ref, onMounted} from 'vue'
import Child from "./child.vue"

const child = ref(null)
onMounted(()=>{
    console.log(child.value.foo)   //如果child没使用defineExpose暴露,此处为undefined
})
</script>

//子组件
<script setup>
import {ref} from 'vue'

const foo = ref(1)
defineExpose({  //编译器宏不需要导入即可使用
    foo
})
</script>

7. props 组件通信

  • 在组合式API中如果使用了<script setup>的组件可以通过defineProps声明传递过来的参数
  • 在没有使用<script setup>时需要通过props选项声明
//使用了<script setup>
<script setup>
//通过字符串数组声明
const props = defineProps(['foo'])  //使用props.foo访问
//通过对象形式声明
defineProps({
    num: Number,
    foo:{
        type: String,
        default: "default", //用于声明默认值
        required: true //是否必传
    }
})
</script>

//没有使用<script setup>
export default {
    props:['foo'],
    setup(props){
        console.log(props.foo)
    }
}

同样的,不管使用哪种风格的API,props 始终都必须遵循单向绑定原则,子组件不被允许修改父组件的状态,最稳妥的做法是在子组件中重新定义一个局部数据属性并将父组件传递过来的状态作为初始值,后续直接操作这个局部属性

8. provide/inject 组件透传

  • provide/inject无法在函数内部的局部作用域使用,当在setup()中使用时必须是同步调用
  • provide提供的值如果是一个refinject接收的也是一个ref对象而不会自动解包
//组合式API
//祖先组件
<script setup>
import { ref, provide } from 'vue'
provide(/*注入名*/ "key", /*值*/ "value")

//如果提供的值是响应式的ref,接收到值的也是该ref对象不会自动解包
const foo = ref(0)
provide("foo", foo)
</script>

//后代组件
<script setup>
import { inject } from "vue"
const key = inject("key")  //"value"
const foo = inject("foo")  //{ value:0 }
const key2 = inject("key", "默认值")  //可选,找不到注入值又未声明默认值时会抛出警告
</script>
//选项式API
//祖先组件
//通过对象形式使用
export default{
    provide: {  
        foo: 0
    }
//通过函数形式使用
    provide(){
        return {
            foo: this.foo  //函数形式使用时可以访问到this实例
        }
    }
}

//后代组件
export default{
    inject: ['foo'], 
    data(){
        return{
            childFoo: this.foo  //data中可以访问到接收的属性
        }
    }
......
//选项式API中如果要为inject设置默认值时必须使用对象的形式声明
    inject:{
        foo:{
            default: "默认值"
        }
    }
}

9. 触发与监听事件

  • 组合式API中 <script setup>的内部无法使用$emit,此时需要使用defineEmits()函数来声明需要触发的事件;但在模板中的使用不受这部分影响
  • defineEmits()只能在<script setup>的顶级作用域使用,无法使用于函数的局部作用域内
//父组件
<Child @some-event="someEvent" />

//子组件
<script setup>
const emit = defineEmits(['someEvent'])
emit('someEvent', 需要传递给父组件的参数)
<script>

<template>
    <div @click="$emit('someEvent')"></div> //模板使用$emit不受影响
</template>

10. 组件生命周期

  • 对比 Vue2 生命周期钩子的名称发生了变化
  • Vue3 中beforeCreate、created两个生命周期钩子,已经包含在了setup()钩子函数内,并且其要早于beforeCreate和created之前执行
//组件式API                //选项式API
setup()           ->     beforeCreate()
setup()           ->     created()
onBeforeMount()   ->     beforeMount()
onMounted()       ->     mounted()
onBeforeUpdate()  ->     beforeUpdate()
onUpdated()       ->     updated()
onBeforeUnmount() ->     beforeDestroy()
onUnmounted()     ->     destroyed()
onActivated()     ->     activated()
onDeactivated()   ->     deactivated()