一篇文章搞定composition-api

1,474 阅读7分钟

Option API 的写法会碰到的问题?

明明是相同操作逻辑,却散落在元件的个地方,导致对应属性的程式码有所增长,这样时间久了不仅不利于程式码维护,而之后想再使用某功能,却因为资料状态依赖关系 也无法重复使用

在当时 Vue 有提供几种方式来解决像是 自定义指令 directive,minxin,但是仍然难以解决Option API 难以被重复使用的缺点

Composition API 程式能依功能分类来使用,可增加阅读性之外同时还可以 跨元件使用来增加复用性

<script setup>

  1. script setup中会自动注册引入的元件,也就是说,它会以档名为主作为元件,这样就不用再写name属性来定义了
  2. 所有的变数、函式都可以直接给模板(template)使用,不需要再return

defineProps 和 defineEmits

defineProps和 defineEmits 主要是用来在<script setup>定义 props 与 emits,而其中这两个接受的值与我们过去定义 props 与 emits 的值相同。

<script setup>
const props = defineProps({
  foo: String
})

const emit = defineEmits(['change', 'delete'])
// setup code
</script>

useSlots 和 useAttrs

针对 $slots$attrs,官方文件有提到通常会直接通过 模板来使用它,不过当你需要在<script setup>中使用它们时,可以引入 useSlotsuseAttrs来读取。

   <script setup>
        import { useSlots, useAttrs } from 'vue'
        const slots = useSlots()
        const attrs = useAttrs()
        
    </script>

Setup()

setup 函式 为 Composition API启动元件的初始入口位置,而在其函式内通常会包含 生命周期 hook状态资料 计算属性 等。最后再将模板需要使用的状态及功能回传出来给 template 就可以了。

其中在元件内的 功能逻辑与资料状态其实不一定要全写在setup,可以依照情境需求适时地抽出来,当需要使用时再 import 进来使用即可, 而这样做的好处主要是可以让元件内的 setup 函式利于 程式码阅读及维护。

setup 中的参数 props/context

当元件启动时,在 setup 函式中会分别带入 props 与 context 两个参数。

props

export default {
  props: {
    name: String
  },
  setup(props,{attrs,slots,emit}) {
  //const { name } = props 不可以这样喔,需透过toRefs!
    console.log(props.name) 
    .... 略
  }
}

从上我们可以看到,因为没有 this 可以使用,所以 setup 透过参数传递 props 物件来供内部函式使用。

而这边注意的是由于 props 中的资料是响应式的,所以要取出 props 内的资料不能直接使用 ES6 解构, 这样会造成 props 的响应消失,如需要解构使用,我们可以透过 Vue3 新增的 APItoRefs方法来完成此目的。

context

而 setup 函数传递的第二个参数就是 context 物件,而在 Context 物件内含有attrsslotsemitAPI,而与 props 不同地方的是,因为 context 只是一个普通的 JS 物件,不是响应式的,所以针对其 可以 ES6 解构来进行使用

生命周期 Hook

Option APIComposition API
beforeCreatesetup
createdsetup
beforeMountonbeforeMount
mountedonMounted
beforeUpdatedonBeforeUpdated
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountonUnmount
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered
activatedonActivated
deactivatedonDeactivated

从上生命周期 hook 对照图我们可以发现,在过去的 created 与 beforeCreate 被 setup 所取代了,这是为什么呢?

针对这个疑问,文件是这样说明的:

在新式setup函数执行时机主要是 介于 beforeCreate、created 两者之间,因此就不需要再特地来定义他们,直接由 setup 来取代即可。而这也表示,在 created 之后的生命周期钩子 (onMounted、onUpdated 等) 都应该在 setup () 中来编写。

import { onMounted, onUpdated, onUnmounted } from 'vue'

export default {
  setup() {
    onMounted(() => {  // 使用的方式改为函数式的方式来使用
      console.log('mounted!')
    })
    onUpdated(() => {
      console.log('updated!')
    })
    onUnmounted(() => {
      console.log('unmounted!')
    })
  }
}

资料定义 ref 与 reactive

过去会透过 Vue Instance 中的data的属性,来 定义资料状态或着方法,而 data 本会身是一个 回传 object 的 function,接着我们就可以在模板 (template) 或着其他属性中来使用。

而针对 data 中内部 资料状态 的响应,Vue 是采去 object.definePropertity 的方式,透过创建 getter 与 setter 来追踪资料状态的变化。

透过宣告一个 ref (null) 来回传给模板 (template) 进行绑定

在 ref () 函式中可以接受一个任何基本型别的参数,且会回传一个响应式物件 (更正 ref 也是被 ES6 proxy 所代理)。而在这物件之中会提供一个.value 属性来更新或读取资料内的状态。

如果是针对 v-for 所渲染的多个动态元素我们要如何抓到各个 Dom 元素呢?

首先,我们可以先宣告一个 ref 包装的阵列 ref([]) 回传给模板使用,同时模板这边透过 :ref 来绑定 function 每一个 Dom 元素

我们就可以透过 divs [x] 取得对应的 Dom 节点了 (自行带入对应 index)

<template>

<div v-for ="(num,i) in nums "
     :ref="(el)=>{ divs[i] = el }">{{num}}</div>

<template/>

<script>
import { ref, reactive} form 'vue'
export default{
   setup(){
       const nums = reactive([1,2,3])
       const divs = ref([]); 
//其中需要注意的地方,因为 Dom 节点是动态产生的,有可能因为 nums 内容顺序更动而改变,所以在每次更动前我们需要在 onBeforeUpdate 重置一下,确保拿到的元素是正确的。       
     onBeforeUpdate(()=>{
         divs.value =[] 
       })

    return{ nums,divs }
   }
}
</script>

其中需要注意的地方,因为 Dom 节点是动态产生的,有可能因为 nums 内容顺序更动而改变,所以在每次更动前我们需要在 onBeforeUpdate 重置一下,确保拿到的元素是正确的。

reactive

reactive 为 Composition API 定义资料状态的另一个函式,其中 reactive () 只接受 物件阵列 型别的参数,最后会回传一个被 ES6 Proxy 所代理过的物件,达到响应式的更新,而其中要读取或更新其资料的方式就不需透过.value 来取值了

toRef & toRefs

props 与 reactive无法使用直接透过 ES6 解构语法来自动解构,因为这样会导致 资料失去原本的响应功能,所以针对这个问题, Vue 提供了 toRef 与 toRefs两个函式来进行 解构后同时保有响应功能

toRef

toRef 会将原本响应式物件内的一个属性抽离出来包装成一个 ref 的值,因为与原本对象是连接的,所以修改时,原本的值也会跟着变动。

<template>
   <div class="wrap">
     <div class="container">
       <h1>使用 toRef 测试</h1>
       {{userName}}
       <input type="text" v-model="userName">
     </div>
   </div>
</template>

<script>
import { reactive,toRef,onUpdated } from 'vue'
export default{
 setup(){
 
   const userInfo= reactive({
     name:'Winnie',
     age: 24
   })

   const userName = toRef(userInfo, 'name') //将响应物件里的属性抽离出来 使用

   onUpdated(()=>{
     console.log('toRef:',userName.value)  
     console.log('元物件:',userInfo.name) // 元物件内的name
   })
   return {
     userName //回传 name属性 给 template使用
   }
   //从上我们可以看到 userName 是从 userInfo 中的 name 属性抽离出来的,
//当传出去给 template 使用并更动它时,除了 userName 的值更新了从 console.log 也可以看 userInfo.name 的值也跟着跟新了。
 }
}
</script>

toRefs

toRefs 会将原本 响应式物件 转为 普通响应式物件。其中在 toRefs 后的物件与值是引用关系且每个值都转为 ref 来达到内部值保有响应性。

...略
 setup(){
   const setCount= reactive({
     count:1,
     add: ()=> setCount.count ++
   })
   // const { count, add } = toRefs(info) 也可以这样来解构,可以选择要用的就好了。
   
   return {
     ...toRefs(setCount)
   }
 }

计算属性 Computed

123 木头人范例:

  1. 我们可以看到在 Composition API 中computed () 参数为一个 getter 函式,同时会回传一个ref 物件来进行响应,所以当我们今天要取 computed中的值时,一样也需要透过 .value 进行读取。
//Character.js
<template>

    <div v-show='HP < 0'> Game Over</div> // 当血量少于0 显示Game over
    <div v-show='HP === 10'>Success</div>
    
    // 角色资讯
    <div>Character :{{characterNum}}</div>
    <div>HP: {{HP}}</div>
    <div>step : {{step}}</div>
    <div>HP:{{HP}}</div>
    
    // 动作执行
    <button @click="goForward">逃命啊</button>
    <button @click="goDie">啊~死了</button>
</template>
<script>
import { ref, computed, onUpdated } from 'vue'
export default{
  setup(){
    const characterNum = '456' 
    const initHP = ref(1)
    const step = ref(0)
    const HP = computed(()=> initHP.value += step.value)//computed 参数为一个getter 函式

    const goForward = ()=> step.value ++
    
    const goDie = ()=> step.value - 10
    
    onUpdated(()=>{
     console.log(HP.value)// computed会回传一个ref物件,所以读取内部值时需要加上.value
    })
        
   
    return {
     characterNum,    //将资料传出去给模板使用
     HP,
     step,
     goForward,
     goDie
    }
  }
}
  1. 另外如果今天我们要指定 set 的话,也可以透过 传入物件 来指定getset,(这边就不用木头人了,因为太长了)
<script>
import { ref, computed, onUpdated } from 'vue'
export default{
  setup(){

    const count = ref(1)
    const setCount = computed({
    get:()=> count.value +1,
    set:(val)=> count.value =val-1
    })
  }
}

watch()& watchEffect()

watch () 用法上,需改为一个 函式 的语法来使用

从下方我们可以看到,当我们监听一个资料状态时,watch () 中第一个参数为观察ref物件,第二个参数为一个 callBack ,当状态更新,就会针对其来执行 callback。

<template>

  <div>
      {{name}} <button @click="setName">隐姓埋名</button>
  </div>
</template>

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

export default({
  setup() {
      const name =ref('winnie') 
      const setName = () => {
          name.value = '大侠爱吃汉堡饱'
      }
      
      //观察一个ref物件
      watch(name, (val,oldVa)=>{
        console.log('newName:'${val},'oldName:'${oldVal});
      })
      return {
          name,
          setName,
      }
  }
});
</script>

watch()三个小问题解答

  1. 如果是 reactive 中的其中一属性呢

我们可以将 watch () 第一个参数改为 有回传值的 getter 函式

//略...
<script>
import {reactive,watch } from 'vue';

//略..
       const name = reactive({
         initName : 'winnie',
         setName: name.initName = '大侠爱吃汉堡饱'
      }) 
 
      watch(
      ()=> name.initName, // 观察一个getter
      (name,oldName)=>{
            console.log('newName:'${val},'oldName:'${oldVal});
      })
//略..

  1. 如果今天有多笔资料需要被观察

我们可以透过 阵列的方式来带入第一个参数之中,同时被观察。

这边需注意 我们可以从 console.log 中看到,因为是多笔资料写在一个 watch () 之中,所以 callback 是共用的,如要执行不同动作,可以分开来写。

//略...
<script>
import {reactive,watch } from 'vue';

//略..
       const name = reactive({
         initName : 'winnie',
         setName: name.initName = '大侠爱吃汉堡饱',
         fakeName: 'Banana',
         setFake :  name.fakeName='你不是大侠吃香蕉'
      }) 
 
      watch(
      [()=> name.initName,()=> name.fakeName], //  使用阵列侦听多笔
      ([name,fakeName],[oldName,oldFakeName])=>{
      
        console.log('Name:'+name,'oldName:'+oldName);
        console.log('fakeName:'+fakeName,'oldName:'+oldFakeName);
      })
//略..
  1. 如要针对 物件中的物件 做深层监听

我们可以在 watch () 第三个参数中 加上 {deep:true}

// 略..
    watch(state.obj,(val,oldVal)=>{}, {deep:true});
//略..

watchEffect()

控制台我们可以看到 ,watchEffect () 不用指定要监听的目标,只要在 callback 函式中对应响应式资料更新后就会依照对应资料来执行了,而与 watch 不同的是,watchEffect () 在初始 setup () 的时候,就会先执行一次了

export default{
  setup(){
    const name =ref('winnie')
    const setName = ()=> name.value = '大侠爱吃汉堡饱'
    
    watchEffect(()=>{
        console.log(name.value)
    
    })
  }
}

解除 watchEffect 监听

在每一个 watchEffect () 中都会回传一个专属的 WachStopHandle 函式。如果想要停止监听,可以直接呼叫其来停止监听。

    const stop = watchEffect(()=>{
        console.log(name.value)
    })
    stop();

provide/inject 实现跨阶层元件之间的状态沟通

从以上祖孙失联中的传话方式就好比我们在 Vue 之中使用传统 props 来做跨阶层元件之间的状态沟通,透过一层一层元件来传递资料,而这样不仅会很麻烦,同时也会增加元件之间的耦合度

我们需在提供资料状态的上层元件加上provide () 函式来传递,而其主要接受两个参数,来定义要传递的资料名称与,再透过 Inject()使用刚才所定义的名称来取值

// grandpa.vue---------------------------------------------
<script>
import { ref, provide } from 'vue';

export default ({
  setup() {
  
    //传递资料也可以是任何形式,如ref物件 、字串等
    provide('figure', "爷爷"); 
    provide('message', "小明 你吃饱了吗"); 
    
  }
});
</script>

// grandson.vue--------------------------------------------
<script>
import { inject } from 'vue';

export default ({
  setup() {
  
  //透过 定义的名称名称来取值
   const messenger= inject('figure') 
   const content = inject('message') 
   
   return {
       messenger,
   }   content 
  }
});
</script>

由于 provide/inject 主要作用是跨元件之间资料状态沟通,而其绑定的值预设并不是响应式的

提问: 跨元件响应式资料的处理我使用在 provide 时使用 ref () 或 reactive () 把资料包装起来,在inject.vue文件中修改状态可以吗?

因为 provide/inject 不像 Vuex 可以追踪各个 state 的变动,如果资料状态操作分散在多个子元件中,被更改出现问题时,要解决就会变得困难,同时程式码也会变得很难维护。

官方文件这边就提出建议,当使用 provide/inject 值时,尽可能将对响应式 property 的所有修改及操作,限制在 provide () 的元件内部中,来更新响应式的资料状态

//provide.vue
import {  ref, provide, readonly }
export default{
    setup(){
      const count = ref(0);
      const setCount = ()={
        count.vlaue++
      }
        
      provide('count', readonly(count)) // 使用readonly把保护起来,只能透过setCount来更动它。
      provide('setCount',setCount)
    }
}

//inject.vue--------------------------------------------
<template>
    <div>{{count}}</div>
    <button @click="updateCount">+1</button>
</template>

//略...

 setup(){
     const count =inject('count') //因为被 readonly保护了,所以只可读,不可直接修改他
     const updateCount  = inject('setCount')
 
     return{
      count,
      updateCount 
     }
 }

//略...

VueUse

VueUse 作者为 Anthony Fu ,是一个主要以 Composition API 为基底的 函式工具库,其中 VueUse 主要特色可以分为以下几点:

  • Vue 2 和 Vue 3 都支援

  • 采取Tree Shaking结构,只会打包引入的程式码

  • 支持各种插件

  • 可配置事件过滤器和目标 VueUse 中封装了很多常用的功能, 像是 追踪 ref 状态的更改,键盘 / 滑鼠输入事件等,主要可分为以下九大类型:

  • Animation (动画)

  • Browser (浏览器)

  • Component (元件)

  • Formatters (格式)

  • Misc (各式各样的)

  • Sensors (传感器)

  • State (状态)

  • Utility (实用方法)

  • Watch (观察) 稍后会在实作中其中一个功能出来介绍,剩下的就不一一介绍了,有兴趣的捧油们可以到官方文件细细品尝 VueUse(vueuse.org/guide/index…

 npm i @vueuse/core 
以 复制文字 为例:
<script setup> //setup语法糖

    import { useClipboard } from '@vueuse/core'; //只需要引入所使用到的  API
    import { ref } from 'vue'
      setup() {
        const input = ref('我是 Winnie')
        const { 
        text, //复制的值
        isSupported, //浏览器有无支援
        copy, //方法
        copied 
        } = useClipboard() 
        
</script>
小声说

点赞是不要钱的,但作者会开心好几天~

其实还想写一些坑,算是composition-api的番外篇,大概会写

  • 用 reactive 还是 ref
  • v-if & v-for 他俩不能在一起啊
  • 生命周期 hooks 与 async/await
  • Suspense 非同步元件 有兴趣的到时候可以来看看~

一篇文章搞定 composition-api(番外篇)