阿卡不拉阿卡不拉
Vue3
阿卡的博客
Vue3
阿卡的博客
  • Vue3

    • 快速入门

      • 搭建工程 👌
      • 模板语法
      • 响应式基础
      • 响应式常用 API
      • 计算属性
      • 类与样式绑定
      • 条件和列表渲染
      • 事件处理
      • 表单处理
      • 生命周期
      • 侦听器
      • 组件介绍
      • Props
      • 自定义事件
      • 组件v-model
      • 插槽
      • 前端路由介绍
      • KeepAlive内置组件
      • 状态管理库
      • 组件库介绍
    • 深入本质

      • 虚拟DOM本质
      • 模板的本质
      • 组件树和虚拟DOM树
      • 数据拦截的本质
      • 响应式数据的本质
      • 响应式的本质
      • 响应式和组件渲染
      • 实现响应式系统 1
      • 实现响应式系统 2
      • 图解EFFECT
      • 实现响应式系统 3
      • 手写computed
      • 手写watch
      • 指令的本质
      • 插槽的本质
      • v-model的本质
      • setup 语法标签
      • 组件生命周期
      • keepalive 生命周期
      • keepalive的本质
      • key的本质
    • 细节补充

      • 【Vue】属性透传
      • 【Vue】依赖注入
      • 【Vue】组合式函数 👌
      • 【Vue】自定义指令
      • 【Vue】插件
      • 【Vue】Transition
      • 【Vue】TransitionGroup
      • 【Vue】Teleport
      • 【Vue】异步组件
      • 【Vue】Suspense
      • 【Router】路由模式
      • 【Router】路由零碎知识
      • 【Router】路由匹配语法
      • 【Router】路由组件传参
      • 【Router】内置组件和函数
      • 【Router】导航守卫
      • 【Router】过渡特效
      • 【Router】滚动行为
      • 【Router】动态路由
      • 【State】通信方式总结
      • 【State】Pinia 自定义插件
      • 【场景】封装树形组件
      • 【场景】自定义 ref 实现防抖
      • 【场景】懒加载
      • 【场景】虚拟列表
      • 【场景】虚拟列表优化
      • 【第三方库】VueUse
      • 【第三方库】vuedragable
      • 【第三方库】vue-drag-resize
      • 【第三方库】vue-chartjs
      • 【第三方库】vuelidate
      • 【第三方库】vue3-lazyload
      • 【场景】Websocket 聊天室
      • 【Vite】✨ 认识 Vite👌
      • 【Vite】配置文件 👌
      • 【Vite】✨ 依赖预构建 👌
      • 【Vite】构建生产版本 👌
      • 【Vite】环境变量与模式
      • 【Vite】CLI
      • 【Vite】Vite 插件
  • 笔面试

    • HTML

      • HTML 面试题汇总
      • 文档声明
      • 语义化
      • W3C 标准组织
      • SEO
      • iframe
      • 微格式
      • 替换元素
      • 页面可见性
    • CSS

      • CSS 面试题汇总
      • CSS 单位总结
      • 居中方式总结
      • 隐藏元素方式总结
      • 浮动
      • 定位总结
      • BFC
      • CSS 属性计算过程
      • CSS 层叠继承规则总结
      • @import 指令
      • CSS3 calc 函数
      • CSS3 媒体查询
      • 过渡和动画事件
      • 渐进增强和优雅降级
      • CSS3 变形
      • 渐进式渲染
      • CSS 渲染性能优化
      • 层叠上下文
      • CSS3 遮罩
    • JavaScript

      • JavaScript 面试题汇总
      • ✨ let、var、const 的区别
      • JS中的数据类型
      • 包装类型
      • 数据类型的转换
      • 运算符
      • ✨ 原型链
      • ✨ this 指向
      • ✨ 垃圾回收与内存泄漏
      • ✨ 执行栈和执行上下文
      • ✨ 作用域和作用域链
      • ✨ 闭包
      • DOM 事件的注册和移除
      • DOM 事件的传播机制
      • 阻止事件默认行为
      • 递归
      • ✨ 属性描述符
      • class 和构造函数区别
      • 浮点数精度问题
      • 严格模式
      • ✨ 函数防抖和节流
      • ✨ WeakSet 和 WeakMap
      • ✨ 深浅拷贝
      • 函数柯里化
      • Node 事件循环
      • 尺寸和位置
    • 浏览器

      • 浏览器面试题汇总
      • ✨ 浏览器的渲染流程
      • ✨ 资源提示关键词
      • 浏览器的组成部分
      • IndexedDB
      • ✨ File API
      • ✨ 浏览器缓存
      • ✨ 浏览器跨标签页通信
      • Web Worker
    • 网络

      • 网络面试题汇总
      • 五层网络模型 👌
      • 常见请求方法 👌
      • ✨cookie👌
      • 面试题
      • 加密
      • ✨JWT👌
      • ✨ 同源策略及跨域问题 👌
      • 文件上传
      • ✨ 输入 url 地址之后
      • 文件下载
      • ✨ session
      • ✨ TCP
      • ✨ CSRF 攻击
      • ✨XSS 攻击 👌
      • ✨ 网络性能优化
      • 断点续传
      • 域名和 DNS
      • SSL、TLS、HTTPS 的关系
      • ✨ HTTP 各版本差异 👌
      • HTTP 缓存协议
      • ✨ WebSocket
    • 工程化

      • CMJ 和 ESM
      • npx
      • ESLint
    • Vue2

      • Vue 面试题汇总相关
      • 组件通信方式总结
      • 虚拟 DOM
      • v-model
      • 数据响应式原理
      • diff
      • 生命周期详解
      • computed
      • 过滤器
      • 作用域插槽
      • 过度和动画
      • 优化
      • keep-alive
      • 长列表优化
      • 其他 API
      • 模式和环境变量
      • 更多配置
      • 更多命令
      • 嵌套路由
      • 路由切换动画
    • Vue3

      • ✨ Vue3 整体变化 👌
      • ✨ Vue3 响应式变化 👌
      • ✨ nextTick 实现原理 👌
      • 两道代码题 👌
      • Vue 运行机制
      • 渲染器核心功能
      • 事件绑定与更新

【Vue】组合式函数 👌

组合式函数,本质上也就是代码复用的一种方式。

  • 组件:对结构、样式、逻辑进行复用
  • 组合式函数:侧重于对 有状态 的逻辑进行复用

官方介绍

快速上手

实现一个鼠标坐标值的追踪器。

<template>
    <div>当前鼠标位置: {{ x }}, {{ y }}</div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from "vue";

const x = ref(0);
const y = ref(0);

function update(event) {
    x.value = event.pageX;
    y.value = event.pageY;
}

onMounted(() => window.addEventListener("mousemove", update));
onUnmounted(() => window.removeEventListener("mousemove", update));
</script>

<style scoped></style>

多个组件中复用这个相同的逻辑,该怎么办?

答:使用组合式函数。将包含了状态的相关逻辑,一起提取到一个单独的函数中,该函数就是组合式函数。

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.pageX;
        y.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>

相关细节

1. 组合式函数本身还可以相互嵌套

2. 和 Vue2 时期 mixin 区别

解决了 Vue2 时期 mixin 的一些问题。

  1. 不清晰的数据来源:当使用多个 minxin 的时候,实例上的数据属性来自于哪一个 mixin 不太好分辨。

  2. 命名空间冲突:如果多个 mixin 来自于不同的作者,可能会注册相同的属性名,造成命名冲突

    mixin

    const mixinA = {
        methods: {
            fetchData() {
                // fetch data logic for mixin A
                console.log("Fetching data from mixin A");
            },
        },
    };
    
    const mixinB = {
        methods: {
            fetchData() {
                // fetch data logic for mixin B
                console.log("Fetching data from mixin B");
            },
        },
    };
    
    new Vue({
        mixins: [mixinA, mixinB],
        template: `
        <div>
          <button @click="fetchData">Fetch Data</button>
        </div>
      `,
    });
    

    组合式函数:

    // useMixinA.js
    import { ref } from "vue";
    
    export function useMixinA() {
        function fetchData() {
            // fetch data logic for mixin A
            console.log("Fetching data from mixin A");
        }
    
        return { fetchData };
    }
    
    // useMixinB.js
    import { ref } from "vue";
    
    export function useMixinB() {
        function fetchData() {
            // fetch data logic for mixin B
            console.log("Fetching data from mixin B");
        }
    
        return { fetchData };
    }
    

    组件使用上面的组合式函数:

    import { defineComponent } from "vue";
    import { useMixinA } from "./useMixinA";
    import { useMixinB } from "./useMixinB";
    
    export default defineComponent({
        setup() {
            // 这里必须要给别名
            const { fetchData: fetchDataA } = useMixinA();
            const { fetchData: fetchDataB } = useMixinB();
    
            fetchDataA();
            fetchDataB();
    
            return { fetchDataA, fetchDataB };
        },
        template: `
        <div>
          <button @click="fetchDataA">Fetch Data A</button>
          <button @click="fetchDataB">Fetch Data B</button>
        </div>
      `,
    });
    
  3. 隐式的跨 mixin 交流

    mixin

    export const mixinA = {
        data() {
            return {
                sharedValue: "some value",
            };
        },
    };
    
    export const minxinB = {
        computed: {
            dValue() {
                // 和 mixinA 具有隐式的交流
                // 因为最终 mixin 的内容会被合并到组件实例上面,因此在 mixinB 里面可以直接访问 mixinA 的数据
                return this.sharedValue + "xxxx";
            },
        },
    };
    

    组合式函数:交流就是显式的

    import { ref } from "vue";
    
    export function useMixinA() {
        const sharedValue = ref("some value");
        return { sharedValue };
    }
    
    import { computed } from "vue";
    
    export function useMixinB(sharedValue) {
        const derivedValue = computed(() => sharedValue.value + " extended");
        return { derivedValue };
    }
    
    <template>
        <div>{{ derivedValue }}</div>
    </template>
    
    <script>
    import { defineComponent } from "vue";
    import { useMixinA } from "./useMixinA";
    import { useMixinB } from "./useMixinB";
    
    export default defineComponent({
        setup() {
            const { sharedValue } = useMixinA();
    
            // 两个组合式函数的交流是显式的
            const { derivedValue } = useMixinB(sharedValue);
    
            return { derivedValue };
        },
    });
    </script>
    

3 异步状态

根据异步请求的情况显示不同的信息:

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

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

如何复用这段逻辑?仍然是提取成一个组合式函数。

如下:

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

现在重构上面的组件:

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

<script setup>
import { useFetch } from "./hooks/useFetch";
const { data, error } = useFetch("xxxx");
</script>

这里为了更加灵活,我们想要传递一个响应式数据:

const url = ref("first-url");
// 请求数据
const { data, error } = useFetch(url);
// 修改 url 的值后重新请求数据
url.value = "new-url";

此时我们就需要重构上面的组合式函数:

import { ref, watchEffect, toValue } from "vue";
export function useFetch(url) {
    const data = ref(null);
    const error = ref(null);
    const fetchData = () => {
        // 每次执行 fetchData 的时候,重置 data 和 error 的值
        data.value = null;
        error.value = null;
        fetch(toValue(url)) // 调用toValue将ref和getter转换为值
            .then((res) => res.json())
            .then((json) => (data.value = json))
            .catch((err) => (error.value = err));
    };
    watchEffect(() => {
        fetchData(); // url更新的时候会自动调用
    });
    return { data, error };
}

约定和最佳实践

1. 命名:组合式函数约定用驼峰命名法命名,并以“use”作为开头。例如前面的 useMouse、useEvent.

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

3. 返回值

组合式函数中推荐返回一个普通对象,该对象的每一项是 ref 数据,这样可以保证在解构的时候仍然能够保持其响应式的特性:

// 组合式函数
export function useMouse() {
    const x = ref(0);
    const y = ref(0);

    // ...

    return { x, y };
}
import { useMouse } from "./hooks/useMouse";
// 可以解构
const { x, y } = useMouse();

如果希望以对象属性的形式来使用组合式函数中返回的状态,可以将返回的对象用 reactive 再包装一次即可:

import { useMouse } from "./hooks/useMouse";
const mouse = reactive(useMouse());

4. 副作用

在组合式函数中可以执行副作用,例如添加 DOM 事件监听器或者请求数据。但是请确保在 onUnmounted 里面清理副作用。

例如在一个组合式函数设置了一个事件监听器,那么就需要在 onUnmounted 的时候移除这个事件监听器。

export function useMouse() {
    // ...

    onMounted(() => window.addEventListener("mousemove", update));
    onUnmounted(() => window.removeEventListener("mousemove", update));

    // ...
}

也可以像前面 useEvent 一样,专门定义一个组合式函数来处理副作用:

import { onMounted, onUnmounted } from "vue";

export function useEventListener(target, event, callback) {
    // 专门处理副作用的组合式函数
    onMounted(() => target.addEventListener(event, callback));
    onUnmounted(() => target.removeEventListener(event, callback));
}

5. 使用限制

  1. 只能在 <script setup>或 setup( ) 钩子中调用:确保在组件实例被创建时,所有的组合式函数都被正确初始化。特别如果你使用的是选项式 API,那么需要在 setup 方法中调用组合式函数,并且返回,这样才能暴露给 this 及其模板使用

    import { useMouse } from "./mouse.js";
    import { useFetch } from "./fetch.js";
    
    export default {
        setup() {
            // 因为组合式函数会返回一些状态
            // 为了后面通过 this 能够正确访问到这些数据状态
            // 必须在 setup 的时候调用组合式函数
            const { x, y } = useMouse();
            const { data, error } = useFetch("...");
            return { x, y, data, error };
        },
        mounted() {
            // setup() 暴露的属性可以在通过 `this` 访问到
            console.log(this.x);
        },
        // ...其他选项
    };
    
  2. 只能被同步调用:Vue 需要在组件实例创建的过程中收集所有的响应式状态和副作用,这个过程是同步的。如果组合式函数被异步调用,可能会导致在组件实例还未完全初始化时,尝试访问未定义的实例数据,从而引发错误以及错过建立响应式依赖。

  3. 可以在像 onMounted 生命周期钩子中调用:在某些情况下,可以在如 onMounted 生命周期钩子中调用组合式函数。这些生命周期钩子也是同步执行的,并且在组件实例已经被初始化后调用,因此可以安全地使用组合式函数。

最近更新:: 2025/7/9 07:29
Contributors: AK
Prev
【Vue】依赖注入
Next
【Vue】自定义指令