Vue组合式函数,详细解析

什么是“组合式函数”?​

在 Vue 应用的概念中,“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。

当构建前端应用时,我们常常需要复用公共任务的逻辑。例如为了在不同地方格式化时间,我们可能会抽取一个可复用的日期格式化函数。这个函数封装了无状态的逻辑:它在接收一些输入后立刻返回所期望的输出。复用无状态逻辑的库有很多,比如你可能已经用过的 loadsh 或是 date-fns。

相比之下,有状态逻辑负责管理会随时间而变化的状态。一个简单的例子是跟踪当前鼠标在页面中的位置。在实际应用中,也可能是像触摸手势或与数据库的连接状态这样的更复杂的逻辑。

鼠标跟踪器示例​

如果我们要直接在组件中使用组合式 API 实现鼠标跟踪功能,它会是这样的:

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'const x = ref(0)
const y = ref(0)function update(event) {x.value = event.pageXy.value = event.pageY
}onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script><template>Mouse position is at: {{ x }}, {{ y }}</template>

但是,如果我们想在多个组件中复用这个相同的逻辑呢?我们可以把这个逻辑以一个组合式函数的形式提取到外部文件中:

// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'// 按照惯例,组合式函数名以“use”开头
export function useMouse() {// 被组合式函数封装和管理的状态const x = ref(0)const y = ref(0)// 组合式函数可以随时更改其状态。function update(event) {x.value = event.pageXy.value = event.pageY}// 一个组合式函数也可以挂靠在所属组件的生命周期上// 来启动和卸载副作用onMounted(() => window.addEventListener('mousemove', update))onUnmounted(() => window.removeEventListener('mousemove', update))// 通过返回值暴露所管理的状态return { x, y }
}

下面是它在组件中使用的方式:

<script setup>
import { useMouse } from './mouse.js'const { x, y } = useMouse()
</script><template>Mouse position is at: {{ x }}, {{ y }}</template>

如你所见,核心逻辑完全一致,我们做的只是把它移到一个外部函数中去,并返回需要暴露的状态。和在组件中一样,你也可以在组合式函数中使用所有的组合式API。现在,useMouse() 的功能可以在任何组件中轻易复用了。

更酷的是,你还可以嵌套多个组合式函数:一个组合式函数可以调用一个或多个其他的组合式函数。这使得我们可以像使用多个组件组合成整个应用一样,用多个较小且逻辑独立的单元来组合形成复杂的逻辑。实际上,这正是为什么我们决定将实现了这一设计模式的 API 集合命名为组合式 API。

举例来说,我们可以将添加和清除 DOM 事件监听器的逻辑也封装进一个组合式函数中:

// event.js
import { onMounted, onUnmounted } from 'vue'export function useEventListener(target, event, callback) {// 如果你想的话,// 也可以用字符串形式的 CSS 选择器来寻找目标 DOM 元素onMounted(() => target.addEventListener(event, callback))onUnmounted(() => target.removeEventListener(event, callback))
}

有了它,之前的 useMouse() 组合式函数可以被简化为:

// mouse.js
import { ref } from 'vue'
import { useEventListener } from './event'export function useMouse() {const x = ref(0)const y = ref(0)useEventListener(window, 'mousemove', (event) => {x.value = event.pageXy.value = event.pageY})return { x, y }
}

异步状态示例​

useMouse() 组合式函数没有接收任何参数,因此让我们再来看一个需要接收一个参数的组合式函数示例。在做异步数据请求时,我们常常需要处理不同的状态:加载中、加载成功和加载失败。

<script setup>
import { ref } from 'vue'const data = ref(null)
const error = ref(null)fetch('...').then((res) => res.json()).then((json) => (data.value = json)).catch((err) => (error.value = err))
</script><template><div v-if="error">Oops! Error encountered: {{ error.message }}</div><div v-else-if="data">Data loaded:<pre>{{ data }}</pre></div><div v-else>Loading...</div>
</template>

如果在每个需要获取数据的组件中都要重复这种模式,那就太繁琐了。让我们把它抽取成一个组合式函数:

// fetch.js
import { ref } from 'vue'export function useFetch(url) {const data = ref(null)const error = ref(null)fetch(url).then((res) => res.json()).then((json) => (data.value = json)).catch((err) => (error.value = err))return { data, error }
}

现在我们在组件里只需要:

<script setup>
import { useFetch } from './fetch.js'const { data, error } = useFetch('...')
</script>

接收响应式状态​

useFetch() 接收一个静态 URL 字符串作为输入——因此它只会执行一次 fetch 并且就此结束。如果我们想要在 URL 改变时重新 fetch 呢?为了实现这一点,我们需要将响应式状态传入组合式函数,并让它基于传入的状态来创建执行操作的侦听器。

举例来说,useFetch() 应该能够接收一个 ref:

const url = ref('/initial-url')const { data, error } = useFetch(url)// 这将会重新触发 fetch
url.value = '/new-url'

或者接收一个 getter 函数:

// 当 props.id 改变时重新 fetch
const { data, error } = useFetch(() => `/posts/${props.id}`)

我们可以用 watchEffect() toValue()  API 来重构我们现有的实现:

// fetch.js
import { ref, watchEffect, toValue } from 'vue'export function useFetch(url) {const data = ref(null)const error = ref(null)const fetchData = () => {// reset state before fetching..data.value = nullerror.value = nullfetch(toValue(url)).then((res) => res.json()).then((json) => (data.value = json)).catch((err) => (error.value = err))}watchEffect(() => {fetchData()})return { data, error }
}

约定和最佳实践​

命名​

组合式函数约定用驼峰命名法命名,并以“use”作为开头。

输入参数​

即便不依赖于 ref 或 getter 的响应性,组合式函数也可以接收它们作为参数。如果你正在编写一个可能被其他开发者使用的组合式函数,最好处理一下输入参数是 ref 或 getter 而非原始值的情况。可以利用 toValue() 工具函数来实现:

import { toValue } from 'vue'function useFeature(maybeRefOrGetter) {// 如果 maybeRefOrGetter 是一个 ref 或 getter,// 将返回它的规范化值。// 否则原样返回。const value = toValue(maybeRefOrGetter)
}

如果你的组合式函数在输入参数是 ref 或 getter 的情况下创建了响应式 effect,为了让它能够被正确追踪,请确保要么使用 watch() 显式地监视 ref 或 getter,要么在 watchEffect() 中调用 toValue()

返回值​

你可能已经注意到了,我们一直在组合式函数中使用 ref() 而不是 reactive()。我们推荐的约定是组合式函数始终返回一个包含多个 ref 的普通的非响应式对象,这样该对象在组件中被解构为 ref 之后仍可以保持响应性:

// x 和 y 是两个 ref
const { x, y } = useMouse()

从组合式函数返回一个响应式对象会导致在对象解构过程中丢失与组合式函数内状态的响应性连接。与之相反,ref 则可以维持这一响应性连接。

如果你更希望以对象属性的形式来使用组合式函数中返回的状态,你可以将返回的对象用 reactive() 包装一次,这样其中的 ref 会被自动解包,例如:

const mouse = reactive(useMouse())
// mouse.x 链接到了原来的 x ref
console.log(mouse.x)
Mouse position is at: {{ mouse.x }}, {{ mouse.y }}

副作用​

在组合式函数中的确可以执行副作用 (例如:添加 DOM 事件监听器或者请求数据),但请注意以下规则:

  • 如果你的应用用到了服务端渲染(SSR),请确保在组件挂载后才调用的生命周期钩子中执行 DOM 相关的副作用,例如:onMounted()。这些钩子仅会在浏览器中被调用,因此可以确保能访问到 DOM。

  • 确保在 onUnmounted() 时清理副作用。举例来说,如果一个组合式函数设置了一个事件监听器,它就应该在 onUnmounted() 中被移除 (就像我们在 useMouse() 示例中看到的一样)。当然也可以像之前的 useEventListener() 示例那样,使用一个组合式函数来自动帮你做这些事。

使用限制​

组合式函数只能在 <script setup> 或 setup() 钩子中被调用。在这些上下文中,它们也只能被同步调用。在某些情况下,你也可以在像 onMounted() 这样的生命周期钩子中调用它们。

这些限制很重要,因为这些是 Vue 用于确定当前活跃的组件实例的上下文。访问活跃的组件实例很有必要,这样才能:

  1. 将生命周期钩子注册到该组件实例上

  2. 将计算属性和监听器注册到该组件实例上,以便在该组件被卸载时停止监听,避免内存泄漏。

<script setup> 是唯一在调用 await 之后仍可调用组合式函数的地方。编译器会在异步操作之后自动为你恢复当前的组件实例。

通过抽取组合式函数改善代码结构​

抽取组合式函数不仅是为了复用,也是为了代码组织。随着组件复杂度的增高,你可能会最终发现组件多得难以查询和理解。组合式 API 会给予你足够的灵活性,让你可以基于逻辑问题将组件代码拆分成更小的函数:

<script setup>
import { useFeatureA } from './featureA.js'
import { useFeatureB } from './featureB.js'
import { useFeatureC } from './featureC.js'const { foo, bar } = useFeatureA()
const { baz } = useFeatureB(foo)
const { qux } = useFeatureC(baz)
</script>

在某种程度上,你可以将这些提取出的组合式函数看作是可以相互通信的组件范围内的服务。

在选项式 API 中使用组合式函数​

如果你正在使用选项式 API,组合式函数必须在 setup() 中调用。且其返回的绑定必须在 setup() 中返回,以便暴露给 this 及其模板:

import { useMouse } from './mouse.js'
import { useFetch } from './fetch.js'export default {setup() {const { x, y } = useMouse()const { data, error } = useFetch('...')return { x, y, data, error }},mounted() {// setup() 暴露的属性可以在通过 `this` 访问到console.log(this.x)}// ...其他选项
}

与其他模式的比较​

和 Mixin 的对比​

Vue 2 的用户可能会对 mixins 选项比较熟悉。它也让我们能够把组件逻辑提取到可复用的单元里。然而 mixins 有三个主要的短板:

  1. 不清晰的数据来源:当使用了多个 mixin 时,实例上的数据属性来自哪个 mixin 变得不清晰,这使追溯实现和理解组件行为变得困难。这也是我们推荐在组合式函数中使用 ref + 解构模式的理由:让属性的来源在消费组件时一目了然。

  2. 命名空间冲突:多个来自不同作者的 mixin 可能会注册相同的属性名,造成命名冲突。若使用组合式函数,你可以通过在解构变量时对变量进行重命名来避免相同的键名。

  3. 隐式的跨 mixin 交流:多个 mixin 需要依赖共享的属性名来进行相互作用,这使得它们隐性地耦合在一起。而一个组合式函数的返回值可以作为另一个组合式函数的参数被传入,像普通函数那样。

基于上述理由,我们不再推荐在 Vue 3 中继续使用 mixin。保留该功能只是为了项目迁移的需求和照顾熟悉它的用户。

和无渲染组件的对比​

在组件插槽一章中,我们讨论过了基于作用域插槽的无渲染组件。我们甚至用它实现了一样的鼠标追踪器示例。

组合式函数相对于无渲染组件的主要优势是:组合式函数不会产生额外的组件实例开销。当在整个应用中使用时,由无渲染组件产生的额外组件实例会带来无法忽视的性能开销。

我们推荐在纯逻辑复用时使用组合式函数,在需要同时复用逻辑和视图布局时使用无渲染组件。

和 React Hooks 的对比​

如果你有 React 的开发经验,你可能注意到组合式函数和自定义 React hooks 非常相似。组合式 API 的一部分灵感正来自于 React hooks,Vue 的组合式函数也的确在逻辑组合能力上与 React hooks 相近。然而,Vue 的组合式函数是基于 Vue 细粒度的响应性系统,这和 React hooks 的执行模型有本质上的不同。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/808395.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AONMIX和WAKEUPMIX的区别

在 ARM 芯片中&#xff0c;术语 AONMIX 指的是 i.MX 93 处理器的输入/输出多路复用控制器。让我们来详细了解一下&#xff1a; i.MX 93 处理器&#xff1a; i.MX 93 是恩智浦半导体公司开发的一种基于 ARM 的处理器。它通常用于各种嵌入式系统和应用。 IOMUX&#xff08;输入/…

js手写并发请求控制

关键点 new promise 一经创建&#xff0c;立即执行 使用 Promise.resolve().then 可以把任务加到微任务队列&#xff0c;防止立即执行迭代方法 微任务处理过程中&#xff0c;产生的新的执行微任务&#xff0c;会在同一事件循环内&#xff0c;追加到微任务队列里 使用 race 在…

【微命令】git 如何修改某个分支的名字(git branch -m newbranch)

简要信息&#xff0c;快速记录 命令 # 切换到某个需要修改的分支 git checkout oldbranch# 修改分支名字 git branch -m newbranch假设作为git设计者&#xff0c;要用来修改branch的命令&#xff0c;那么就是 git branch作为前缀&#xff0c;然后进一步修改的命令是branch相关…

gradio简单搭建——关键词简单筛选【2024-4-11优化】

gradio简单搭建——关键词简单筛选[2024-4-11 优化] 新的思路&#xff1a;标签自动标注界面搭建优化数据处理与生成过程交互界面展示 新的思路&#xff1a;标签自动标注 针对通过关键词&#xff0c;在文本数据中体现出主体的工作类型这一任务&#xff0c;这里使用展示工具grad…

2012年认证杯SPSSPRO杯数学建模A题(第二阶段)蜘蛛网全过程文档及程序

2012年认证杯SPSSPRO杯数学建模 A题 蜘蛛网 原题再现&#xff1a; 第二阶段问题   现在我们假设一个具体的环境。假设有一个凸多边形的区域&#xff0c;蜘蛛准备在这个区域&#xff08;或其一部分&#xff09;上结一张网。   问题一&#xff1a; 在区域的边界上安置有若干…

leetcode解题思路分析(一百五十五)1352 - 1358 题

最后 K 个数的乘积 请你实现一个「数字乘积类」ProductOfNumbers&#xff0c;要求支持下述两种方法&#xff1a; add(int num) 将数字 num 添加到当前数字列表的最后面。 getProduct(int k) 返回当前数字列表中&#xff0c;最后 k 个数字的乘积。 你可以假设当前列表中始终 至少…

HarmonyOS开发实例:【手势截屏】

介绍 本篇Codelab基于手势处理和截屏能力&#xff0c;介绍了手势截屏的实现过程。样例主要包括以下功能&#xff1a; 根据下滑手势调用全屏截图功能。全屏截图&#xff0c;同时右下角有弹窗提示截图成功。根据双击手势调用区域截图功能。区域截图&#xff0c;通过调整选择框大…

GeoServer:忘记密码重置

操作步骤 1. 找到data_dir/security/usergroup/default目录下的users.xml文件&#xff0c; 2.修改password为plain:geoserver&#xff0c; 这里无论原来的密码是什么&#xff0c;改为plain:geoserver之后&#xff0c;就可以通过admin&#xff1a;geoserver默认账户密码登录了。…

室内定位中文综述阅读

1 室内高精度定位技术总结与展望 [4]柳景斌,赵智博,胡宁松等.室内高精度定位技术总结与展望[J].武汉大学学报(信息科学 版),2022,47(07):997-1008.DOI:10.13203/j.whugis20220029. 1.1.1 WiFi‐RTT定位 2016 年 12 月&#xff0c;随着新版 IEEE802.11 标准的公布&#xff0c…

开一家抖音小店都需要准备什么?今天一篇文章带你成功开店!

大家好&#xff0c;我是电商小布。 想要来玩抖店&#xff0c;第一件事情当然就是来开一家属于自己的抖店。 那么大家开抖店需要准备什么内容呢&#xff1f;这个是很多新手小伙伴不了解的东西。 所以呢今天&#xff0c;小布就从所需的材料和资金方面&#xff0c;来带大家详细的…

直播带货行业将迎来大地震

关注卢松松&#xff0c;会经常给你分享一些我的经验和观点。 为什么这么多人喊着关闭直播带货?实体经济是到底因为什么萧条的?为什么大街上冷冷清清的?是房租、虚高的价格、还是直播带货引起的? 在4月9日的国务院政策吹风会上&#xff0c;市场监管明确指出&#xff1a; …

* ./cptable in ./node_modules/xlsx-style/dist/cpexcel.js

1) 这个报错信息就是在使用xlsx-style的时候dist中cpexcel不支持, 2) 解决方案: 在自己的前端项目中找到vue.config.js,如果没有新建一个这样的文件 在文件中编写以下内容即可: module.exports { configureWebpack: { externals: { ./cptable: var cptable } } } 3) 重新 运…

选择程序员是为什么?

本章节是关于为什么会选择一名程序员的经验分享 首先&#xff0c;我为什么会选择这个方向&#xff0c;可能是因为钱多&#xff0c;学东西不就是为了赚钱嘛&#xff1f;这是一点&#xff0c;不过最让我接收这个行业的是好奇世界的新大陆&#xff0c;可以简单的说就是&#xff0c…

ubuntu系统安装python虚拟环境

一、安装python&#xff1a; 步骤1&#xff1a;在Ubuntu系统中打开终端&#xff0c;你可以使用快捷键CtrlAltT来打开终端&#xff0c;或者在应用程序菜单中找到终端。 步骤2&#xff1a;更新软件包列表&#xff0c;在终端中输入以下命令&#xff0c;更新软件包列表&#xff1…

MobX原理剖析:基于可观察状态和自动依赖追踪的响应式状态管理

我们用代码示例来说明 MobX 的核心原理。 首先,我们定义一个简单的 Store 类,其中包含一个可观察的计数器状态: import { observable, action } from mobx;class CounterStore {observable count 0;actionincrement () > {this.count;};actiondecrement () > {this.…

Linux Shell:`alias`命令

Linux Shell&#xff1a;alias命令 alias命令是Linux和Unix系统中Shell的内置命令&#xff0c;用于创建命令的简短名称&#xff0c;即别名。这些别名通常用来缩短长命令或为常用命令序列创建便捷的缩写&#xff0c;从而提高工作效率。别名在当前Shell会话中有效&#xff0c;除…

Leetcode算法训练日记 | day20

一、合并二叉树 1.题目 Leetcode&#xff1a;第 617 题 给你两棵二叉树&#xff1a; root1 和 root2 。 想象一下&#xff0c;当你将其中一棵覆盖到另一棵之上时&#xff0c;两棵树上的一些节点将会重叠&#xff08;而另一些不会&#xff09;。你需要将这两棵树合并成一棵新…

基于SSM的电影网站(有报告)。Javaee项目。ssm项目。

演示视频&#xff1a; 基于SSM的电影网站&#xff08;有报告&#xff09;。Javaee项目。ssm项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff0c;通过Spring SpringMv…

196算法之谜在 JSP 中使用内置对象 request 获取 form 表单的文本框 text 提交的数据。

(1&#xff09;编写 inputNumber . jsp &#xff0c;该页面提供一个 form 表单&#xff0c;该 form 表单提供一个文本框 text &#xff0c;用于用户输入一个正整数&#xff0c;用户在 form 表单中输入的数字&#xff0c;单击 submit 提交键将正整数提交给 huiwenNumber . jsp 页…

开源项目ChatGPT-Next-Web的容器化部署(四)-- k8s容器部署使用configmap配置

一、接着上文 本文的内容是在k8s容器中&#xff0c;如何使用configmap对.env文件进行挂载&#xff0c;实现环境的差异化配置。 二、源码结构 项目ChatGPT-Next-Web使用了.env文件来配置不同环境下的值&#xff1a; 所以&#xff0c;我们同理新增两个配置文件&#xff0c;见下…