VUE 组合式API

参考资料

介绍

什么是组合式API

通过创建Vue组件,我们可以将界面中重复的部分连同其功能一起提取为可复用的代码段。但是仅仅如此还不够,尤其当应用变得非常大的时候,共享和重用代码变得尤其重要。

假设我们的应用中有一个显示某个用户的仓库列表的视图。此外,我们还希望有搜索和筛选功能。实现此视图组件的代码可能如下所示:

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

该组件有以下几个职责:

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

使用(data,computed,methods,watch)组件选项来组织逻辑通常都很有效。然而,当我们的组件开始变得更大时,逻辑关注点的列表也会增长并逐渐碎片化。导致代码难以阅读、理解和维护。

此时,选项式的代码会暴露出这些问题,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块。

如果能够将同一个逻辑关注点相关代码收集在一起会更好。而这正是组合式API使我们能够做到的。

组合式API基础

使用组合式API,首先需要一个可以实际使用它的地方。在Vue组件中,我们将此位置称为setup

setup组件选项

新的setup选项在组件创建之前执行,一旦props被解析,就会作为组合式API的入口。

注意setup中应该避免使用this,因为它不会找到组件实例。

setup选项是一个接收propscontext的函数,我们将setup返回的所有内容暴露给组件的其余部分(计算属性,方法,生命周期钩子等)以及组件的模板。

让我们把setup添加到组件中:

export default{
	comoponents:{RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
	props:{
		user: {
		      type: String,
		      required: true
	    }
	},
	setup(props){
		console.log(props) // { user: '' }	
	    return {} // 这里返回的任何内容都可以用于组件的其余部分
	},
	//其余部分
}

现在让我们从提取第一个逻辑关注点开始(在原始代码段中标记为“1”)

1.从假定的外部API获取该用户的仓库,并在用户有任何更改时进行刷新

我们将从最明显的部分开始:

  • 仓库列表
  • 更新仓库列表的函数
  • 返回列表和函数,以便其他组件可以对它们进行访问
// src/components/UserRepositories.vue `setup` function
import {fetchUserRepositories} from '@/api/repositories'

//在我们的组件内
setup(props){
	let repositories=[]
	const getUserRepositories=async()=>{
		repositories=await fetchUserRepositories(props.user)
	}
	return {
		repositoires,
		getUserRepositories// 返回的函数与方法的行为相同
	}
}

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

带ref的响应式变量

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

import {ref} from 'vue'
const counter=ref(0)

ref接收参数并将其包裹在一个带有valueproperty的对象中返回,然后可以使用该property访问或更改响应式变量的值:

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

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

在任何值周围都有一个封装对象,这样我们就可以在整个应用中安全地传递它,而不用担心在某个地方失去它的响应性。
换句话说,ref为我们的值创建了一个响应式引用。在整个组合式API中经常会使用引用的概念。

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

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

	//在我们的组件中
	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'

exprot 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 Prop发生变化时执行此操作。

我们将从生命周期钩子开始。

在setup内注册生命周期钩子

为了使组合式API的功能和选项式API一样完整,我们还需要一种在setup中注册声明周期钩子的方法。这要归功于Vue导出的几个新函数。组合式API上的生命周期钩子与选项式API的名称相同,但前缀为on,即mounted对应onMounted

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

// 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
  }
}

现在我们需要对userprop的变化做出反应。为此,我们将使用独立的watch函数。

watch响应式更改

就像我们在组件中使用watch选项并在userproperty上设置侦听器一样,我们也可以从Vue导入的watch函数执行相同的操作。它接受3个参数:

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

它可以如下工作

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,侦听将触发并执行回调(第二个参数)。
以下是等效的选项式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` property 的响应式引用
  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。这是为了确保我们的侦听器能够根据userProp的变化做出反应。

有了这些变化,我们就把第一个逻辑关注点转移到了一个地方。现在可以对第二个关注点执行相同的操作----基于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)
console.log(twiceTheCounter.value)

这里我们给computed函数传递了第一个参数,它是一个类似getter的回调函数,输出的是一个只读的响应式引用 。为了访问新创建的计算变量的value,我们需要像ref一样使用.valueproperty

让我们将搜索功能移动到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` property 的响应式引用
  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('')
  const repositoriesMatchingSearchQuery = computed(() => {
    return repositories.value.filter(
      repository => repository.name.includes(searchQuery.value)
    )
  })

  return {
    repositories,
    getUserRepositories,
    searchQuery,
    repositoriesMatchingSearchQuery
  }

对于其他的逻辑关注点我们也可以这样做,但是这样就存在一个问题,setup选项变得非常大。针对这个问题,我们可以将上述代码提取到一个独立的组合式函数中。让我们从创建useUserRepositories函数开始:

// src/composables/useUserRepositories.js

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

export default function userUserRepositories(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
  }
}

现在我们有了两个单独的功能模块,接下来就可以开始在组件中使用它们了。以下是如何做到这一点:

// 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 {
      // 因为我们并不关心未经过滤的仓库
      // 我们可以在 `repositories` 名称下暴露过滤后的结果
      repositories: repositoriesMatchingSearchQuery,
      getUserRepositories,
      searchQuery,
    }
  },
  data () {
    return {
      filters: { ... }, // 3
    }
  },
  computed: {
    filteredRepositories () { ... }, // 3
  },
  methods: {
    updateFilters () { ... }, // 3
  }
}

剩余的过滤功能依然如此。

// 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
    }
  }
}

Setup

基于组合式API响应性原理

参数


使用setup函数时,它将接收两个参数:

  • props
  • context

Props

setup函数中的第一个参数是propssetup函数中的props是响应式的,当传入新的prop时,它将被更新。

注意 因为props是响应式的,所以不能使用ES6解构,否则会消除prop的响应性。

如果需要解构prop,可以在setup函数中使用toRefs函数来完成此操作:

import {toRefs} from 'vue'

setup(props){
	const {title}=toRefs(props)
	console.log(title.value)
}

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

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

Context

传递给setup函数的第二个参数context是一个普通的JavaScript对象,它暴露组件的三个property:

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

    // 插槽 (非响应式对象)
    console.log(context.slots)

    // 触发事件 (方法)
    console.log(context.emit)
  }
}

context不是响应式的,这意味着可以安全地对其进行ES6解构

export default{
	setup(props,{attrs,slots,emit}){
	...
	}
}

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

访问组件的property

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

  • props
  • attrs
  • slots
  • emit

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

  • data
  • computed
  • methods

结合模板使用

如果setup返回一个对象,那么该对象的property以及传递给setupprops参数中的property就可以在模板中访问到:

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

<script>
  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返回的ref是被自动浅解包的,因此不影在模板中使用.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])
  }
}

使用this

setup()内部,this不是该活跃实例的引用,因为setup()是在解析其它组件选项之前被调用的。

Provide/Inject

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

设想场景


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

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

<script>
import MyMarker from 'MyMap.vue'

export default{
	components:{
		MyMarker
	},
	provide:{
		location:'North Pole',
		geolocation:{
			longitude:90,
			latitude:135
		}
	}
}
</script>
<!--MyMarker.vue-->
<script>
exprot default{
	inject:['location','geolocation']
}
</script>

使用Provide


setup()中使用provide时,我们首先从vue显式导入provide方法。
provide函数允许通过两个参数定义property:

  • name(String类型)
  • value

使用MyMap组件后,provide的值可以按照如下方式重构:

<!--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函数有两个参数:

  • 要inject的属性名
  • 默认值(可选)

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

<!--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组件,我们的代码可以修改如下:

<!--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>

修改响应式property

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

然而,有时需要在注入数据的组件内部更新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的组件更改,建议对提供者的property使用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。在虚拟DOM补丁算法中,如果VNode的ref键对应于渲染上下文中的ref,则VNode的相应元素或组件实例将被分配给该ref的值。这是在虚拟DOM挂载/打补丁过程中执行的,因此模板引用只会在初始渲染之后获得赋值。

作为模板使用的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>
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值