不只懂 Vue 语法:试说明 Composition API 与 Options API 概念和语法的分别?

问题回答

Composition API 是以逻辑功能来分割程序码,像是写原生 JavaScript 一样,我们会把实现同样功能的程序码写在附近。但 Options API 是以程序码的性质来分割程序码,例如有 3 个函式各自用来实现不同功能,但如果它们都是 computed 函式,就全都一起写在 computed 属性里。所以会产生以下问题:

  • 程序码较难阅读,因为不是按功能逻辑来放程序码,以致在比较大型的专案上,阅读时就要跳来跳去,也比较耗时。
  • 如果要重用程序码,就需要用 mixin。但如果 mixin 太多,每次看程序码时就很难追踪,需要逐一打开所有引入的 mixin 来看。

关於这个题目会分两篇来写,这篇会整理 Composition API 的概念和语法。下一篇就会以实战为主,试试改写 Options API 例子。

概念方面

程序码架构的差别

Options API 是根据程序的性质来分割程序码。

export default{
    props: {},
    components: {},
    data() {
        // 所有资料
    },
    computed() {
        // filter todos 功能
    },
    mounted() {
        // 显示 todos 功能
    },
    methods() {
        // 删除 todos 功能
    }
}

但是,Compositions API 是按逻辑来分割程序码。

export default {
    setup() {
        // 1. 显示 todos 的功能
        // ...



        // 2. filter todos 的功能
        // ...



        // 3. 删除 todos 的功能
        // ....
        
    }
}

概念如下图一样,用不同颜色代表不同功能需求。 Vue 2 的 options API 的写法明显会比较难阅读:

解决 mixin 难以追踪和命名冲突问题

在 options API,需要用 mixin 来引入可重用的程序码。但在大型专案中,有机会会在一个元件里引入很多 mixin,以致以下问题:

  • 假设有一个元件里的功能出错,我就要逐一打开此元件用到的 mixin 去查看,找出哪个 mixin 有涉及到此功能,然後再检查这些程序码有没有问题。
  • 假设某元件里的程序码已经很多,命名了很多函式。当我想在此再引入一个 mixin 来用时,有可能 mixin 里的函式名称,与我目前这个元件里所定义的函式名称相同而产生冲突。这样的话,Vue 2 无视 mixin 的函式,只执行元件里的函式,详细可见官方例子

Vue 3 的 composition API 就能解决 mixin 的问题。假设现在我要重用 serachKeyword 此功能

composables/ searchKeyword.js

export default function searchKeyword(keyword, key, list){
    const result = list.find( item => item[key] === keyword)
    return {
        result
    }
}

Home.vue

import searchKeyword from '@/composables/searchKeyword'

export default{
    setup() {
        const { result } = searchKeyword('tshirt', 'title', [{ title: 'hat', price: 100}, {title: 'tshirt', price: 200}])
        console.log(result); // {title: 'tshirt', price: 200}
    }
}

以上是一个很简单例子,searchKeyword 是用来找出在 list 中合乎 keyword 的那笔资料。以上例子很清晰看到,result 的值是来自 searchKeyword 这个可重用的函式。而且,把它 import 进来 Home.vue 时,我把它也可以改名,不一定用 searchKeyword 这个名称。因此,比起 Vue 2 的 mixin 更好用。

语法方面

使用 setup 函式

起手式是建立一个 setup 函式,此函式会在 app 挂载在 DOM 上後,并在元件建立前(created 生命周期)执行,而且只会执行一次。

export default {
  setup(props, context) {
    // 所有程序码写在这里
    return {
        // 所有在模版上会使用的资料、函式
    }
  }
}
  • props:跟 options API 一样,props 就是用来接收外层资料。
  • context:一个物件。里面会有 slot, attrs, emit, expose

注意,因 setup 是在元件建立前执行,所以只可使用: propsslotattrsemitexpose。不可使用: datacomputedmethodsrefs(模版用 refs)

setup 函式会回传一个物件,里面要放所有在模版里会用到的资料或函式:

<template>
    <div>
      <p>{{ user }} </p>
      <button @click="greeting" type="button">Say Hi!</button>
    </div>
</template>
export default {
  setup() {
    const user = 'Alysa'
    const greeting = () => {
      alert('Hi I am a front-end developer')
    }
    return { 
      greeting, user
    }
  }
}

如果资料或函式不在 return 物件里,模版就不能使用。

注意,没法使用解构赋值取出 props 的响应式资料

如果使用解构赋值的手法,会导致该 props 的资料不再是响应式。举例说,要把在外层(App.vue)的 count 传入内层(List.vue):

App.vue

<template>
  <p>App: {{ n }} </p>
  <List :count="n"></List>
</template>
export default {
  components: { 
    List
  },
  setup() {
    const n = ref(1)
    // 每 1 秒加 1
    setInterval( () => n.value++,1000)
    return { 
      n
    }
  }
}

List.vue

<template>
  <p>List: {{ count }} </p>
</template>

export default {
    props: {
        count: Number
    },
    setup(props) {
        // count 失去响应式,不会再更新!
        const { count } = props
        return {
            count
        }
    }
}

结果:

App 里的 count 目前已经累加到 5,但 List 里的 count 仍然是 1。

解决方法:使用 toReftoRefs 包装

const count  = toRef(props, 'count')

或者

// 使用解构赋值建立 count 变数
const { count } = toRefs(props)

结果就能保持响应式,外层和内层的 count 数值同步:

此文较後部分会再详细讲解两者的差异和用法。

使用 ref 与 reactives 定义响应式资料

在 Vue 2 时,我们全都放到 data 属性里就行了。但现在 Vue 3 里就需要使用 ref()reactive() 来建立响应式资料。

假设有 user、todos 这两笔资料。

Vue 2 做法:

data() {
    return{
        user: 'Alysa',
        todos: ['Watch Netflix', 'Buy dinner']
    }
}

Vue 3 做法:

setup() {
    const user = ref('Alysa')
    const todos = reactive(['Watch Netflix', 'Buy dinner'])
    // 用 ref 也可以,看你个人偏好
    return {
        user,todos
    }
}

ref

  • ref 的接收一个参数,可以是任何型别。
  • ref 无法监听阵列或物件内部属性的变更。(硬要监听的话用 watch 的 deep true 也可以)
  • ref 会回传一个响应式物件,里面只有 value 这个属性。因此,取值时的写法如下:
const user = ref('Alysa')
console.log(user.value) // Alysa

reactive

  • reactive 的参数只能是阵列或物件。
  • reactive 可以深层监听阵列或物件内部属性的变更。
  • 取值时不用使用 .value。因为 reactive 会把整个物件或阵列回传回来。

个人偏好是用 ref 定义基本型别资料,用 reactive 定义物件、阵列资料。但这也取决於公司团队做法,也有听说过由头到尾都是用 ref 的做法。

使用 toRef 或 toRefs 引用响应式资料

上面提及过,可使用 toReftoRefs 来保持 props 是响应式。此两种语法是针对用 reactive 定义的物件。

  • 画面方面:当 reactive 原资料修改,画面会变化。但 toRef / toRefs 修改时,画面不会变化。
  • 资料方面:当 reactive 原资料修改时,toRef / toRefs 会随之而变化。反之,当 toRef / toRefs 改动时,原资料不会受影响。
  • 参数方面:toRef 接收两个参数,回传一个值。toRefs 只会接收一个参数,回传一个物件。
const x = toRef(物件, 属性)

toRefs 只接收一个参数,回传一个物件:

const  x  = toRefs(物件)

readonly

最後,Vue 3 新增了设定资料为唯读的功能。这能避免在多人团队开发时,不小心修改了某些函式里的值,以致程序出错。

const todos = reactive(['Watch Netflix', 'Buy dinner'])
const copyTodos = readonly(todos)
const edit = () => {
  copyTodos[0] = 'sleep'
}
edit() //Set operation on key "0" failed: target is readonly

ref 的资料也一样:

const name = ref('Alysa')
const copyName = readonly(name) 

总结

  • Composition API 是按逻辑来区分程序码,但 Options API 是按程序码的性质来区分
  • Options API 在阅读上会比 Composition API 困难。
  • Composition API 新增多个语法,例如 setUp()ref / reactive 来建立资料等等。

参考资料

ref,toRef,toRefs三者的使用及区别
Vue 3 - Composition API
Mike - 2021 Vue 3 专业职人 入门篇


<<:  #22-掰惹Gif!用Sprite雪碧图做动画! (CSS & Canvas)

>>:  Day23 :【TypeScript 学起来】先了解 ES6 Class

Spring Framework X Kotlin Day 15 AOP

GitHub Repo https://github.com/b2etw/Spring-Kotlin...

Day 19: Recap Google Cloud Platform

Backup 文章 https://hackmd.io/@WePlus/Sky0eQUNF Revi...

【Day29】Cordic 演算法的实现

假设今天再做某种数位信号处理时,不小心用到了 arctan(y/x) 函数,那麽当然可以用泰勒展开得...

[Day29] 建立购物车系统 - 12

1. 在WebMvc专案新增购物车服务的功能 1.1 新增ViewComponent CartLis...

Day 15 (Ps)

1.图层有"锁头"就不能被更改,记得开锁头-------------------...