前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版

Vue3之Composition API详解 从setup到组合式函数

首页2021-02-17 20:12:12Front-End
VueVue3Composition API

一个业务组件写到八百行,data 里堆了二十个字段,methods 里躺着三十个函数,你想改「购物车优惠券」这一块的逻辑,得在文件里上下横跳七八次,因为它的状态在第 30 行,计算在第 210 行,方法在第 480 行,监听在第 700 行。这不是代码写得烂,是选项式 API 按「选项类型」而不是按「业务关注点」组织代码的必然结果。组合式 API 就是冲着这件事来的。这篇把 setup、ref、reactive、toRefs、computed、watch、watchEffect、生命周期钩子和 provide / inject 逐个拆开讲,重点不在 API 签名,而在什么时候用哪个、边界在哪。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • setup 的执行时机、两个参数,以及它为什么没有 this
  • ref 和 reactive 到底该用哪个,reactive 的三个坑
  • toRefs 怎么让解构不丢响应性,这是组合式函数的关键一环
  • computed 和 readonly 的典型用法
  • watchEffect 自动收集依赖,watch 手动指定源,两者的四点区别
  • 组合式 API 的生命周期钩子改名规律和注册时机
  • provide / inject 跨层级传值,以及怎么保住响应性
  • 把逻辑抽成组合式函数,这才是组合式 API 存在的理由
  • 关于 script setup,2021 年它还是实验特性,现在已经是推荐写法

Composition API 也叫组合式 API,是 Vue 3.x 的新特性。

官方文档里的说法是这样的:通过创建 Vue 组件,我们可以将接口的可重复部分及其功能提取到可重用的代码段中,仅此一项就能让应用在可维护性和灵活性上走得更远。但经验证明光靠这一点可能不够,尤其是当你的应用变得非常大的时候,比如上百个组件的规模。在处理这么大的应用时,共享和重用代码变得尤为重要。

通俗的讲:没有 Composition API 之前,Vue 相关业务的代码需要配置到 option 的特定区域,中小型项目没有问题,但在大型项目中会导致后期维护复杂,同时代码可复用性不高。Vue 3.x 中的 composition-api 就是为了解决这个问题而生的。

compositon api 提供了以下几个函数:

  • setup
  • ref
  • reactive
  • watchEffect
  • watch
  • computed
  • toRefs
  • 生命周期的 hooks

# 一、setup组件选项

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

这里有个提示要记住:由于在执行 setup 时尚未创建组件实例,因此在 setup 选项中没有 this。所以除了 props 之外,你无法访问组件中声明的任何属性,本地状态、计算属性、方法一个都拿不到。

使用 setup 函数时,它会接受两个参数:

  1. props
  2. context

下面分别看这两个参数怎么用。

# 1. Props

setup 函数中的第一个参数是 props。正如在一个标准组件中所期望的那样,setup 函数中的 props 是响应式的,当传入新的 prop 时,它会被更新。

// MyBook.vue

export default {
  props: {
    title: String
  },
  setup(props) {
    console.log(props.title)
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

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

这个坑很多人踩过。const { title } = props 之后,title 就是一次性取出来的普通字符串,父组件再怎么改,你手里这个变量都不会动。如果需要解构 prop,可以通过使用 setup 函数中的 toRefs 来安全地完成此操作:

// MyBook.vue

import { toRefs } from 'vue'

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

	console.log(title.value)
}
@前端进阶之旅: 代码已经复制到剪贴板

这里有个额外的坑:如果某个 prop 是可选的、父组件没传,toRefs 在那个 key 上拿不到东西,这种情况用 toRef(props, 'title') 更稳,它对不存在的 key 也能建立引用。

顺便说个时效性的事。新版本 Vue 对 defineProps 的解构做了编译期处理,让解构后的变量也能保持响应性,不再需要处处包 toRefs。具体从哪个版本开始默认开启、有哪些限制,以官方文档为准。

# 2. 上下文

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

// MyBook.vue

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

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

    // 触发事件 (方法)
    console.log(context.emit)
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

context 是一个普通的 JavaScript 对象,它不是响应式的,所以你可以安全地对 context 使用 ES6 解构:

// MyBook.vue
export default {
  setup(props, { attrs, slots, emit }) {
    ...
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

不过 attrs 和 slots 是有状态的对象,它们总是会随组件本身的更新而更新。所以你应该避免对它们再往下解构,始终以 attrs.x 或 slots.x 的方式引用 property。请注意,与 props 不同,attrs 和 slots 是非响应式的。如果你打算根据 attrs 或 slots 做副作用,那应该在 onUpdated 生命周期钩子中执行。

这三个之外,context 上还有一个 expose,用来显式声明这个组件对外暴露哪些属性和方法,父组件通过模板 ref 拿到的就是你 expose 出去的那些。

# 3. setup 组件的 property

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

  • props
  • attrs
  • slots
  • emit

也就是说,你将无法访问以下组件选项:

  • data
  • computed
  • methods

这个限制不是故意为难人,它是执行顺序决定的。setup 跑在 beforeCreate 和 created 之间,那时候 data() 还没被调用,computed 还没被求值,自然什么都没有。

# 4. ref reactive 以及 setup 结合模板使用

在看 setup 结合模板使用之前,我们首先得知道 ref 和 reactive 方法。

如果 setup 返回一个对象,则可以在模板中绑定对象中的属性和方法。但要定义响应式数据的时候,需要用 ref 或 reactive 方法。

# 错误写法

下面这段是最典型的入门错误,msg 是个普通字符串,点按钮之后 alert 会弹,值也确实改了,但页面纹丝不动:

<template>
{{msg}}
<br>

<button @click="updateMsg">改变setup中的msg</button>

<br>
</template>

<script>
export default {
    data() {
        return {

        }
    },
    setup() {
        let msg = "这是setup中的msg";
        let updateMsg = () => {
            alert("触发方法")
            msg = "改变后的值"
        }
        return {
            msg,
            updateMsg
        }
    },

}
</script>
@前端进阶之旅: 代码已经复制到剪贴板

为什么改了值页面不更新呢?因为 return 出去的那一刻,msg 的值就被拷贝进返回对象了,之后你改的是 setup 作用域里那个局部变量,和模板拿到的那份已经没有关系。Vue 也没法在这中间插进去,一个普通字符串上没有任何可以被劫持的入口。这块的底层机制我在 Vue响应式原理模拟 里从 Object.defineProperty 和 Proxy 讲起手写了一遍。

# 正确写法一 ref

ref 用来定义响应式的字符串、数值、数组、Bool 类型:

import {  
    ref
} from 'vue'
@前端进阶之旅: 代码已经复制到剪贴板

模板部分照常写,注意模板里用 msg 不用带 .value:

<template>
{{msg}}
<br>
<br>
<button @click="updateMsg">改变setup中的msg</button>
<br>
<br>
<ul>
    <li v-for="(item,index) in list" :key="index">
        {{item}}
    </li>
</ul>

<br>
</template>
@前端进阶之旅: 代码已经复制到剪贴板

脚本里定义和修改都要走 .value:

<script>
import {

    ref
} from 'vue'

export default {
    data() {
        return {

        }
    },
    setup() {
        let msg = ref("这是setup中的msg");

        let list = ref(["马总", "李总", "刘总"])

        let updateMsg = () => {
            alert("触发方法");
            msg.value = "改变后的值"
        }
        return {
            msg,
            list,
            updateMsg
        }
    },

}
</script>
@前端进阶之旅: 代码已经复制到剪贴板

ref 的原理就是包一层。它返回一个带 value 属性的对象,读写都过这个属性的 getter/setter,这样 Vue 就有地方做依赖收集和触发更新了。模板里之所以不用写 .value,是因为渲染的时候 Vue 会对顶层的 ref 自动解包。这个自动解包只在模板的顶层属性上生效,把 ref 塞进数组或者 Map 里再取出来,还是得自己写 .value。

# 正确写法二 reactive

reactive 用来定义响应式的对象:

import {
    reactive   
} from 'vue'
@前端进阶之旅: 代码已经复制到剪贴板

模板里 reactive 定义的对象直接按属性路径访问:

<template>
{{msg}}
<br>
<br>
<button @click="updateMsg">改变setup中的msg</button>
<br>
<br>
<ul>
    <li v-for="(item,index) in list" :key="index">
        {{item}}
    </li>
</ul>
<br>
{{setupData.title}}
<br>
<button @click="updateTitle">更新setup中的title</button>
<br>
<br>
</template>
@前端进阶之旅: 代码已经复制到剪贴板

脚本里 reactive 返回的是一个 Proxy 代理对象,改属性不需要 .value:

fe
  • 一、setup组件选项
    • 1. Props
    • 2. 上下文
    • 3. setup 组件的 property
    • 4. ref reactive 以及 setup 结合模板使用
      • 错误写法
      • 正确写法一 ref
      • 正确写法二 reactive
      • 那到底该用哪个
    • 5. 使用 this
  • 二、toRefs 解构响应式对象数据
  • 三、computed 计算属性
  • 四、readonly 深层的只读代理
  • 五、watchEffect 自动收集依赖
  • 六、watch 与 watchEffect 的区别
  • 七、组合式api生命周期钩子
  • 八、provide 和 inject 跨层级传值
    • 1. 非组合式 api 中的写法
    • 2. 组合式 api 中的写法
    • 3. provide inject 的响应性
  • 九、把逻辑抽成组合式函数
  • 十、关于 script setup
  • 总结
  • 参考

← Vue组件化实践详解 九种组件通信方式的取舍Vue3基础小结 从建项目到Teleport的完整上手路径 →