组合式API

阅读: 6705     评论:0

介绍

本节是Vue3相对于Vue2在使用上的最大变化。

但必须强调,组合式API不是强制使用的,你完全可以不用。

什么是组合式 API?

通过创建 Vue 组件,我们可以将页面中重复的部分连同其功能一起提取并封装为可重用的代码段。

这样不但提高了开发效率,而且在可维护性和灵活性方面相当有帮助。

想想拥有几百个组件的大型应用,共享和重用代码变得尤为重要。

然而,工作实践告诉我们,光靠封装组件可能并不够。

看下面这个例子,假设有一个显示某个用户的仓库列表的页面,并且它具有搜索和筛选功能。

实现此页面的组件代码可能如下所示(这里有一些单文件组件的语法,可以先不关心):

// src/components/UserRepositories.vue

export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: { 
      type: String,
      required: true
    }
  },
  data () {
    return {
      repositories: [], // 1    b
      filters: { ... }, // 3
      searchQuery: '' // 2
    }
  },
  computed: {
    filteredRepositories () { ... }, // 3
    repositoriesMatchingSearchQuery () { ... }, // 2
  },
  watch: {
    user: 'getUserRepositories' // 1
  },
  methods: {
    getUserRepositories () {
      // 使用 `this.user` 获取用户仓库
    }, // 1
    updateFilters () { ... }, // 3
  },
  mounted () {
    this.getUserRepositories() // 1
  }
}

该组件有以下几个功能:

  1. 从假定的外部 API 获取该用户的仓库,并在用户有任何更改时进行刷新
  2. 使用 searchQuery 字符串搜索仓库
  3. 使用 filters 对象筛选仓库

使用 (datacomputedmethodswatch) 组件选项的方式来组织代码逻辑通常都很有效(这也是Vue2一直使用的模式)。

然而,当我们的组件开始变得更大时,逻辑关注点的列表也会急剧增长。对于那些一开始没有参与编写这些组件的人来说,这些组件变得难以阅读和理解,如下图所示:

Vue 选项式 API: 按选项类型分组的代码

上面是一个大型组件的示例,其中逻辑关注点按颜色进行分组。

这种代码组织方式,使得理解和维护,复杂的组件变得困难。不同选项的分离掩盖了潜在的逻辑问题。

此外,在处理单个逻辑关注点时,我们还必须不断地“跳转”相关代码的选项块。

如果能够将同一个逻辑关注点相关代码收集在一起会更好,而这正是组合式 API 存在的核心目的。

通俗地解释一下上面的内容:有些程序员觉得把代码分别写在data、computed、methods等选项卡中不利于维护和理解,希望可以按照功能组合代码。组合式API不是为了提高性能,也不是带来新特性,而是方便程序员看代码维护代码。

组合式 API 基础知识

为了使用组合式 API,我们首先需要一个可以实际使用它的地方(也就是代码写在哪里)。

在 Vue 组件中,我们将此位置称为 setup

setup 组件选项

你没看错,为了解决选项的问题,我们又创建了一个新的选项。

之所以叫做setup,有启动、一开始、预先配置的意思。

因为 setup 是围绕 beforeCreatecreated 生命周期钩子运行的,也就是围绕组件在创建初期时的设定工作。

所以,在 setup 中不能使用 this,因为这时候还没有组件实例。

setup 的调用发生在 datacomputedmethods 被解析之前,所以无法在 setup 中访问这些选项。

setup的特性:

  • setup 选项在组件创建之前执行,一旦 props 被解析,就将作为组合式 API 的入口。
  • setup 选项是一个接收 propscontext 参数的函数。
  • setup 函数的返回值,可以被组件的其余部分 (计算属性、方法、生命周期钩子等) ,以及组件的模板使用。

setup的主要目的就是上面的第三条,你可以把这些返回值看作data、methods等的一部分。

现在让我们利用组合式API将前面例子的3个逻辑关注点聚合在一起。

首先把 setup 添加到例子中:

// src/components/UserRepositories.vue

export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: {
      type: String,
      required: true
    }
  },
  setup(props) {
    console.log(props) // { user: '' }

    return {} // 这里返回的任何内容都可以用于组件的其余部分
  }
  // 组件的“其余部分”
}

第一步,提取第一个逻辑关注点开始 (在原始代码段中标记为“1”)。

这个逻辑关注点的业务功能:从假定的外部 API 获取该用户的仓库,并在用户有任何更改时进行刷新

我们从最基础的部分开始:

  • 仓库列表
  • 更新仓库列表的异步函数
  • 返回列表和函数,以便其他组件选项可以对它们进行访问
// src/components/UserRepositories.vue        `setup` function
// 假设我们有这么一个异步的api
import { fetchUserRepositories } from '@/api/repositories'

// 在我们的组件内
setup (props) {
  let repositories = []
  const getUserRepositories = async () => {
    repositories = await fetchUserRepositories(props.user)
  }

  return {
    repositories,
    getUserRepositories // 返回的函数与方法的行为相同,注意了,这是个函数名,不是函数的执行返回值
  }
}

这是我们的出发点,但它还无法生效,因为 repositories 变量目前还是非响应式的。这意味着从用户的角度来看,仓库列表将始终为空。让我们来解决这个问题!

ref 的响应式变量

在 Vue 3.0 中,我们可以通过一个新的 ref 函数使任何响应式变量在任何地方起作用,如下所示:

import { ref } from 'vue'

const counter = ref(0)

ref 可以接收参数,并将其包裹在一个带有 value 键的对象中返回({value: 参数值}),然后可以使用该属性访问或更改响应式变量的值,如下所示:

import { ref } from 'vue'

const counter = ref(0)

console.log(counter) // { value: 0 }
console.log(counter.value) // 0

counter.value++
console.log(counter.value) // 1

一定要注意这个counter的数据类型和值的获取方式!

将值封装在一个对象中,看似没有必要,但为了保持 JavaScript 中不同数据类型的行为统一,这是必须的。这是因为在 JavaScript 中,NumberString 等基本类型是通过值而非引用传递的:

按引用传递与按值传递

用对象封装值使得我们可以在整个应用中安全地传递它,而不必担心在某个地方失去它的响应性。

换句话说,ref 为我们的值创建了一个响应式引用。在整个组合式 API 中会经常使用引用的概念。

回到我们的例子,让我们创建一个响应式的 repositories 变量:

// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref } from 'vue'

// 在我们的组件中
// 注意变化,repositories.value

setup (props) {
  const repositories = ref([])
  const getUserRepositories = async () => {
    repositories.value = await fetchUserRepositories(props.user)
  }

  return {
    repositories,
    getUserRepositories
  }
}

好了!现在,每当我们调用 getUserRepositories 时,repositories 都将发生变化,视图也会更新以反映变化。我们的组件现在应该如下所示:

// src/components/UserRepositories.vue
import { fetchUserRepositories } from '@/api/repositories'
import { ref } from 'vue'

export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: {
      type: String,
      required: true
    }
  },
  setup (props) {
    const repositories = ref([])
    const getUserRepositories = async () => {
      repositories.value = await fetchUserRepositories(props.user)
    }

    return {
      repositories,
      getUserRepositories
    }
  },
  data () {
    return {
      filters: { ... }, // 3
      searchQuery: '' // 2
    }
  },
  computed: {
    filteredRepositories () { ... }, // 3
    repositoriesMatchingSearchQuery () { ... }, // 2
  },
  watch: {
    user: 'getUserRepositories' // 1
  },
  methods: {
    updateFilters () { ... }, // 3
  },
  mounted () {
    this.getUserRepositories() // 1
  }
}

我们已经将第一个逻辑关注点中的几个部分移到了 setup 方法中,它们彼此非常接近。

剩下的就是在 mounted 钩子中调用 getUserRepositories,并设置一个监听器,以便在 user 发生变化时自动调用API重新获取用户仓库列表。

第二步将从生命周期钩子开始。

setup 内注册生命周期钩子

为了使组合式 API 的功能和选项式 API 一样完整,我们还需要一种在 setup 中注册生命周期钩子的方法。

为此, Vue 设计了几个新函数。组合式 API 上的生命周期钩子与选项式的名称相同,但前缀为 on:即 mounted 看起来会像 onMounted

这些函数接受一个回调,当钩子被组件调用时,该回调将被执行。

让我们将其添加到 setup 函数中:

// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted } from 'vue'   // 一定不要忘记导入

// 在我们的组件中
setup (props) {
  const repositories = ref([])
  const getUserRepositories = async () => {
    repositories.value = await fetchUserRepositories(props.user)
  }

  onMounted(getUserRepositories) // 在 `mounted` 时调用 `getUserRepositories`

  return {
    repositories,
    getUserRepositories
  }
}

这样就搞定了,还是蛮简单的。

第三步,对 user 的变化做出反应。为此,我们将使用独立的 watch 函数。

watch 响应式更改

就像我们在组件中使用 watch 选项并为 user 属性设置侦听器一样,我们也可以使用从 Vue 导入的 watch 函数执行相同的操作。

watch函数接受 3 个参数:

  • 一个想要侦听的响应式引用或 getter 函数
  • 一个回调
  • 可选的配置选项

下面让我们快速了解一下它是如何工作的

// watch函数需要提前导入
import { ref, watch } from 'vue'

const counter = ref(0)
watch(counter, (newValue, oldValue) => {
  console.log('The new counter value is: ' + counter.value)
})

每当 counter 被修改时,例如 counter.value=5,侦听将触发并执行回调 (第二个参数),在本例中,它将把 'The new counter value is:5' 打印到控制台中。

以下是等效的选项式 API:

export default {
  data() {
    return {
      counter: 0
    }
  },
  watch: {
    counter(newValue, oldValue) {
      console.log('The new counter value is: ' + this.counter)
    }
  }
}

现在我们将其应用到我们的示例中:

// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch, toRefs } from 'vue'

// 在我们组件中
setup (props) {
  // 使用 `toRefs` 创建对prop的 `user` 属性的响应式引用
  const { user } = toRefs(props)

  const repositories = ref([])
  const getUserRepositories = async () => {
    // 更新 `prop.user` 到 `user.value` 访问引用值
    repositories.value = await fetchUserRepositories(user.value)
  }

  onMounted(getUserRepositories)

  // 在 user prop 的响应式引用上设置一个侦听器
  watch(user, getUserRepositories)

  return {
    repositories,
    getUserRepositories
  }
}

你可能已经注意到了,在 setup 的顶部我们使用了 toRefs。这是为了确保我们的watch侦听器函数能够根据 user 属性的变化做出反应。

如果直接使用prop.user,它是非响应式的。

经过上面的一系列操作,我们就把第一个逻辑关注点聚合到了一个地方。

我们现在可以对第二个关注点执行相同的操作——基于 searchQuery 进行过滤,这次是使用计算属性。

独立的 computed 属性

refwatch 类似,也可以使用从 Vue 导入的 computed 函数在 Vue 组件外部创建计算属性。比如前面 counter 的例子:

import { ref, computed } from 'vue'

const counter = ref(0)
const twiceTheCounter = computed(() => counter.value * 2)

counter.value++
console.log(counter.value) // 1
console.log(twiceTheCounter.value) // 2

这里我们给 computed 函数传递了第一个参数,它是一个类似 getter 的回调函数,输出的是一个只读响应式引用

访问新创建的计算变量twiceTheCountervalue,我们也需要像 ref 一样使用 .value属性。

让我们将搜索功能移到 setup 中:

// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch, toRefs, computed } from 'vue'

// 在我们的组件中
setup (props) {
  // 使用 `toRefs` 创建对 props 中的 `user` 属性 的响应式引用
  const { user } = toRefs(props)

  const repositories = ref([])
  const getUserRepositories = async () => {
    // 更新 `props.user ` 到 `user.value` 访问引用值
    repositories.value = await fetchUserRepositories(user.value)
  }

  onMounted(getUserRepositories)

  // 在 user prop 的响应式引用上设置一个侦听器
  watch(user, getUserRepositories)

  const searchQuery = ref('')  // 这相当于一个data选项中的变量

  // 下面的语法有多层嵌套函数。repositoriesMatchingSearchQuery最终是一个仓库列表,而不是一个函数名
  const repositoriesMatchingSearchQuery = computed(() => {
    return repositories.value.filter(
      repository => repository.name.includes(searchQuery.value)
    )
  })

  return {
    repositories,
    getUserRepositories,
    searchQuery,
    repositoriesMatchingSearchQuery
  }
}

独立的JS模块

对于其他的逻辑关注点我们也可以像上面那样做,但是你可能已经在问这个问题了——这不就是把代码移到 setup 选项并使它变得非常大吗?我们额外导入了更多的东西,编写了更多的代码,它真的比选项式API优秀吗?

记住:简单的场景用选项式API,复杂的场景用组合式API!

为了解决组合式API变得比选项式API更臃肿的问题,Vue设计了下面的模式。

将上述代码提取到一个个独立的组合式函数中,并将每个函数又保存在一个单独的js文件中。这些js文件又放在一个composables目录中。

让我们从创建 useUserRepositories 函数开始:

// src/composables/useUserRepositories.js

import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch } from 'vue'

export default function useUserRepositories(user) {
  const repositories = ref([])
  const getUserRepositories = async () => {
    repositories.value = await fetchUserRepositories(user.value)
  }

  onMounted(getUserRepositories)
  watch(user, getUserRepositories)

  return {
    repositories,
    getUserRepositories
  }
}

然后是搜索功能:

// src/composables/useRepositoryNameSearch.js

import { ref, computed } from 'vue'

export default function useRepositoryNameSearch(repositories) {
  const searchQuery = ref('')
  const repositoriesMatchingSearchQuery = computed(() => {
    return repositories.value.filter(repository => {
      return repository.name.includes(searchQuery.value)
    })
  })

  return {
    searchQuery,
    repositoriesMatchingSearchQuery
  }
}

现在我们有了两个单独的JS功能模块,接下来就可以开始在组件中使用它们了:

// src/components/UserRepositories.vue
import useUserRepositories from '@/composables/useUserRepositories'
import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
import { toRefs } from 'vue'

export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: {
      type: String,
      required: true
    }
  },
  setup (props) {
    const { user } = toRefs(props)

    const { repositories, getUserRepositories } = useUserRepositories(user)

    const {
      searchQuery,
      repositoriesMatchingSearchQuery
    } = useRepositoryNameSearch(repositories)

    return {
      // 别看我们获取了4个引用,但不是全都需要
      // 因为我们并不关心未经过滤的仓库,也就是说所有的仓库列表都是过滤后的结果
      // 我们可以在 `repositories` 名称下暴露过滤后的结果
      // 外部没有人使用getUserRepositories,所以它也被抛弃了
      repositories: repositoriesMatchingSearchQuery,
      getUserRepositories,
      searchQuery,
    }
  },
  data () {
    return {
      filters: { ... }, // 3
    }
  },
  computed: {
    filteredRepositories () { ... }, // 3
  },
  methods: {
    updateFilters () { ... }, // 3
  }
}

看见没有?解决不了问题,我就把问题本身消灭掉!你不是嫌组合式API的代码都写在组件的setup中太臃肿吗?那我把它们抽取出去,分别写在不同的JS文件中,再导入进来,美其名曰模块化!眼不见心不烦!多好,大家都高兴了!

此时,你可能已经知道了其中的奥妙,所以让我们跳到最后,迁移剩下的过滤功能。我们不需要深入了解真实代码的实现细节,因为这不是重点。

// src/components/UserRepositories.vue
import { toRefs } from 'vue'
import useUserRepositories from '@/composables/useUserRepositories'
import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
import useRepositoryFilters from '@/composables/useRepositoryFilters'

export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: {
      type: String,
      required: true
    }
  },
  setup(props) {
    const { user } = toRefs(props)

    const { repositories, getUserRepositories } = useUserRepositories(user)

    const {
      searchQuery,
      repositoriesMatchingSearchQuery
    } = useRepositoryNameSearch(repositories)

    const {
      filters,
      updateFilters,
      filteredRepositories
    } = useRepositoryFilters(repositoriesMatchingSearchQuery)

    return {
      // 因为我们并不关心未经过滤的仓库
      // 我们可以在 `repositories` 名称下暴露过滤后的结果
      repositories: filteredRepositories,
      getUserRepositories,
      searchQuery,
      filters,
      updateFilters
    }
  }
}

基本就是这样了。

对于初学者而言,选项式API其实更好适应,组合式API的思维模式反而更难以接受,要熟练应用更是需要转换脑子。

Setup

前面只是将setup用起来了,并没有仔细介绍setup本身。

参数

setup 本质上是个函数,可以接收两个参数:

  1. props
  2. context

Props

setup 函数中的第一个参数是 props。它包含了组件所有的props选项中的变量。

记住,setup 函数中的 props 是响应式的,当传入新的 prop 时,它将被更新。

// MyBook.vue

export default {
  props: {
    title: String
  },
  setup(props) {
    console.log(props.title)
  }
}

但是,也正因为 props 是响应式的,所以你不能使用 ES6 语法解构它,这会消除 prop 的响应性。

如果需要解构 prop,可以在 setup 函数中使用 toRefs 函数来完成此操作(前面的大例子中就是这么做的):

// MyBook.vue

import { toRefs } from 'vue'

setup(props) {
  const { title } = toRefs(props)

  console.log(title.value)
}

如果 title 是可选的 prop,则传入的 props 中可能没有 title 。在这种情况下,toRefs 将不会为 title 创建一个 ref 。你需要使用 toRef 替代它:

// MyBook.vue
import { toRef } from 'vue'
setup(props) {
  const title = toRef(props, 'title')
  console.log(title.value)
}

Context

传递给 setup 函数的第二个参数是 context

context 是一个普通 JavaScript 对象,携带了其它可能在 setup 中有用的值,可以简单的理解为组件本身:

// MyBook.vue

export default {
  setup(props, context) {
    // Attribute (非响应式对象,等同于 $attrs)
    console.log(context.attrs)

    // 插槽 (非响应式对象,等同于 $slots)
    console.log(context.slots)

    // 触发事件 (方法,等同于 $emit)
    console.log(context.emit)

    // 暴露公共 property (函数)
    console.log(context.expose)
  }
}

context 是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着你可以安全地对 context 使用 ES6 解构。

// MyBook.vue
export default {
  setup(props, { attrs, slots, emit, expose }) {
    ...
  }
}

attrsslots 是有状态的对象,它们总是会随组件本身的更新而更新。这意味着你应该避免对它们进行解构,并始终以 attrs.xslots.x 的方式引用属性。请注意,与 props 不同,attrsslots 的 属性是响应式的。如果你打算根据 attrsslots 的更改应用副作用,那么应该在 onBeforeUpdate 生命周期钩子中执行此操作。

可以访问的组件选项

执行 setup 时,组件实例尚未被创建。因此,你只能访问以下属性:

  • props
  • attrs
  • slots
  • emit

换句话说,你将无法访问以下组件选项:

  • data
  • computed
  • methods

结合模板使用

如果 setup 返回一个对象,那么该对象的属性(键值)以及传递给 setupprops 参数中的属性都可以在模板中访问:

<!-- MyBook.vue -->
<template>
  <div>{{ collectionName }}: {{ readersNumber }} {{ book.title }}</div>
</template>

<script>
      // 这个reactive是干嘛的?
  import { ref, reactive } from 'vue'

  export default {
    props: {
      collectionName: String
    },
    setup(props) {
      const readersNumber = ref(0)
      const book = reactive({ title: 'Vue 3 Guide' })

      // 暴露给 template
      return {
        readersNumber,
        book
      }
    }
  }
</script>

注意,从 setup 返回的 refs 在模板中访问时是被自动浅解包的,因此不需要在模板中使用 .value

使用渲染函数

setup 还可以返回一个渲染函数,该函数可以直接使用在同一作用域中声明的响应式状态:

// MyBook.vue

import { h, ref, reactive } from 'vue'

export default {
  setup() {
    const readersNumber = ref(0)
    const book = reactive({ title: 'Vue 3 Guide' })
    // 请注意这里我们需要显式使用 ref 的 value
    return () => h('div', [readersNumber.value, book.title])
  }
}

返回一个渲染函数将阻止我们返回任何其它的东西。

从内部来说这不应该成为一个问题,但当我们想要将这个组件的方法通过模板 ref 暴露给父组件时就不一样了。

我们可以通过调用 expose 来解决这个问题,给它传递一个对象,其中定义的属性将可以被外部组件实例访问:

import { h, ref } from 'vue'
export default {
  setup(props, { expose }) {
    const count = ref(0)
    const increment = () => ++count.value

    expose({
      increment
    })

    return () => h('div', count.value)
  }
}

increment 方法现在将可以通过父组件的模板 ref 访问。

关于 this

setup() 内部,this 不是当前实例的引用,因为 setup() 是在解析其它组件选项之前被调用的,所以 setup() 内部的 this 的行为与其它选项中的 this 完全不同。这使得 setup() 在和其它选项式 API 一起使用时可能会导致混淆。

生命周期钩子

我们可以通过在生命周期钩子前面加上 “on” 来访问setup内的生命周期钩子函数,如下表所示。

选项式 API中的生命周期钩子 setup中的钩子函数
beforeCreate 不需要
created 不需要
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered
activated onActivated
deactivated onDeactivated

因为 setup 是围绕 beforeCreatecreated 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写。

这些钩子函数接受一个回调函数,当钩子被组件调用时将会被执行:

// MyBook.vue

export default {
  setup() {
    // mounted
    onMounted(() => {
      console.log('Component is mounted!')
    })
  }
}

Provide / Inject

我们也可以在组合式 API 中使用 provide/inject。但两者都只能在当前活动实例的 setup() 期间调用。

场景设想

假设我们要重写以下代码,其中包含一个 MyMap 组件,该组件使用组合式 API 为 MyMarker 组件提供用户的位置。

<!-- src/components/MyMap.vue -->
<template>
  <MyMarker />
</template>

<script>
import MyMarker from './MyMarker.vue'

export default {
  components: {
    MyMarker
  },
  provide: {
    location: 'North Pole',
    geolocation: {
      longitude: 90,
      latitude: 135
    }
  }
}
</script>

MyMarker组件如下所示:

<!-- src/components/MyMarker.vue -->
<script>
export default {
  inject: ['location', 'geolocation']
}
</script>

使用 Provide

setup() 中使用 provide 时,我们首先需要从 vue 显式导入 provide 方法。

provide 方法允许你通过两个参数定义属性:

  1. name (<String> 类型)
  2. value

MyMap 组件中,provide 的值可以按如下方式重构:

<!-- src/components/MyMap.vue -->
<template>
  <MyMarker />
</template>

<script>
import { provide } from 'vue'
import MyMarker from './MyMarker.vue'

export default {
  components: {
    MyMarker
  },
  setup() {
    provide('location', 'North Pole')
    provide('geolocation', {
      longitude: 90,
      latitude: 135
    })
  }
}
</script>

使用 inject

setup() 中使用 inject 时,也需要从 vue 显式导入。

inject 函数有两个参数:

  1. 要 inject 的属性的 name
  2. 默认值 (可选)

MyMarker 组件中,可以使用以下代码对其进行重构:

<!-- src/components/MyMarker.vue -->
<script>
import { inject } from 'vue'

export default {
  setup() {
    const userLocation = inject('location', 'The Universe')
    const userGeolocation = inject('geolocation')

    return {
      userLocation,
      userGeolocation
    }
  }
}
</script>

响应性

添加响应性

为了让 provide 值和 inject 值之间具有响应性,我们可以在 provide 值时使用 refreactive

MyMap 组件中,我们的代码可以更新如下:

<!-- src/components/MyMap.vue -->
<template>
  <MyMarker />
</template>

<script>
import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue'

export default {
  components: {
    MyMarker
  },
  setup() {
    const location = ref('North Pole')
    const geolocation = reactive({
      longitude: 90,
      latitude: 135
    })

    provide('location', location)
    provide('geolocation', geolocation)
  }
}
</script>

现在,如果这两个属性发生变化,MyMarker 组件也将自动更新!

修改响应式 property

当使用响应式 provide / inject 值时,建议尽可能将对响应式属性的所有修改限制在定义 provide 的组件内部

例如,在需要更改用户位置的情况下,我们最好在 MyMap 组件中执行此操作。

<!-- src/components/MyMap.vue -->
<template>
  <MyMarker />
</template>

<script>
import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue'

export default {
  components: {
    MyMarker
  },
  setup() {
    const location = ref('North Pole')
    const geolocation = reactive({
      longitude: 90,
      latitude: 135
    })

    provide('location', location)
    provide('geolocation', geolocation)

    return {
      location
    }
  },
  methods: {
    updateLocation() {
      this.location = 'South Pole'
    }
  }
}
</script>

然而,有时我们需要在注入数据的组件内部更新 inject 的数据。在这种情况下,我们建议 provide 一个方法来负责改变响应式 property。

<!-- src/components/MyMap.vue -->
<template>
  <MyMarker />
</template>

<script>
import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue'

export default {
  components: {
    MyMarker
  },
  setup() {
    const location = ref('North Pole')
    const geolocation = reactive({
      longitude: 90,
      latitude: 135
    })

    const updateLocation = () => {
      location.value = 'South Pole'
    }

    provide('location', location)
    provide('geolocation', geolocation)
    provide('updateLocation', updateLocation)
  }
}
</script>
<!-- src/components/MyMarker.vue -->
<script>
import { inject } from 'vue'

export default {
  setup() {
    const userLocation = inject('location', 'The Universe')
    const userGeolocation = inject('geolocation')
    const updateUserLocation = inject('updateLocation')

    return {
      userLocation,
      userGeolocation,
      updateUserLocation
    }
  }
}
</script>

最后,如果要确保通过 provide 传递的数据不会被 inject 的组件更改,我们建议对提供者的属性使用 readonly限制。

<!-- src/components/MyMap.vue -->
<template>
  <MyMarker />
</template>

<script>
import { provide, reactive, readonly, ref } from 'vue'
import MyMarker from './MyMarker.vue'

export default {
  components: {
    MyMarker
  },
  setup() {
    const location = ref('North Pole')
    const geolocation = reactive({
      longitude: 90,
      latitude: 135
    })

    const updateLocation = () => {
      location.value = 'South Pole'
    }

    provide('location', readonly(location))
    provide('geolocation', readonly(geolocation))
    provide('updateLocation', updateLocation)
  }
}
</script>

模板引用

在使用组合式 API 时,响应式引用和模板引用的概念是统一的。

为了获得对模板内元素或组件实例的引用,我们可以像往常一样声明 ref 并从 setup() 返回:

<template> 
  <div ref="root">This is a root element</div>
</template>

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

  export default {
    setup() {
      const root = ref(null)

      onMounted(() => {
        // DOM 元素将在初始渲染后分配给 ref
        console.log(root.value) // <div>This is a root element</div>
      })

      return {
        root
      }
    }
  }
</script>

这里我们在渲染上下文中暴露 root,并通过 ref="root",将其绑定到 div 作为其 ref。

作为模板使用的 ref 的行为与任何其他 ref 一样:它们是响应式的,可以传递到 (或从中返回) 复合函数中。

JSX 中的用法

export default {
  setup() {
    const root = ref(null)

    return () =>
      h('div', {
        ref: root
      })

    // with JSX
    return () => <div ref={root} />
  }
}

v-for 中的用法

组合式 API 模板引用在 v-for 内部使用时没有特殊处理。

相反,请使用函数引用执行自定义处理:

<template>
  <div v-for="(item, i) in list" :ref="el => { if (el) divs[i] = el }">
    {{ item }}
  </div>
</template>

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

  export default {
    setup() {
      const list = reactive([1, 2, 3])
      const divs = ref([])

      // 确保在每次更新之前重置ref
      onBeforeUpdate(() => {
        divs.value = []
      })

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

侦听模板引用

侦听模板引用的变更可以替代前面例子中演示使用的生命周期钩子。

但与生命周期钩子的一个关键区别是,watch()watchEffect() 在 DOM 挂载或更新之前运行副作用,所以当侦听器运行时,模板引用还未被更新。

<template>
  <div ref="root">This is a root element</div>
</template>

<script>
  import { ref, watchEffect } from 'vue'

  export default {
    setup() {
      const root = ref(null)

      watchEffect(() => {
        // 这个副作用在 DOM 更新之前运行,因此,模板引用还没有持有对元素的引用。
        console.log(root.value) // => null
      })

      return {
        root
      }
    }
  }
</script>

因此,使用模板引用的侦听器应该用 flush: 'post' 选项来定义,这将在 DOM 更新运行副作用,确保模板引用与 DOM 保持同步,并引用正确的元素。

<template>
  <div ref="root">This is a root element</div>
</template>

<script>
  import { ref, watchEffect } from 'vue'

  export default {
    setup() {
      const root = ref(null)

      watchEffect(() => {
        console.log(root.value) // => <div>This is a root element</div>
      }, 
      {
        flush: 'post'
      })

      return {
        root
      }
    }
  }
</script>

 动画的过渡 Mixin 

评论总数: 0


点击登录后方可评论