VUE 3——4:组件内部的组合式 API

复用与组合对工程化的大项目更有意义,因此我们提前介绍了如何创建 Vue 3 项目,简单创建并创建使用了一些 SFC,接下来就来介绍如何实现 SPC 的复用与组合。

一,组合式 API 简介

(一)组合式 API 需要解决什么问题

假设我们的应用能显示某个用户的仓库列表,其中还有搜索和筛选功能,代码如下:

<script>
export default {
  name: "UserRepositories",
  // 三个子组件
  components: {
    RepositoriesList,     // 列表,1
    RepositoriesSearchBy, // 搜索,2
    RepositoriesFilters,  // 过滤,3
  },
  props: {
    user: {
      type: String,
      required: true
    }
  },
  data() {
    return {
      repositories: [],   // 1
      searchQuery: '',    // 2
      filters: {},        // 3
    }
  },
  computed: {
    filteredRepositories() {            // 3
    },
    repositoriesMatchingSearchQuery() { // 2
    },
  },
  watch: {
    user: 'getUserRepositories'         // 1
  },
  methods: {
    getUserRepositories() {             // 1
      // 使用 `this.user` 获取用户仓库
    },
    updateFilters() {                   // 1
      // 更新 `this.filters`
    },
  },
  mounted() {
    // 在实例挂载完成后调用方法获取用户仓库
    this.getUserRepositories()			// 1
  }
}
</script>

该组件有以下几个职责:

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

尽管在组件中使用 (datacomputedmethodswatch) 组件的选项式 API 来组织逻辑关注点(上面代码中包含数字的注释)通常都很有效,就像以前我们做的那样。

然而,当我们的组件开始变得更大时,就完全有理由相信逻辑关注点的列表也会增长,这会导致组件难以阅读和维护:
在这里插入图片描述

  • 逻辑关注点按颜色进行分组。

选项式 API 的这种代码分离,让我们在处理单个逻辑关注点时,需要经常不断地在相关代码的选项块之间跳转,这严重地然乱了逻辑顺序,进而掩盖了潜在的逻辑问题。

如果能够将同一个逻辑关注点相关代码收集在一起就好了,而这正是组合式 API 所能做的。

(二)组合式 API 基础

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

1,setup 组件选项

和软件测试的单元测试中设置固件时使用的 setUp() 方法一样,setup 选项在组件创建之前执行,一旦 props 被解析,setup 选项就将作为组合式 API 的入口。

  • 因为 setup 的调用发生在 props 被解析之后、在 datacomputedmethods 被解析之前,所以在 setup 中无法使用 this

setup 选项是一个接收 propscontext 的函数,并能够将它返回的所有内容都暴露给组件的其余部分 (datamethodscomputedwatch、生命周期钩子等等) 以及组件的模板。

举个例子🌰:

<body>
<div class="app">

</div>

<script>
    const Root = {
        setup() {
            return {
                name: 'Root',
                handleClick: () => {
                    alert('done!');
                },
            }
        },
        template: `
            <button @click="handleClick">点我!</button>
        `,
    };

    const app = Vue.createApp(Root);

    const vm = app.mount('.app');
</script>

在这里插入图片描述

回到教程👨‍💻,这就来把 setup 添加到组件中:

// src/components/UserRepositories.vue

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

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

首先来提取第一个逻辑关注点:从假定的外部 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 {
	    repositories,
	    getUserRepositories // 返回的函数与 methods 的行为相同
	  }
	}
...

我们设计了一个异步函数,这个函数通过一个函数从一个 API 中获取仓库数据,并将它们更新到一个数组变量中。

但还有个问题,因为 repositories 变量是非响应式的,这意味着从用户的角度来看,仓库列表将始终为空。
举个例子🌰:说明非响应式变量:

<div id="app"></div>

<script>
    const Root = {
        setup() {
            let name = 'root';
            // 每过 2 秒钟,更新一下 name
            setInterval(() => {
                name = name === 'root' ? 'ROOT' : 'root';
            }, 2000);

            return {
                name
            }
        },
        template: `
          <p>{{ name }}</p>
        `,
    };

    const app = Vue.createApp(Root);

    const vm = app.mount('#app');
</script>

会发向并没有像我们期望的那样:每过 2 秒钟,更新一下 name。就是因为 name 是非响应式的。

2,用 ref 或 reactive 创建响应式变量

在 Vue 3.0 中,可以通过一个 ref 函数或 reactive 函数创建响应式变量,从而使得这个变量能在任何地方响应式地起作用。

let name = 'root';
let reactivityName = ref(name);
  • ref 为我们要使用的基础类型的数据(比如字符串、数字)创建了一个响应式引用
  • 非基础类型的数据(数组、对象、Map 和 Set )可使用 reactive 来处理。

但比较特殊的是,变量在作为参数传入到 ref 函数后,会被包裹在一个带有 value property 的对象(proxy({value:0}))中返回。

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

将变量封装为引用对象后,就可以使用 value property 访问或更改 ref 响应式变量的值:

<div class="app"></div>

<script>
    const Root = {
        setup() {
            const {ref} = Vue;          // 只在html中这么用,SFC 中提前用 import { ref } from 'vue' 导入,处理简单数据类型
            const {reactive} = Vue;     // 用 reactive 处理数组与对象
            let name = 'root';
            let nameObj = {
                name: 'ROOT'
            };
            let reactivityName = ref(name);             // 包装为响应式数据
            let reactivityNameObj = reactive(nameObj);  // 包装为响应式数据
            // 每过 1 秒钟,更新一下 name
            setInterval(() => {
                reactivityName.value = reactivityName.value === 'root' ? 'ROOT' : 'root';       // 使用 .value 获取或变更 ref 响应式数据的在值
                reactivityNameObj.name = reactivityNameObj.name === 'root' ? 'ROOT' : 'root';
            }, 1000);

            return {                  // 返回的数据会被自动挂载到组件实例上
                reactivityName,
                reactivityNameObj
            }
        },
        // 在 DOM 中直接<p>{{ reactivityName.value }}</p>不会显示变化,因为 vue 会自动浅层次解包内部值,
        // 只有访问嵌套的 ref 时需要在模板中添加 .value,具体详见 Vue 官网文档:响应式状态解构
        // 在模板中可直接像下面这样用:
        template: `
          <p>{{ reactivityName }}</p>
          <p>{{ reactivityNameObj.name }}</p>
        `,
    };

    const app = Vue.createApp(Root);

    const vm = app.mount('.app');
</script>

在这里插入图片描述
refreactive 创建响应式变量可一定程度上代替 data property。

回到教程👨‍💻,这就来将 repositories 变量创建为响应式变量:

// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref } from 'vue'
	...
	// 在我们的组件中
	setup (props) {
	  const repositories = reactive([])
	  const getUserRepositories = async () => {
	    repositories.value = await fetchUserRepositories(props.user)
	  }
	
	  return {
	    repositories,
	    getUserRepositories
	  }
	}
	...

从此开始,每当我们调用 getUserRepositories 时,如果结果有变化,则 repositories 将发生同样的变化。

  • 如果需要,完全可以使用 readonly 函数将变量变为只读变量。

3,在 setup 内使用生命周期钩子

接口准备好了,调用接口的函数准备好了,存储接口数据的响应式变量也准备好了,现在就来执行仓库获取逻辑吧。

那么,在哪里调用查询函数 getUserRepositories 呢?

在这个系列的最开始,我们说过,每个组件在被创建时都要经过一系列的初始化过程,在初始化过程中就会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会:
在这里插入图片描述
从这张生命周期图中我们能看到正在使用的 setup 函数。

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

恰恰 Vue 也提供这些方法。这些函数接受一个回调,当钩子被组件调用时,该回调将被执行。组合式 API 上的生命周期钩子与选项式 API 的名称相同,但需要加个前缀 on,即 mounted 看起来会像 onMounted

回到教程👨‍💻,这里就在 setup 中使用 onMounted 来回调查询函数 getUserRepositories:

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

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

4,watch 响应式更改

因为查询函数使用父组件传入的 user 作为参数,显然我们需要为不同的 user 进行查询操作,所以就需要随时监听 user 的变化。

就像我们在组件中使用 watch 选项监听 data 中的 user 一样,我们也可以使用从 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,watch 将触发并执行回调 (第二个参数),在本例中,它将把 ‘The new counter value is:5’ 记录到控制台中。

等效的选项式 API:

  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) {
	  const { user } = toRefs(props)	// 从父组件传入的用户信息
	  const repositories = ref([])
	  const getUserRepositories = async () => {
	    repositories.value = await fetchUserRepositories(user.value)
	  }
	  onMounted(getUserRepositories)
	
	  watch(user, getUserRepositories)
	
	  return {
	    repositories,
	    getUserRepositories
	  }
	}

至此,我们已经把第一个逻辑关注点移到了同一个地方,看一下整体代码:

<template>

</template>

<script>
import {fetchUserRepositories} from '@/api/repositories'
import {ref, onMounted, watch, toRefs} from 'vue'

export default {
  name: "UserRepositories",
  components: {RepositoriesFilters, RepositoriesSortBy, RepositoriesList},
  props: {
    user: {
      type: String,
      required: true
    }
  },
  // 在我们的组件内
  setup(props) {
    // 使用 `toRefs` 解包的方法创建对 `props` 中的 `user` property 的响应式引用
    const {user} = toRefs(props)
    // 创建一个响应式的 `repositories` 数组变量,准备存储从外部 API 获取到的仓库信息列表
    const repositories = ref([])
    // 初始化本地仓库列表数据
    const getUserRepositories = async () => {
      repositories.value = await fetchUserRepositories(user.value)
    }
    // 在组件挂载时获取数据
    onMounted(getUserRepositories);

    // 在 user prop 的响应式引用上设置一个侦听器,用于根据用户信息获取对应的仓库列表信息
    watch(user, getUserRepositories)
    
    return {
      repositories,       // 返回一个响应式变量,
      getUserRepositories // 返回的函数与方法的行为相同
    }
  }
}
</script>

<style scoped>

</style>

5,独立的 computed 属性

我们现在可以对第二个逻辑关注点执行相同的操作——基于 searchQuery 进行过滤。

在选项式 API 中,我们介绍过 computed property,我们可用它来简化模板中的计算运算。而且使用 computed property 的一个好处就是,它将基于响应依赖关系进行计算缓存,只会在相关响应式依赖发生改变时才重新求值。

而组合式 API 也提供了 computed 函数来在 Vue 组件外部创建计算属性。
举个例子🌰:

<div class="app"></div>

<script>
    const Root = {
        setup() {
            const {ref, computed} = Vue;
            const counter = ref(0);
            const handClick = () => {
                counter.value += 1;
            };
            const counterAddFive = computed(() => {
                return counter.value * 2;
            });

            return {
                counter,         // 曝露出去的状态数据
                counterAddFive,  // 曝露出去的计算属性
                handClick        // 曝露出去的方法
            }
        },
        template: `
          <button @click="handClick">{{ counter }}</button>——<span> {{ counterAddFive }} </span>
        `,
    };

    const app = Vue.createApp(Root);

    const vm = app.mount('.app');
</script>
  • 这里我们给 computed 函数传递了一个箭头函数作为参数,它是一个类似 getter 的回调函数,输出的是一个只读的响应式引用。为了访问新创建的计算变量的 value,我们需要像 ref 一样使用 .value property。

回到教程👨‍💻,将搜索功能移到 setup 中:

// src/components/UserRepositories.vue
<template>

</template>

<script>
import {fetchUserRepositories} from '@/api/repositories'
import {ref, onMounted, watch, toRefs, computed} from 'vue'

export default {
  name: "UserRepositories",
  components: {RepositoriesFilters, RepositoriesSortBy, RepositoriesList},
  props: {
    user: {
      type: String,
      required: true
    }
  },
  // 在我们的组件内
  setup(props) {
    /*关注点一*/
    // 使用 `toRefs` 解包的方法创建对 `props` 中的 `user` property 的响应式引用
    const {user} = toRefs(props)
    // 创建一个响应式的 `repositories` 数组变量,准备存储从外部 API 获取到的仓库信息列表
    const repositories = ref([])
    // 初始化本地仓库列表数据
    const getUserRepositories = async () => {
      repositories.value = await fetchUserRepositories(user.value)
    }
    // 在组件挂载时获取数据
    onMounted(getUserRepositories);
    
    /*关注点二*/
    // 在 user prop 的响应式引用上设置一个侦听器,用于根据用户信息获取对应的仓库列表信息
    watch(user, getUserRepositories)

    /*关注点三*/
    // 创建一个响应式的字符串,用作过滤条件
    const searchQuery = ref('')
    // 通过 `computed` 创建一个响应式的函数,用于过滤数据
    const repositoriesMatchingSearchQuery = computed(() => {
      return repositories.value.filter(
          repository => repository.name.includes(searchQuery.value)   // 根据 searchQuery 进行仓库过滤
      )})

    return {
      repositories,                       // 曝露出去的状态数据,仓库列表
      getUserRepositories,                // 曝露出去的方法,用于初始化仓库列表
      searchQuery,                        // 曝露出去的状态数据,查询条件
      repositoriesMatchingSearchQuery     // 曝露出去的方法,用于查询仓库
    }
  }
}
</script>

<style scoped>

</style>

这样一来,我们已经将所有逻辑关注点都放到 setup 中了,但这又让 setup 变得比较臃肿,

因此,我们进一步模块化代码,将各个关注点提取到一个个独立的组合式函数中,作具体的功能进行分组。

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

// src/utils/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/utils/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/utils/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,
    }
  }
}

最后,迁移剩余的过滤功能(这里就不关心怎么实现的):

// src/utils/UserRepositories.vue

import useUserRepositories from '@/composables/useUserRepositories'
import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
import useRepositoryFilters from '@/composables/useRepositoryFilters'
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)
    // 获取仓库过滤器
    const {
      filters,
      updateFilters,
      filteredRepositories
    } = useRepositoryFilters(repositoriesMatchingSearchQuery)

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

Done!至此,我们:

  1. 将选项式 API 重构为组合式 API,实现逻辑关注点分离。
  2. 对组合式 setup() 做功能拆分,进一步简化代码复杂度,同时有利于对拆分出的内容进行进一步的复用。

总之,组合式 API 的核心诉求就是提高组件、逻辑的复用性。

接下来就把用到的一些重要概念和细节拆开来看看。

二,Setup

(一)参数说明

1,props

先回忆一下,组件中的 props property 接收来自父组件的数据,然后整个组件中都可使用。

这里之所以将响应式的 props 作为第一个参数传入 setup 函数,是为了方便在 setup 函数中使用其中的一些数据来完成某些初始化工作(数据状态的初始化以及方法的准备)。

src/App.vue:

<template>
  <img alt="Vue logo" src="./assets/logo.png">
  <HelloWorld name="root" msg="welcome to your Vue.js App"/>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  }
}
</script>

需要注意的是,在 setup 函数中不能使用 ES6 语法解包这个参数,它会消除 prop 的响应性,而应该使用 toRefs 函数来完成此操作——为响应式的对象提供解包/展开能力:

// src/components/HelloWorld.vue

<template>
  <div class="hello">
    <h1>{{name}}, {{ msg }}</h1>
  </div>
</template>

<script>
import {toRef, toRefs} from "vue";

export default {
  name: 'HelloWorld',
  props: {
    name:String,
    msg: String
  },
  setup(props) {
    // 输出 props 的 prop 值
    console.log(props.name, props.msg)

    // 使用 toRefs() 将props转换为可解包的对象
    const {name, msg} = toRefs(props)
    console.log(name.value, msg.value)
  }
}
</script>

还可以使用 toRef() 函数创建本处需要但可能未传入的 prop —— 为对象的某个属性提供响应式能力。

// src/components/HelloWorld.vue

<template>
  <div class="hello">
    <h1>{{name}}, {{ msg }}</h1>
  </div>
</template>

<script>
import {toRef, toRefs} from "vue";

export default {
  name: 'HelloWorld',
  props: {
    name:String,
    msg: String
  },
  setup(props) {
    // 使用 toRef() 创建对象的某属性的响应式形式
    const title = toRef(props, 'title', '<TITLE>')
    console.log(title.value)
  }
}
</script>

2,Context

传递给 setup 函数的第二个参数是 contextcontext 是一个普通 JavaScript 对象,是非响应式的,可以是 attrs, slots, emit, expose:

<script>
export default {
  name: 'HelloWorld',
  props: {
    name: String,
    msg: String
  },
  setup(props, context) {
    // Attribute (非响应式对象,等同于 $attrs)
    console.log(context.attrs)

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

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

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

因为 attrs property 和 slots property 是非响应式的。如果你打算根据 attrsslots 的变更应用副作用,那么应该在 onBeforeUpdate 生命周期钩子中执行此操作。

总之,执行 setup 时,你只能访问propsattrsslotsemit

(二)在模板使用暴露出来的内容

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

src/App.vue
<template>
  <img alt="Vue logo" src="./assets/logo.png">
  <HelloWorld collectionName="Vue 3 Guide"></HelloWorld>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  }
}
</script>


src/components/HelloWorld.vue
<template>
  <p> {{ collectionName }} ({{ version }}):</p>
  <div v-for="(blogInfo,key) in blogsInfo" :key="key">
    {{ blogInfo.title }}
    <span>{{ blogInfo.likes }}</span>
  </div>
</template>

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

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

    const version = ref("v1.2.3")
    const blogs = [
      {id: 1, title: 'My journey with Vue😘', likes: 23},
      {id: 2, title: 'Blogging with Vue🚄', likes: 14},
      {id: 3, title: 'Why Vue is so interesting🥰', likes: 100}
    ]

    const blogsInfo = reactive(blogs)

    // 暴露给 template
    return {
      version,
      blogsInfo
    }
  }
}
</script>

在这里插入图片描述
之前也说过,从 setup 返回的 refs 在模板中访问时是被自动浅解包的,因此不应在模板中使用 .value 而应该直接使用变量。

(三)使用 <script setup> 简化 setup()

三,生命周期钩子

setup 内部支持以下生命周期钩子:
在这里插入图片描述

四,Provide / Inject

Provide / Inject 的存在简化了嵌套组件之间的自顶向下的数据传递过程。

(一)在 setup 内部使用 Provide / Inject

通过一个例子说明: 这里有一个 MyMap 组件,该组件使用组合式 API 为 MyMarker 子组件提供用户的位置。

setup 中使用 provide 时,首先从 vue 显式导入 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>

setup 中使用 inject 时,也需要显式导入 inject 方法::

<!-- 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 值和 inject 值之间的响应性,我们可以在 provide 值时使用 refreactive(使用前者还是后者,在最上面讲过):

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

这样一来,当父组件 MyMap 提供的位置信息变化时,子组件 MyMarker 也将自动更新!

当使用响应式 provide / inject 值时,官方建议尽可能将对响应式 property 的所有修改限制在定义 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 一个方法来负责响应式变更:

<!-- 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 变更方法
    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 的组件更改,官方建议对 provideer 的 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 允许我们分离关注点,通过对大型的 SFC内部的逻辑进行切割与整合,有利于后期的代码维护。

五,组合式 API 的响应式

(一)响应式对象的创建与解包

上面将国,reactive() 会创建一个响应式的对象、数组 Map 和 Set 这样的集合类型,但实际上它就是返回一个原始对象的响应式代理对象,它和原始对象是不相等的:

const raw = {}
const proxy = reactive(raw)

// 代理对象和原始对象不是全等的
console.log(proxy === raw) // false

// 在同一个对象上调用 reactive() 会返回相同的代理
console.log(reactive(raw) === proxy) // true

// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(proxy) === proxy) // true

也就是说,只有代理对象是响应式的,更改原始对象不会触发更新。因此,为获得响应式能力,我们应该使用代理对象而不是原始对象。

使用组合式 API 的问题就是 ref 和响应式对象到底用哪个:

  • 原始响应式对象存在解构丢失响应性的问题,而代理对象需要到处使用 .value 则感觉很繁琐,并且在没有类型系统的帮助时很容易漏掉 .value

更多组合式 API 中的响应式内容请参考 响应式基础

(二)使用 <script setup>

setup() 函数中手动返回老暴露大量的状态和方法可能会非常繁琐,可以使用 <script setup> 来大幅度地简化代码:

  • <script setup> 中的顶层的导入、函数和变量声明等可在同一组件的模板中直接使用。可以理解为模板中的表达式和 <script setup> 中的代码处在同一个作用域中—— <script setup>里面的代码会被编译成组件 setup() 函数的内容。
<template>
  <MyComponent />
  <div>{{ capitalize('hello') }}</div>
  <button @click="increment">
    {{ state.count }}
  </button>
</template>

<script setup>
import {reactive} from 'vue'
import { capitalize } from './helpers'

import MyComponent from './MyComponent.vue'

const state = reactive({count: 0})  // reactive() is a function that returns a reactive object

// a function that increments the count property of the reactive state object.
// this type of function is always called as a event handler.
function increment() {
  state.count++
}
</script>

使用 <script setup> 时没有明显的 props 参数,所以需要 defineProps() 函数和 defineEmits() 函数来进行显式的声明:

<script setup>
const props = defineProps({
  foo: String
})

const emit = defineEmits(['change', 'delete'])
</script>

尽管在 <script setup> 使用 slotsattrs 的情况应该是相对来说较为罕见的——因为可以在模板中直接通过 $slots$attrs 来访问。但确实要使用的话,可以通过 useSlots() 函数和 useAttrs() 函数来获取:

<script setup>
import { useSlots, useAttrs } from 'vue'

const slots = useSlots()
const attrs = useAttrs()
</script>

官方推荐在组合式 API 中使用 SFC + <script setup> 的语法。

Vue3之script-setup全面解析
上手后才知道 ,Vue3 的 script setup 语法糖是真的爽

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值