我的 composition-api 风格指南

852 阅读4分钟

开始

不知不觉 vue3 已经发布一年多了,大部分 vue 项目开始拥抱 composition-api ,这个新特性可以说是 vue3 的最大的惊喜。但 composition-api 相较于过去的 options-api 写法需要有许多新的思维方式,使用 composition-api 也有一段时间了,总结一下过去我在 composition-api 一些实践。

功能区与逻辑

composition-api 文档中详细介绍了 composition-api 的一大优势: 强大的逻辑组合与功能划分能力。但这很容易陷入一个误区,是否所有的代码都要按照功能划分的方式来写。初使用 vue3 的时候我写过这样的实践。

import { reactive, computed, ref, watchEffect, toRefs } from 'vue'

// 搜索功能
const searchData = reactive({ name: '', age: '', sex: 0 })
const searchOption = reactive({ nameOption: [], ageOption: [] ,sexOption: [] })
const getSearchOption = () => {
  fetch('searchOption').then(( { nameOption,ageOption,sexOption }) => {
      searchOption.nameOption = nameOption
      searchOption.ageOption = ageOption
      searchOption.sexOption = sexOption
    })
  })
}

// table 列表
const tableData = reactive({
  list: [],
  total: 0,
  page: 1,
  pageSize: 10,
  loading: false,
})
const getList = () => {
  tableData.loading = true
  fetch('getList', searchData).then(({ list, total }) => {
    tableData.list = list
    tableData.total = total
    tableData.loading = false
  })
}

其实这样的代码没什么问题,功能清晰,一目了然,但是写了一段时间我就放弃了这种写法,因为现实中的业务场景更复杂,并没有那么多的心力去拆解和维护功能区。上面的代码其实是一个相对理想的状态,搜索去的代码和表格并没有太多耦合,维护者可以一眼看到当前页面的两个功能,但实际中业务的复杂度更大,耦合更深,功能越多,就会发现搜索和表格之间的功能就会很难界定,需要花费更多的时间思考这段逻辑属于哪个功能区,于是我又回到了类 vue2 的写法。

import { reactive, computed, ref, watchEffect, toRefs } from 'vue'

// data
const searchData = reactive({ name: '', age: '', sex: 0 })
const searchOption = reactive({ nameOption: [], ageOption: [] ,sexOption: [] })
const tableData = reactive({
  list: [],
  total: 0,
  page: 1,
  pageSize: 10,
  loading: false,
})

// methods
const getSearchOption = () => {
  fetch('searchOption').then(( { nameOption,ageOption,sexOption }) => {
      searchOption.nameOption = nameOption
      searchOption.ageOption = ageOption
      searchOption.sexOption = sexOption
    })
  })
}
const getList = () => {
  this.loading = true
  fetch('getList', searchData).then(({ list, total }) => {
    tableData.list = list
    tableData.total = total
    tableData.loading = false
  })
}

开始的我也会疑惑,这不就是面条代码么?但长久的实践让我意识到 composition-api 并不是全盘否定 vue2 时代 options-api datamethods 分离的理念,而是在此之外提供了逻辑组合能力。如果想要抽象逻辑,可以使用 自定义hook

import { reactive, computed, ref, watchEffect, toRef } from 'vue'

// data
const searchData = reactive({ name: '', age: '', sex: 0 })
const searchOption = reactive({ nameOption: [], ageOption: [] ,sexOption: [] })
const tableData = reactive({
  list: [],
  total: 0,
  page: 1,
  pageSize: 10,
  loading: false,
})

const { pageChange } = usePagination(toRef(tableData, 'page'), toRef(tableData, 'pageSize'))
const { x, y } = useScroll()

// methods
const getSearchOption = () => {
  fetch('searchOption').then(( { nameOption,ageOption,sexOption }) => {
      searchOption.nameOption = nameOption
      searchOption.ageOption = ageOption
      searchOption.sexOption = sexOption
    })
  })
}
const getList = () => {
  this.loading = true
  fetch('getList', searchData).then(({ list, total }) => {
    tableData.list = list
    tableData.total = total
    tableData.loading = false
  })
}

简而言之就是,当代码保持在一个合理的行数(比如 500 行)和进行合理命名,业务耦合不深或者一些复杂逻辑可以单独抽取成自定义 hook ,这样我想很难写成面条代码。

ref 和 reactive

vue3 中通过 refreactive 两个 API 来为数据添加响应式。当然,这并不算是一个很舒服的方式。

import { reactive, ref } from 'vue'
const count = ref(0)
const data = ref([])

count.value++
data.value.push(1)
data.value = [1, 2, 3]

想要为基本数据类型添加响应式就绕不过去 .value 这个操作,但还算一个可接受操作,如果加持了 ts 也不会感到不适,真正麻烦的是 ArrayObject

import { reactive, ref } from 'vue'
const data = ref([])
const data2 = reactive([])

data.value.push(1)
data2.push(1)
data.value = [1, 2, 3]
data2.splice(0, data2.length, ...[1, 2, 3])

上述例子,refreactive 对数组的操作都不是很舒服,ref 利于重置, reactive 在数组读写操作上更直观,但无法中和,个中取舍只能自己去衡量。

我实验了一些方案,比如只用 ref 或者只用 reactive,最后还是决定混用。我发现业务代码中对于重置和读写行为并不需要太多的心智负担,大部分的 table list tree 数据基本不需要什么写入操作,只需要调取接口的时候重置就可以。而 form 表单一类需要频繁操作的数据,可以用 reactive ,很少会出现覆盖掉整个对象或者数组的情况。

watch 和 watchEffect

vue3 中,可以通过 watchEffectwatch 监听数据触发响应,无论哪个相比 vue2 中的 watch 功能上都有所加强。watchEffect 更类似 vue2 中的 computed 隐式监听响应,watch 显示监听并且可监听多个。

在业务场景中一些比较复杂的逻辑我更喜欢使用 watch ,因为 watch API 更加 显式

import { watchEffect, watch } from 'vue'

watchEffect(() => {
  if (searchData.name) {
    getTableData()
  } else {
    // ...
  }
})

watch(
  () => searchData.name,
  name => {
    if (name) {
      getTableData()
    } else {
      // ...
    }
  }
)

可以看出,虽然 watch 繁琐了一些,但是相对 watchEffect 不需要太多的理解成本,显式的表露当前响应是基于 searchData.name,在后期维护中更直观 。

另一点,触发的 getTableData() 函数,如果其内部也有响应式数据,使用 watchEffect 会增加不必要的侦听,甚至导致无限递归。复杂逻辑下使用 watch 可以让代码更可控。

结束

上面就是我写 vue3 的一些小总结,如果你也在用,可以一起交流一下。