Vue 3 响应性机制 之 watch 应用

2021-09-07  本文已影响0人  AizawaSayo

直接简单粗暴地上实例,这是一个用来获取分页列表数据的组合函数:

📃src/composables/useList.js

import { ref } from 'vue'
/**
 * 获取分页列表相关数据
 * @listQuery {object} page 和 pageSize 等查询参数
 * @getListApi {Function} 请求列表调用的 Api
 */
export default function useList(listQuery, getListApi) {
  const list = ref(null)
  const total = ref(0)
  const listLoading = ref(false)
  const error = ref(null)

  const getList = async () => { // 封装异步请求
    listLoading.value = true
    try {
      const response = await getListApi(listQuery)
      list.value = response.data.list
      total.value = response.data.total
    } catch (err) {
      error.value = err
    } finally {
      listLoading.value = false
    }
  }

  return {
    list,
    total,
    listLoading,
    error,
    getList,
  }
}

然后在我们的User页面:
📃src/views/User.vue

<template>
  <div class="app-container">
    总条数:{{ total }} 当前页:{{ listQuery.page }}
    <div v-for="item in list" :key="item._id">
      {{ item.username }}
    </div>
    <pagination
      v-show="total > 0"
      :total="total"
      v-model:page="listQuery.page"
      v-model:limit="listQuery.pageSize"
    />
  </div>
</template>

<script>
import { reactive, onMounted, watch } from 'vue'
import { getUsers } from '@api/user'
import useList from '@composables/useList'
export default {
  name: 'User',
  setup() {
    const listQuery = reactive({ query: '', page: 1, pageSize: 10 })
    const { list, total, listLoading, getList } = useList(listQuery, getUsers)
    
    // onMounted(getList)
    watch(
      [() => listQuery.page, () => listQuery.pageSize],
      (newValues, prevValues) => {
        getList()
      },
      { immediate: true } // 初始化时立即执行一次,代替 onMounted 钩子
    )

    return {
      list,
      total,
      listLoading,
      listQuery,
      getList,
    }
  },

其中<pagination/ >组件是对 element-plus 的 ELPagination 的一个简单封装,被我注册在了全局。
主要就是包了一层<div/ >,内容非常简单,代码如下:

<template>
  <div :class="{ hidden: hidden }" class="pagination-container">
    <el-pagination
      :background="background"
      v-model:current-page="currentPage"
      v-model:page-size="pageSize"
      :layout="layout"
      :page-sizes="pageSizes"
      :total="total"
      v-bind="$attrs"
    />
  </div>
</template>

<script>
import { computed, onUpdated } from 'vue'

export default {
  name: 'Pagination',
  props: {
    total: {
      required: true,
      type: Number,
    },
    page: {
      type: Number,
      default: 1,
    },
    limit: {
      type: Number,
      default: 20,
    },
    pageSizes: {
      type: Array,
      default() {
        return [10, 20, 30, 50]
      },
    },
    layout: {
      type: String,
      default: 'total, sizes, prev, pager, next, jumper',
    },
    background: {
      type: Boolean,
      default: true,
    },
    autoScroll: {
      type: Boolean,
      default: true,
    },
    hidden: {
      type: Boolean,
      default: false,
    },
  },
  setup(props, { emit }) {
    const currentPage = computed({
      // 当前页
      get: () => props.page,
      set: val => {
        emit('update:page', val)
      },
    })
    const pageSize = computed({
      // 每页条数
      get: () => props.limit,
      set: val => {
        emit('update:limit', val)
      },
    })

    // 计算属性通过传入一个包含 get 和 set 函数的对象,返回一个 `可写的` ref 对象
    onUpdated(() => console.log(currentPage.value))

    return {
      currentPage,
      pageSize,
    }
  },
}
</script>

<style scoped>
.pagination-container {
  background: #fff;
  padding: 32px 16px;
}
.pagination-container.hidden {
  display: none;
}
</style>

到这里代码本身已经能够正常地运行了。但我们虽然提取了组合函数,但似乎并没有真正把负责分页请求的业务独立开来

为什么要使用组合式 API,因为可以很方便地把属于同一块业务逻辑的代码提取到一个组合函数中,除了避免setup()过于冗长、整体阅读性更好,还能实现一定程度的复用(取代以前的公共方法)。官方也建议我们以业务逻辑为单位来组织代码

这只是最简单的一个分页请求的功能,试想我们后面再加上查询、筛选、添加、删除等功能,新增的querysort等等数据都堆叠在一起,使用watch时也混在一起,根本未将它们区分来提升可读性。

所以我考虑把负责不同功能的数据分开,然后分别在它们各自的组合函数里单独监听。有以下调整方式:

首先我们得清楚 watch 如何使用:
如果第一个监听参数设置不当,是无法正常监测和执行副作用的。

侦听器数据源只能是getter/effect函数、refreactive对象,或者包含这些类型(的数据)的数组
换句话说,只要侦听数据不是refreactive对象,就必须传入一个箭头函数

现在我们就可以愉快地把 watch 提到组合函数useList里了。

第一种方法:

📃src/composables/useList.js
就是简单粗暴地把之前User页面的 watch 移到这个函数里,这样没有什么问题,但是未免有些繁琐,可读性不佳。

import { ref, watch } from 'vue'
export default function useList(listQuery, getListApi) {
  // ...
  watch(
    [() => listQuery.page, () => listQuery.pageSize],
    (newValue, prevValue) => {
      getList()
    },
    { immediate: true } // 初始化时就执行一次,代替 onMounted 钩子
  )
  // ...
}

第二种方法:

二是直接监听listQuery,因为我们把reactive对象listQuery传进来了。
这样看着很方便简洁,但是不推荐监听一整个对象,因为我们很难清楚到底是其中的哪个属性变动了。

import { ref, watch } from 'vue'
export default function useList(listQuery, getListApi) {
  // ...
  watch(
    listQuery,
    () => getList(),
    { immediate: true }
  )
  // ...
}

第三种方法:

我个人倾向于第三种,即侦听单独的ref对象 /ref对象数组,因为不仅能确保响应性,副作用的运行依赖于哪些数据(的变动)也一目了然。

import { isRef, ref, toRefs, watch } from 'vue'

export default function useList(listQuery, getListApi) {
  // 用 toRefs 建立 listQuery 指定属性的响应式 ref,即 page 和 pageSize 都是 ref
  const { page, pageSize } = toRefs(listQuery)

  console.log(isRef(listQuery.page), ifRef(page))

  watch(
    [page, pageSize],
    (newValues, prevValues) => {
      getList()
    },
    { immediate: true }
  )
}

吃不准数据是否是ref的时候可以用isRef检测下。

上一篇下一篇

猜你喜欢

热点阅读