企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
[TOC] > [@vue/composition-api](https://github.com/vuejs/composition-api) # 为什么使用组合API(Composition API) 在使用 Vue 2 时可能遇到三个限制: * 随着您的组件变得更大的可读性变得越来越困难。 * 当前的代码重用模式都具有缺点。 * Vue 2 提供了有限的 TypeScript 支 持。 我将详细讨论前两个问题,这样新 API 解决了什么问题就很明显了。 # 大型组件是很难阅读和维护的 首先我们来看一个查找商品的组件: ![](https://img.kancloud.cn/24/be/24be9388b81dedffe6eec244bac67fd3_673x186.png) 其源代码如下: ```js <script> export default { data() { return { // ...search data... } }, methods: { // ...search methods... } } </script> ``` 当我们想添加其他一些数据或操作时,比如:添加一个搜索结果排序的操作!那么我们的代码就会是这种形式: ```js <script> export default { data() { return { // ...search data... // ...sort data... } }, methods: { // ...search methods... // ...sort methods... } } </script> ``` 目前该代码量还算好,如果我们还想要添加搜索过滤、结果分页等功能时。我们就需要在该组件的选项(components、props、data、computed、methods 和 lifecycle methods)之间进行拆分,来填充功能代码。如果我们使用颜色(下图)将其可视化,您可以看到功能代码将如何分解,这些使组件更加难以阅读和解析哪些代码与哪些功能相匹配: ![](https://img.kancloud.cn/95/0c/950c7208cd75af9d50b003ecfa6e30e4_618x490.png) 查看上面➡右图,如果我们把某个功能的相关代码都放在一起,就会️更加容易阅读理解,也就更方便维护。查看我们原本的示例,如果通过 Composition API 来组织我们的代码。那么情况就会像下面这样: ![](https://img.kancloud.cn/86/69/86691ece4f36165acddcb89d45b4ef93_344x474.png) Vue 3的 Composition API 的 `setup()`是新的语法。这种新语法完全是可选的,Vue 组件原本的标准方法仍然是完全有效的。 当您使用 Composition API 根据功能来组织组件时,我们会通过构成功能来对功能分组,然后会在`setup`方法中被调用。 ![](https://img.kancloud.cn/42/52/4252ff309aff530c556c09553ef7c22c_904x452.png) 现在,可以使用逻辑关注点(也称为“功能”)来组织我们的组件。 但是,这并不意味着我们的用户界面将由更少的组件组成。 我们仍然需要使用良好的组件设计模式来组织应用程序: ![](https://img.kancloud.cn/5c/3a/5c3a058578cbcea620eced2461271fbf_898x294.png) 接下来我们讨论 Vue 2的第二个限制! # 没有特别好的方式来重用组件的逻辑代码 在 Vue 2 中有 3 种好的解决方案可以跨组件重用代码,但是每种解决方案都有其局限性。 让我们逐一介绍示例。 首先,有 Mixins。 ## Mixins ![](https://img.kancloud.cn/c0/24/c02472f00e86a17465f927295a1759fb_898x514.png) 1. 优点: * 能按照功能组织代码 2. 缺点: * 它们容易发生冲突,并且您最终可能会遇到属性名称冲突。 * 如果 Mixins 存在相互作用,会不清楚它们是如何相互作用。 * 如果要在其他组件之间使用 Mixin,需要配置 Mixin,不方便重用。 ## Mixin Factories 我们看一下 **Mixin Factories**,这是返回自定义版本的 Mixin 的函数。 ![](https://img.kancloud.cn/65/17/651783f0961cd69f194ec13079169384_758x746.png) 如上图所示,Mixin 工厂函数 允许我们通过传入配置选项来自定义 Mixins。 现在,我们可以配置此代码以在多个组件中使用。 1. 优点: * 我们可以配置代码,因此可以轻松重用。 * 关于 Mixins 如何进行交互,具有了更明确的关系。 2. 缺点: * 命名间隔需要严格的惯例和纪律。 * 仍然有隐式的属性添加,这意味着我们必须查看 Mixin 内部以找出它公开的属性。 * 运行时没有实例访问权限,因此无法动态生成 Mixin 工厂。 ## 作用域插槽 幸运的是,还有一种解决方案,作用域插槽(Scoped Slots): ![Scoped Slots](https://img.kancloud.cn/3a/89/3a894ad59f6dcfb6224b7dab2786bf36_892x514.png) 1. 优点: * 解决了 Mixins 的几乎所有缺点。 2. 缺点: * 您的配置最终出现在模板中,理想情况下,模板应仅包含我们要呈现的内容。 * 它们会增加模板的缩进量,从而降低可读性。 * 公开的属性仅在模板中可用。 * 由于我们使用的是3个组件而不是1个,因此性能有所降低。 如您所见,每种解决方案都有其局限性。 Vue 3的 Composition API 为我们提供了提取可重用代码的第四种方式,该方式可能类似于: ![Composition API](https://img.kancloud.cn/7d/60/7d60dde85e051c61bae28a2797b6cc17_736x652.png) 现在,我们将使用组合 API 内部的函数来创建组件,这些函数将**在需要进行配置的`setup`方法中导入并使用**。 1. 优点: * 编写的代码更少,因此将功能从组件中提取到函数中变得更加容易。 * 它建立在我们已有的技能上,而且我们早已熟悉了函数。 * 它比 Mixins 和作用域插槽更具灵活性,因为它们只是函数。 * 智能提示、自动完成和 typings 在很多代码编辑器中已经可以使用了。 2. 缺点: * 需要学习新的低级API来定义合成功能。 * 现在除了组件的标准语法外,还多了这种编写组件的方法。 希望您现在清楚了组合 API 的由来。 在下一课中,我将深入探讨组合组件的新语法。 # `setup` 和 响应式引用 首先,我们想弄清楚什么时候来使用它,然后我们将学习`setup`函数 和`Reactive References`以及`refs`。 > 免责声明:如果你还不明白的话(If you haven’t caught on yet),Composition API 纯粹是附加的,也没有弃用什么。您可以像在 Vue 2 进行编码一样对 Vue 3 进行同样编码。 ## 什么时候去使用 组合 API? 下面任何一种情况都可以: 1. 您需要最佳的 TypeScript 支持。 2. 组件太大了,需要根据功能划分来组织! 3. 需要在其他组件中复用代码。 4. 你和你的团队更喜欢新的组合 API。 > 免责声明:下面的示例很简单,其实是不需要使用 Composition API 的!这里仅仅是为了方便学习而已! 先从普通 Vue 2 API 编写非常简单的组件开始,该组件在 Vue 3 中也是有效的。 ```js <template> <div>Capacity: {{ capacity }}</div> </template> <script> export default { data() { return { capacity: 3 }; } }; </script> ``` 这里有个`capacity`属性,它是响应式的。Vue 会获取组件选项中`data`属性所返回的每个属性,并使他们成为响应式的属性!当组件中的响应式属性被改变时,组件就会被重新渲染。 ## 使用`Setup`函数 我们使用 Vue 3 的组合 API 中 的`setup`函数: ``` <template> <div>Capacity: {{ capacity }}</div> </template> <script> export default { setup() { // more code to write } }; </script> ``` 这个`setup`函数会在计算以下的任何选项之前,被执行: * Components * Props * Data * Methods * Computed Properties * Lifecycle methods 和其他组件选项不同,`setup`函数中,不能访问`this`!所以为了获得对组件属性的访问,方便操作,`setup`函数有两个可选的参数。第一个参数是响应式的,而且能被监听: ``` import { watch } from "vue"; export default { props: { name: String }, setup(props) { watch(() => { console.log(props.name); }); } }; ``` 第二个参数是 `context`,可以获取一堆有用的数据: ``` setup(props, context) { context.attrs; context.slots; context.parent; context.root; context.emit; } ``` 回到我们的例子,我们现在需要一个响应式的引用。 ## 响应式引用 ``` <template> <div>Capacity: {{ capacity }}</div> </template> <script> import { ref } from "vue"; export default { setup() { const capacity = ref(3); // additional code to write } }; </script> ``` 上例中的`const capacity = ref(3);`创建了一个“响应式引用”。基本类型`3`被包装到了一个对象中,方便我们跟踪它的变化。先前的`data`中的`capacity`属性已经被包装在了一个对象中。 > 旁白:组合 API 允许我们声明不与组件关联的响应式基本类型,我们是这样做的。 最后一步,我们需要明确的的返回包含需要被渲染的数据对象。 ``` <template> <div>Capacity: {{ capacity }}</div> </template> <script> import { ref } from "vue"; export default { setup() { const capacity = ref(3); return { capacity }; } }; </script> ``` 这个返回的对象是我们在`renderContext`中公开需要访问哪些数据的方式。 像这样明确表示有点冗长,但是故意为之的。 它有助于我们长期维护,因为我们可以控制暴露给模板的内容,并跟踪定义模板属性的位置。 ## 将 Vue 3 与 Vue 2 一起使用 可以通过使用`@vue/composition-api`插件,将 Vue 3 Composition API 与 Vue 2一起使用。在 Vue 2 应用上安装并配置它之后,您将使用上面的语法更改: ``` import { ref } from "vue"; ``` 改为: ~~~ import { ref } from "@vue/composition-api"; ~~~ 接下来,我们将学习如何使用这种新语法编写组件。 # 方法 我们已经学会了如何创建响应式引用,那么组合组件的下一个构建块就是创建方法。 这是我们当前的代码: ``` <template> <div>Capacity: {{ capacity }}</div> </template> <script> import { ref } from "vue"; export default { setup() { const capacity = ref(3); return { capacity }; } }; </script> ``` 如果我们要添加一个允许我们从按钮增加`capacity`的方法,在常规组件语法中可以这样写: ``` methods: { increase_capacity() { this.capacity++; } } ``` 但是,如何使用新的 Vue 3 Composition API 呢? 首先要在`setup`方法中定义一个函数,返回该方法使组件可以访问它,然后在按钮内使用它: ``` <template> <div> <p>Capacity: {{ capacity }}</p> <button @click="increaseCapacity()">Increase Capacity</button> </div> </template> <script> import { ref } from "vue"; export default { setup() { const capacity = ref(3); function increaseCapacity() { // <--- Our new function // TBD } return { capacity, increaseCapacity }; } }; </script> ``` 当我们需要方法时,只需使用 Composition API 将它们创建为函数即可。但是,我们如何从`setup`方法内部增加`capacity`呢? 您可能会猜测: ``` function increaseCapacity() { capacity++; } ``` 但是,`capacity`是一个响应式引用,一个包装了我们整数的对象。 递增操作一个对象肯定是错误的。 在本例中,我们需要递增的是 响应式引用 封装的内部整数`value`。 我们可以通过访问`capacity.value`做到这一点: ``` function increaseCapacity() { capacity.value++; } ``` 现在就可以了。 但是,如果查看模板,您会注意到在打印`capacity`时: ``` <p>Capacity: {{ capacity }}</p> ``` 我们不必写`capacity.value`,您可能想知道为什么。 事实证明,当 Vue 在模板中找到`ref`时,它会自动公开内部值,因此您无需在模板内调用`.value`。 # 计算属性 学习使用新的 Composition API 语法创建计算属性。 首先,需要将其添加到示例应用程序中,这样就有了一个参加活动的人员列表。 ```js <template> <div> <p>Capacity: {{ capacity }}</p> <button @click="increaseCapacity()">Increase Capacity</button> <h2>Attending</h2> <ul> <li v-for="(name, index) in attending" :key="index"> {{ name }} </li> </ul> </div> </template> <script> import { ref } from "vue"; export default { setup() { const capacity = ref(4); const attending = ref(["Tim", "Bob", "Joe"]); // <--- New Array function increaseCapacity() { capacity.value++; } return { capacity, attending, increaseCapacity }; } }; </script> ``` 现在我们有了一个 attending(参与者)的新数组,网页展示如下: ![](https://img.kancloud.cn/62/e5/62e5c05653f2eeff1b20e83b059c9977_396x582.png) 要创建对计算属性的需求,让我们更改在模板中打印容量的方式: ``` <template> <div> <p>Spaces Left: {{ spacesLeft }} out of {{ capacity }}</p> ... ``` 注意上面的`spacesLeft`,它将根据容量减去参加人数来显示事件中剩余的空间数量。 使用常规组件语法创建计算属性,它会是这样: ``` computed: { spacesLeft() { return this.capacity - this.attending.length; } } ``` 使用新的 Composition API 创建呢? 它会像这样: ``` <template> <div> <p>Spaces Left: {{ spacesLeft }} out of {{ capacity }}</p> <h2>Attending</h2> <ul> <li v-for="(name, index) in attending" :key="index"> {{ name }} </li> </ul> <button @click="increaseCapacity()">Increase Capacity</button> </div> </template> <script> import { ref, computed } from "vue"; export default { setup() { const capacity = ref(4); const attending = ref(["Tim", "Bob", "Joe"]); const spacesLeft = computed(() => { // <------- return capacity.value - attending.value.length; }); function increaseCapacity() { capacity.value++; } return { capacity, attending, spacesLeft, increaseCapacity }; } }; </script> ``` 如上面的代码中看到的那样,先从Vue API导入了`computed`,然后使用它,传入一个匿名函数并将其设置为等于一个称为`spacesLeft`的常量。 然后,从`setup`函数将其返回到对象中,使得模板可以访问它。 ## 定义可更改的计算属性 `computed`可传入`get`和`set`,用于定义可更改的计算属性。 基本示例如下所示,与 Vue 2.x 类似的,可以定义可更改的计算属性。 ``` const count = ref(1); const plusOne = computed({ get: () => count.value + 1, set: val => { count.value = val - 1 } }); plusOne.value = 1; console.log(count.value); // 0 ``` # 响应式语法 到目前为止,我们一直在使用响应式引用将 JavaScript 基本数据包装装在对象中以使其具有响应式。 但是,还有另一种方法可以将这些基本数据包装在对象中。 就是使用`reactive`语法。 看下图,左侧是使用响应式引用的示例,右边使用了替代的`reactive`语法。 ![`reactive`语法](https://img.kancloud.cn/a1/09/a109a1e7e1cd8a855d320f5a83cf91a1_854x321.png) 如上右图所示,我们创建了一个新的`event`常量,该常量接受一个普通的 JavaScript 对象并返回一个响应式对象。 这与在常规组件语法中使用`data`选项很相似,在常规组件语法中,我们还会发送一个对象。但是,如上面看到的,也可以将计算的属性发送到该对象中。 还应该注意,使用这种语法时,在访问属性时不再需要编写`.value`。 这是因为我们只是访问`event`对象上的对象属性。 您还应该注意,我们在`setup`函数的末尾返回了整个`event`对象。 请注意,这两种语法都是完全有效的,不认为是“最佳实践”。 为了使代码正常工作,我们需要按以下方式更新模板代码: ``` <p>Spaces Left: {{ event.spacesLeft }} out of {{ event.capacity }}</p> <h2>Attending</h2> <ul> <li v-for="(name, index) in event.attending" :key="index"> {{ name }} </li> </ul> <button @click="increaseCapacity()">Increase Capacity</button> ``` 注意 我们现在是如何通过`event.`来访问属性的。 # 解构? 当我第一次看到以下代码时: ``` return { event, increaseCapacity } ``` 我在想是否可以通过其他方式来解构`event`对象,以便在模板中不必总是编写`event.` ? 我更倾向于这样书写模板: ``` <p>Spaces Left: {{ spacesLeft }} out of {{ capacity }}</p> <h2>Attending</h2> <ul> <li v-for="(name, index) in attending" :key="index"> {{ name }} </li> </ul> <button @click="increaseCapacity()">Increase Capacity</button> ``` 但是我如何解构`event`呢?我尝试了下面两种方式,都失败了: ``` return { ...event, increaseCapacity }; return { event.capacity, event.attending, event.spacesLeft, increaseCapacity }; ``` 这些都不起作用,因为拆分该对象将删除其响应式特性。 为了使这项工作有效,我们需要能够将此对象拆分为**响应式引用**,以便能够保持它的响应式特性。 ## 介绍 `toRefs` 幸运的是,可以使用`toRefs`方法。 此方法将响应式对象转换为普通对象,其中每个属性都是指向原始对象上属性的响应式引用。 这是我们使用此方法完成的代码: ``` import { reactive, computed, toRefs } from "vue"; export default { setup() { const event = reactive({ capacity: 4, attending: ["Tim", "Bob", "Joe"], spacesLeft: computed(() => { return event.capacity - event.attending.length; }) }); function increaseCapacity() { event.capacity++; } return { ...toRefs(event), increaseCapacity }; } }; ``` 请注意,先导入`toRefs`,然后在`return`语句中使用它,然后解构该对象。 看起来这很棒吧! ## 旁白 组合 API 还提供`isRef`,用于检查一个对象是否是`ref`对象: ~~~ const unwrapped = isRef(foo) ? foo.value : foo; ~~~ 这里稍微提一下,如果代码不需要同时在返回值中返回递增容量的函数,那么可以简单地编写为: ``` return toRefs(event); ``` 因为`setup`方法希望我们返回的是一个对象,而`toRefs`返回的也正是一个对象。(这肯定是一开始代码设计者设计好的👌) # 模块化 我们可能会使用组合 API 的两个原因是**按功能组织组件并在其他组件之间重用我们的代码**。 到目前为止,我们已经对代码示例进行了深入探讨,所以现在就开始做吧。 这是我们当前的代码,请注意,我已改回使用**响应式引用**,该语法的内容对我来说似乎更干净。 ``` <template> ... </template> <script> import { ref, computed } from "vue"; export default { setup() { const capacity = ref(4); const attending = ref(["Tim", "Bob", "Joe"]); const spacesLeft = computed(() => { return capacity.value - attending.value.length; }); function increaseCapacity() { capacity.value++; } return { capacity, attending, spacesLeft, increaseCapacity }; } }; </script> ``` ## 将其提取到组合函数中 ``` <template> ... </template> <script> import { ref, computed } from "vue"; export default { setup() { return useEventSpace(); // <--- Notice I've just extracted a function } }; function useEventSpace() { const capacity = ref(4); const attending = ref(["Tim", "Bob", "Joe"]); const spacesLeft = computed(() => { return capacity.value - attending.value.length; }); function increaseCapacity() { capacity.value++; } return { capacity, attending, spacesLeft, increaseCapacity }; } </script> ``` 就是将所有代码移到一个函数中,该函数不在`export default {}`的范围内了。`setup()`方法现在成为我将组合函数绑定在一起的地方。 ## 提取到文件以重用代码 如果`useEventSpacep()`是想在多个组件中使用的一段代码,那么我要做的就是将该函数提取到文件中,并使用导出默认值: 文件位置:**use/event-space.vue**: ``` import { ref, computed } from "vue"; export default function useEventSpace() { const capacity = ref(4); const attending = ref(["Tim", "Bob", "Joe"]); const spacesLeft = computed(() => { return capacity.value - attending.value.length; }); function increaseCapacity() { capacity.value++; } return { capacity, attending, spacesLeft, increaseCapacity }; } ``` 我使用了名为`use`的文件夹来专门保存我的组合函数文件。当然你可以按你自己的规范命名。 `composables`或`hooks`也是毕竟好的名字。 现在,我的组件代码只需要导入此组合函数并使用它。 ``` <template> ... </template> <script> import useEventSpace from "@/use/event-space"; export default { setup() { return useEventSpace(); } }; </script> ``` ## 添加另一个组合函数 如果我们还有另一个组合函数(可能在`use/event-mapping.js`中)来映射我们的事件,并且我们想在这里使用它,我们可以这样写: ``` <template> ... </template> <script> import useEventSpace from "@/use/event-space"; import useMapping from "@/use/mapping"; export default { setup() { return { ...useEventSpace(), ...useMapping() } } }; </script> ``` 如您所见,在各个组件之间共享组合函数非常简单。 实际上,我可能会共享一些要发送给这些函数的数据,例如使用Vuex 从 API取得的事件数据。 # 生命周期钩子 您可能对Vue生命周期挂钩很熟悉,它使我们能够在组件达到执行中的特定状态时运行代码。 让我们回顾一下典型的生命周期钩子: * **beforeCreate-** 在初始化实例之后,在处理组件选项之前调用。 * **created -** 在创建实例之后调用。 * **beforeMount -** 在挂载 DOM 之前立即i调用。 * **mounted -** 在挂载实例时调用 (浏览器已经完成更新) * **beforeUpdate -** 在响应式数据发生更改时,重新渲染 DOM 之前调用。 * **updated -** 当响应式数据发生更改并且 DOM 重新渲染后调用。 * **beforeDestroy -** 在 Vue 实例正好被销毁之前调用。 * **destroyed -毁灭** 在 Vue 实例被销毁后调用。 有两种新的 Vue 2 生命周期方法,你可能不熟悉: * **activated -** 用于当内部的一个组件被打开时。 * **deactivated -** 用于当内部的一个组件被关闭时。 * **errorCaptured -** 当捕获来自任何子代组件的错误时调用。 更详细的解释请查 [生命周期钩子](https://vuejs.org/v2/api/#Options-Lifecycle-Hooks)上的 API 文档。 ## 在 Vue 3 中的卸载 在 Vue 3 中,`beforeDestroy()`也可以编写为`beforeUnmount()`,而`destroy()`可以编写为`unmount()`。 当我向 Evan You 询问这些更改时,他提到这只是更好的命名约定,因为 Vue 会挂载和卸载组件。 ## 组合 API 的生命周期方法 在 Vue 3 的 Composition API 中,我们可以通过在`setup()`中创建回调,添加到生命周期方法名称中: ``` import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted, onActivated, onDeactivated, onErrorCaptured } from "vue"; export default { setup() { onBeforeMount(() => { console.log("Before Mount!"); }); onMounted(() => { console.log("Mounted!"); }); onBeforeUpdate(() => { console.log("Before Update!"); }); onUpdated(() => { console.log("Updated!"); }); onBeforeUnmount(() => { console.log("Before Unmount!"); }); onUnmounted(() => { console.log("Unmounted!"); }); onActivated(() => { console.log("Activated!"); }); onDeactivated(() => { console.log("Deactivated!"); }); onErrorCaptured(() => { console.log("Error Captured!"); }); } }; ``` 您可能会注意到缺少两个钩子。 [@vue/composition-api](https://github.com/vuejs/composition-api) 删除了 `onBeforeCreate` 和 `onCreated`。因为`setup` 总是会在创建组件实例时被调用,`setup()`执行之前会立即调用`beforeCreate()`,而在`setup()`之后会立即调用`created()`,因此只使用`setup`即可。 ## 两种新的 Vue 3 生命周期方法 Vue 3 中还有另外两个观察者。Vue 2 Composition API 插件尚未实现这些观察者(在我撰写本文时),因此如果不使用 Vue 3 就无法使用它们。 * **onRenderTracked -** 在渲染期间,当响应式依赖在呈现函数中第一次被访问时调用。现在将跟踪此依赖项。这有助于查看正在跟踪哪些依赖项,以便进行调试。 * **onRenderTriggered -** 触发新的渲染时调用,允许您检查是哪个依赖项触发了组件的重新渲染。 高兴期待看到这两个钩子在未来将会创建什么样的优化工具。 # 监听(Watch) 现在看一个使用组合 API 的简单示例。 这里的代码具有一个简单的搜索输入框,使用搜索文本来调用 API,并返回与输入结果匹配的事件数。 ``` <template> <div> Search for <input v-model="searchInput" /> <div> <p>Number of events: {{ results }}</p> </div> </div> </template> <script> import { ref } from "@vue/composition-api"; import eventApi from "@/api/event.js"; export default { setup() { const searchInput = ref(""); const results = ref(0); results.value = eventApi.getEventCount(searchInput.value); return { searchInput, results }; } }; </script> ``` 它好像没有效果哦。 这是因为`results.value = eventApi.getEventCount(searchInput.value)` 仅中第一次运行`setup()`时被调用一次。 当`searchInput`被更新时,它便不会再次触发。 ## 解决办法:Watch 要解决此问题,我们需要使用*监听*。 这将在下一个刻度(the next tick)上运行我们的函数,同时以响应式的方式跟踪其依赖关系,并在依赖项发生更改时会重新运行它。 像这样: ``` setup() { const searchInput = ref(""); const results = ref(0); watch(() => { results.value = eventApi.getEventCount(searchInput.value); }); return { searchInput, results }; } ``` 当它第一次运行时,使用了响应式来开始跟踪`searchInput`,并且在`searchInput`更新时,它将重新进行 API 调用,然后更新`results`。 由于模板中使用了`results`,因此将重新渲染模板。 如果我想要更具体地监听哪个源的更改,我可以在监视器(watcher)定义中指定它,像这样 ``` watch(searchInput, () => { ... }); ``` 另外,如果我需要访问被监视项的新值和旧值,我可以编写: ``` watch(searchInput, (newVal, oldVal) => { ... }); ``` ## 监听多个项 如果我想观察两个响应式引用,我可以将它们发送到一个数组中: ``` watch([firstName, lastName], () => { ... }); ``` 如果其中任何一个被更改,代码将重新运行。 可以通过以下方式访问它们的新值和旧值: ``` watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => { ... }); ``` # 共享状态(Sharing State) 现在使用 Composition API 来从组件中提取一些可重用的代码。 在使用 API​​ 调用时,很多时候我们可能希望围绕调用构建许多代码和功能。 具体来说就是加载状态,错误状态和try / catch块。 让我们看一下这段代码,然后使用Composition API 正确地提取它。 **/src/App.js** 代码示例: ``` <template> <div> Search for <input v-model="searchInput" /> <div> <p>Loading: {{ loading }}</p> <p>Error: {{ error }}</p> <p>Number of events: {{ results }}</p> </div> </div> </template> <script> import { ref, watch } from "@vue/composition-api"; import eventApi from "@/api/event.js"; export default { setup() { const searchInput = ref(""); const results = ref(null); const loading = ref(false); const error = ref(null); async function loadData(search) { loading.value = true; error.value = null; results.value = null; try { results.value = await eventApi.getEventCount(search.value); } catch (err) { error.value = err; } finally { loading.value = false; } } watch(searchInput, () => { if (searchInput.value !== "") { loadData(searchInput); } else { results.value = null; } }); return { searchInput, results, loading, error }; } }; </script> ``` ## 操作共享状态 这是在 Vue 应用程序中非常常见的模式,在该应用程序中我有一个 API 调用,我需要考虑结果,加载和错误状态。 我如何提取它以使用 组合 API ? 首先,我可以创建一个新文件并提取常用功能。 **/composables/use-promise.js** 代码示例: ~~~js import { ref } from "@vue/composition-api"; export default function usePromise(fn) { // fn is the actual API call const results = ref(null); const loading = ref(false); const error = ref(null); const createPromise = async (...args) => { // Args is where we send in searchInput loading.value = true; error.value = null; results.value = null; try { results.value = await fn(...args); // Passing through the SearchInput } catch (err) { error.value = err; } finally { loading.value = false; } }; return { results, loading, error, createPromise }; } ~~~ 请注意,此函数是如何保存响应式引用以及包装 API 调用的函数,以及需要传递到 API 调用的所有参数的。 现在使用此代码: **/src/App.js** 代码示例: ``` <template> <div> Search for <input v-model="searchInput" /> <div> <p>Loading: {{ getEvents.loading }}</p> <p>Error: {{ getEvents.error }}</p> <p>Number of events: {{ getEvents.results }}</p> </div> </div> </template> <script> import { ref, watch } from "@vue/composition-api"; import eventApi from "@/api/event.js"; import usePromise from "@/composables/use-promise"; export default { setup() { const searchInput = ref(""); const getEvents = usePromise(search => eventApi.getEventCount(search.value) ); watch(searchInput, () => { if (searchInput.value !== "") { getEvents.createPromise(searchInput); } else { getEvents.results.value = null; } }); return { searchInput, getEvents }; } }; </script> ``` 上面就是所有代码,并且我们获得了前面示例的相同功能。 特别要注意的是,在我的`use-promise.js`文件中使用响应式状态(加载,错误和结果)是多么容易,该文件在组件内部使用。 而且如果有另一个 API 调用时,就可以方便的使用`use-promise`了。 ## 警告 当我让 Vue 核心团队的成员运行此操作时,他们提示有要注意`...getEvents`。 具体来说就是,我不应该破坏对象。 在不解构数据时,数据将在`getEvents`命名空间下进行访问,这使它更易于封装,并可以清楚地知道数据在组件中的位置。 看起来是这样: ```js <template> <div> Search for <input v-model="searchInput" /> <div> <p>Loading: {{ getEvents.loading }}</p> <p>Error: {{ getEvents.error }}</p> <p>Number of events: {{ getEvents.results }}</p> </div> </div> </template> <script> ... export default { setup() { ... return { searchInput, getEvents }; } }; </script> ``` 但是,当我在浏览器中运行时,得到以下结果: ![](https://img.kancloud.cn/75/83/7583b9fb8f1457e1250bb9dcc095e1d3_739x438.png) 似乎带有组合 API 的 Vue 2 无法正确识别响应式引用并按应有的方式调用`.value`。 可以通过手动添加`.value`或使用 Vue 3版本来解决此问题。我用 Vue 3 测试了该代码,它确实看到了响应引用,并正确地显示了`.value`。 接下来,介绍 Vue 3 响应式引擎的一些新核心概念。 # 悬念(Suspense) 当编写 Vue 应用程序的代码时,会大量使用 API​​ 调用来加载后端数据。 当用户等待该 API 数据加载时,用户界面最好是让用户知道数据此时正在加载。 如果用户的互联网连接速度较慢,则更加需要这样做。 通常,在 Vue 中,我们在等待数据加载时使用了很多`v-if`和`v-else`语句来显示一些 HTML,然后在数据加载后将其切换出来。 当我们有多个组件进行 API 调用时,事情会变得更加复杂,而我们希望等到所有数据加载完毕后再显示页面。 但是,Vue 3 带有受 React 16.6 启发的替代选项`Suspense`。 这样,您就可以在显示组件之前等待任何异步操作(例如进行数据 API 调用)完成。 `Suspense`是一个内置组件,我们可以使用它包装两个不同的模板,如下所示: ``` <template> <Suspense> <template #default> <!-- Put component/components here, one or more of which makes an asychronous call --> </template> <template #fallback> <!-- What to display when loading --> </template> </Suspense> </template> ``` 当`Suspense`加载时,它将首先尝试渲染出在`<template #default>`中找到的内容。如果在任何时候找到了一个返回一个`promise`或异步组件(这是 Vue 3 的一个新功能)`setup`函数,那么它将渲染`<template #fallback>`直到所有的`promise`已经被解决了。 在这里您可以看到我正在加载`Event`组件: ``` <template> <Suspense> <template #default> <Event /> </template> <template #fallback> Loading... </template> </Suspense> </template> <script> import useEventSpace from "@/composables/use-event-space"; export default { async setup() { const { capacity, attending, spacesLeft, increaseCapacity } = await useEventSpace(); return { capacity, attending, spacesLeft, increaseCapacity }; }, }; </script> ``` 需要注意到:`setup()`方法被标记为`async`和`await useEventSpace()`。 显然,`useEventSpace()`函数中有一个 API 调用,我将等待返回。 现在,当我加载页面时,我会看到`loading …`消息,直到完成 API 调用承诺,然后显示最终结果模板。 ## 多个异步调用 `Suspense`的优点是可以进行多个异步调用,而`Suspense`将等待所有这些调用解决后才能显示任何内容。 所以,如果我把: ``` <template> <Suspense> <template #default> <Event /> <Event /> </template> <template #fallback> Loading... </template> </Suspense> </template> ``` 注意有两个`Event`吗? `Suspense`将等待它们都解决,然后再显示。 ## 深度嵌套的异步调用 更强大的是,我可能具有一个嵌套嵌套的组件,该组件具有异步调用。`Suspense`将等待所有异步调用完成,然后再加载模板。 因此,您可以在应用程序上拥有一个加载屏幕,该屏幕等待应用程序的多个部分加载。 ## 出现错误咋办? 如果 API 调用无法正常运行,通常需要回退,因此我们需要某种错误展示以及加载展示。 幸运的是,`Suspense`语法允许您将其与旧的`v-if`一起使用,并且我们有一个新的`onErrorCaptured`生命周期挂钩,可用于监听错误: ``` <template> <div v-if="error">Uh oh .. {{ error }}</div> <Suspense v-else> <template #default> <Event /> </template> <template #fallback> Loading... </template> </Suspense> </template> <script> import Event from "@/components/Event.vue"; import { ref, onErrorCaptured } from "vue"; export default { components: { Event }, setup() { const error = ref(null); onErrorCaptured((e) => { error.value = e; return true; }); return { error }; }, }; </script> ``` 注意`template`的`div`和`Suspense`标签上的`v-else`。 还要注意`setup`方法中的`onErrorCaptured`回调。 从`onErrorCaptured`返回`true`可以防止错误进一步传播。 这样,我们的用户就不会在其浏览器控制台中看到错误。 ## 创建骨架加载屏幕 使用`Suspense`标记使创建骨架加载屏幕之类的事情变得非常简单。 就像这样: ![](https://img.kancloud.cn/4f/7c/4f7ce2ffea16a7bb0280560796b44883_367x553.png) ![](https://img.kancloud.cn/3f/9c/3f9cf01a224314bfdb2e1036c71f4b89_356x618.png) 骨架将进入您的`<template #fallback>`中,而渲染出的 HTML 将进入您的`<template #default>`中。 真他妈的简单😌! # Teleport Vue 的组件体系结构使我们能够将用户界面构建到可以很好地组织业务逻辑和表示层的组件中。 但是,在某些情况下,一个组件中的某些 html 需要在其他位置呈现。 例如: 1. 需要固定或绝对定位和`z-index`的样式。 例如,通常将 UI 组件(如模式)放置在标记之前,以确保将其正确放置在网页的所有其他部分之前。 2. 当我们的 Vue 应用程序在网页的一小部分(或窗口小部件)上运行时,有时我们可能希望将组件移至 Vue 应用程序之外的其他 DOM 位置中。 ## 解决方案 Vue 3 提供的解决方案是**Teleport**组件。 以前,它被命名为“portal”,但名称已更改为 Teleport,以免与将来的元素冲突,后者未来可能会成为 HTML 标准的一部分。 Teleport 组件允许我们指定模板 html(其中可能包含子组件),我们可以将其发送到 DOM 的另一部分中。 我将向您展示一些非常基本的用法,然后向您展示如何在更高级的方法中使用它。 首先,在 Vue CLI 生成的基本 Vue 应用程序之外添加 div 标签: **/public/index.html**: ``` ... <div id="app"></div> <div id="end-of-body"></div> </body> </html> ``` 然后,让我们尝试将一些文本从 Vue 应用程序内部传送到该`#end-of-body` div 上。 **/src/App.vue**: ``` <template> <teleport to="#end-of-body"> This should be at the end. </teleport> <div> This should be at the top. </div> </template> ``` 注意,在传送线(teleport line)中,我们指定了要将模板代码移动到的`div`,如果操作正确执行了,则顶部的文本应该移至底部。 果然: ![](https://img.kancloud.cn/b6/40/b640769913b9152ff6acc70a8bf9ad6c_663x331.png) ## `Teleport`的`to`属性 我们的`to`属性只需要是一个有效的 DOM 查询选择器。 除了像我上面那样使用`id`之外,还有另外三个示例: 1. **类选择器** ~~~ <teleport to=".someClass"> ~~~ 2. **data 选择器** ~~~ <teleport to="[data-modal]"> ~~~ 3. **动态选择器** 如果需要,您甚至可以绑定动态选择器,添加冒号 ~~~ <teleport :to="reactiveProperty"> ~~~ ## 禁用状态 模块对话框和弹窗通常开始都是隐藏的,直到它们显示在屏幕上为止。 因此,传送具有`disabled`状态,其中内容保留在原始组件内。 直到启用传送功能,它才会移动到目标位置。 让我们更新代码以能够切换`showText`,如下所示: ``` <template> <teleport to="#end-of-body" :disabled="!showText"> This should be at the end. </teleport> <div> This should be at the top. </div> <button @click="showText = !showText"> Toggle showText </button> </template> <script> export default { data() { return { showText: false }; } }; </script> ``` 随着切换,传送内部的内容将从组件内部移动到组件外部。 如果我们实时检查源,可以看到内容实际上正在从`DOM`中的一个地方移动到另一个地方。 ![](https://img.kancloud.cn/2d/15/2d154733948df12b48dc9197fe3c4cf3_617x609.png) ## 自动保存状态 当`teleport`从禁用变为启用时,DOM 元素将被重用,因此它们完全保留现有的状态。这可以通过传送播放的视频来说明。 ``` <template> <teleport to="#end-of-body" :disabled="!showText"> <video autoplay="true" loop="true" width="250"> <source src="flower.webm" type="video/mp4"> </video> </teleport> <div> This should be at the top. </div> <button @click="showText = !showText"> Toggle showText </button> </template> <script> export default { data() { return { showText: false }; } }; </script> ``` 在实际效果中,你会看到视频在 DOM 位置之间移动时的状态保持不变。 ## 隐藏文字 如果我们在`teleport`中拥有的内容是模态的内容,我们可能不希望在其激活之前将其显示。 现在“This should be at the end.”,即使在`showText`为`false`的情况下也会中组件内部显示。 我们可以通过添加`v-if`来禁止显示。 这应该在最后。 ``` <template> <teleport to="#end-of-body" :disabled="!showText" v-if="showText"> This should be at the end. </teleport> ... ``` 现在,仅当`showText`为`true`时,文本才会被传送到页面底部显示。 ## 多个传送到同一个地方 如果您将两个相同内容的 DOM 传送到同一位置时会发生什么? 我可以看到(尤其是使用模态)您可能想传送多件事情。 先看一个简单的示例,简单地创建一个`showText2`。 ``` <template> <teleport to="#end-of-body" :disabled="!showText" v-if="showText"> This should be at the end. </teleport> <teleport to="#end-of-body" :disabled="!showText2" v-if="showText2"> This should be at the end too. </teleport> <div> This should be at the top. </div> <button @click="showText = !showText"> Toggle showText </button> <button @click="showText2 = !showText2"> Toggle showText2 </button> </template> <script> export default { data() { return { showText: false, showText2: false }; } }; </script> ``` 可以看到:多个元素只是简单的被添加到后面。例如先点击“Toggle showText2”,然后再点击“Toggle showText”的效果: ![](https://img.kancloud.cn/62/71/6271ca77d63b78a8e9b33f1c8775949f_649x314.png) ## 结论 `Teleport`提供了一种将代码保留在同一组件中,但可以将代码段移至页面其他部分的方法。 可以将其用于模态窗口显示(需要显示在页面其余部分的顶部,并置于`</body>`标签的正上方)的解决方案。 有关更详细的说明,请参阅[RFC](https://github.com/vuejs/rfcs/blob/rfc-portals/active-rfcs/0025-teleport.md)。 # 参考 > [VueMastery - Vue 3 Essentials](https://coursehunters.online/t/vuemastery-vue-3-essentials/2479) > [0000-sfc-script-setup.md](https://github.com/vuejs/rfcs/blob/sfc-improvements/active-rfcs/0000-sfc-script-setup.md)