[译] Vue3 Composition API

950 阅读27分钟

原文:Vue3 Composition API

1. Why the Composition API

1.1. why the composition api

如果你对新的Vue3 Composition API引起了一些困惑,那么在本课程结束时,应该明确为什么Vue 2的局限性导致了它的产生,以及它如何为我们解决了一些问题。 目前,在使用Vue 2时可能遇到三个限制:

  • 随着您的组件变得更大,可读性变得越来越困难。
  • 当前的代码重用模式都具有缺点。
  • Vue 2提供了有限的TypeScript支持。

我将详细介绍前两个,因此很明显新API解决了什么问题。

1.2. Large components can be hard to read & maintain.

为了解决这个问题,我们来考虑一下可以在我们的网站上搜索产品的组件。 image.png 使用标准Vue组件语法的该组件代码将如下所示: image.png 当我们还想添加对搜索结果进行排序的功能时,会发生什么情况。我们的代码如下所示: image.png 不错,直到我们要向同一组件添加搜索过滤器和分页功能。我们的新功能将包含代码片段,我们将在所有组件选项(components,props,data,computed,methods和lefecycle methods)之间进行拆分。如果我们使用颜色(在下面)将其可视化,您将看到我们的功能代码将如何拆分,从而使我们的组件更难以阅读和解析哪个功能代码与哪个功能一起使用。 image.png 您可以想象(如右图所示),如果我们可以将功能代码保持在一起,那么我们的代码将更具可读性,从而更易于维护。如果我们回到原始示例并使用composition API将事物分组在一起,则结果如下所示: image.png 要使用setup()方法(如上所示)执行此操作,我们需要使用新的Vue 3 composition API。setup()中的代码是一种新语法,我将在以后的课程中进行讲授。值得注意的是,这种新语法是完全可选的,并且编写Vue组件的标准方法仍然完全有效。 我知道当我第一次看到它时,我想知道:“等等,这是否意味着我创建了一个巨大的设置方法,并将所有代码都放在其中?怎么会这样呢?” 不,不用担心,这不会发生。当使用Composition API按功能组织组件时,您会将功能分组为可通过设置方法调用的组合功能,如下所示: image.png 现在,可以使用逻辑关注点(也称为“功能”)来组织我们的组件。但是,这并不意味着我们的用户界面将由更少的组件组成。您仍将使用良好的组件设计模式来组织您的应用程序: image.png 既然您已经了解了Component API如何使大型组件更具可读性和可维护性,那么我们可以继续讲Vue 2的第二个限制。

1.3. There’s no perfect way to reuse logic between components.

关于跨组件重用代码,在Vue 2中有3种好的解决方案可以做到这一点,但是每种解决方案都有其局限性。让我们通过我们的示例进行遍历。首先,有Mixins。 image.png 优点

  • Mixins可以按功能进行组织。

缺点

  • 它们容易发生冲突,并且您最终可能会遇到属性名称冲突。
  • 不清楚混合素如何相互作用(如果存在)。
  • 如果要配置Mixin以在其他组件之间使用,则不容易重用

这最后一项使我们看一下Mixin工厂,这是返回自定义版本的Mixin的函数。 image.png 如您在上面看到的,Mixin工厂允许我们通过发送配置来自定义Mixins。现在,我们可以配置此代码以在多个组件中使用。 优点

  • 现在我们可以配置代码,因此可以轻松重用
  • 关于我们的Mixins交互方式,我们具有更明确的关系

缺点

  • 命名间隔需要严格的惯例和纪律。
  • 我们仍然有隐式的属性添加,这意味着我们必须查看Mixin内部以找出它公开的属性。
  • 在运行时没有实例访问权限,因此无法动态生成Mixin工厂。

幸运的是,还有一个解决方案通常是最有用的作用域插槽image.png 优点

  • 解决了Mixins的几乎所有缺点。

缺点

  • 您的配置最终出现在模板中,理想情况下,模板应仅包含我们要呈现的内容。
  • 它们会增加模板的缩进量,从而降低可读性。
  • 公开的属性仅在模板中可用。
  • 由于我们使用的是3个组件而不是1个,因此性能有所降低。

如您所见,每种解决方案都有其局限性。Vue 3的composition API为我们提供了提取可重用代码的第四种方式,该方式可能类似于: image.png 现在,我们将使用composition API内部的函数来创建组件,这些函数会在我们需要任何配置的设置方法中导入并使用。 优点

  • 我们正在编写的代码更少,因此将功能从组件中拉入功能变得更加容易。
  • 由于您已经熟悉功能,因此它以您现有的技能为基础。
  • 它比Mixins和Scoped插槽更灵活,因为它们只是功能。
  • Intellisense,自动完成和键入已在您的代码编辑器中起作用。

缺点

  • 需要学习新的低级API来定义合成功能。
  • 现在有两种编写组件的方法,而不仅仅是标准语法。

希望您现在清楚了成分API背后的“为什么”,我知道一开始我还不清楚。在下一课中,我将深入探讨组成组件的新语法。

2. Setup & Reactive References

在这一点上,您可能想知道新的composition API语法是什么样,我们将深入其中。首先,我们想弄清楚何时使用它,然后我们将学习设置功能和反应性参考或参考。另外,如果您还没有的话,您可能想获取Vue Mastery的Vue 3备忘单。 **免责声明:**如果您还没有赶上这一步,那么composition API纯粹是可加的,不会过时。您可以像对Vue 2进行编码一样对Vue 3进行编码。

2.1 When to use the Composition API?

如果满足以下任一条件:

  • 您需要最佳的TypeScript支持。
  • 组件太大,需要按功能进行组织。
  • 需要在其他组件之间重用代码。
  • 您和您的团队更喜欢替代语法。

**免责声明2:**以下示例非常简单。使用Composition API如此简单地构造组件是不必要的,但使它更易于学习。 让我们从使用普通的Vue 2 API编写的非常简单的组件开始,该组件在Vue 3中有效。

<template>
  <div>Capacity: {{ capacity }}</div>
</template>
<script>
export default {
  data() {
    return {
      capacity: 3
    };
  }
};
</script>

注意,我有一个简单的capacity属性,它是反应性的。Vue知道采用data属性返回的对象中的每个属性,并使它们具有反应性。这样,当这些反应性属性更改时,使用此属性的组件将重新呈现。h,对不对?在浏览器中,我们看到: image.png

2.2 Using the Setup Function

在使用Composition API的Vue 3中,我将从编写setup以前见过的方法开始:

<template>
  <div>Capacity: {{ capacity }}</div>
</template>
<script>
export default {
  setup() {
    // more code to write
  }
};
</script>

setup评估以下任一选项之前的执行:

  • components
  • props
  • data

还值得一提的是setup,与其他Component选项不同,该方法无权访问“ this”。为了获得对属性的访问权限,我们通常使用此属性进行访问,它setup具有两个可选参数。第一个是响应式(Reactive)的并且可以watch的props,例如:

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

但是,让我们回到我们的例子。我们的代码需要一个响应式引用(Reactive reference):

2.3 Reactive References

<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)正在创建“Reactive Reference”,基本上,它将原始整数(3)包装在一个对象中,这将允许我们进行跟踪和更改。请记住,以前我们的data()选项已经将我们的原始(容量)包装在一个对象内。 另外: composition API允许我们声明与组件无关的反应性基元,这就是我们的方法。 最后一步,我们需要显式返回一个对象,该对象的属性需要模板正确呈现。

<template>
  <div>Capacity: {{ capacity }}</div>
</template>
<script>
import { ref } from "vue";
export default {
  setup() {
    const capacity = ref(3);
    return { capacity };
  }
};
</script>

这个返回的对象是我们如何在中公开需要访问哪些数据的方法renderContext。 像这样明确表示有点冗长,但这也是故意的。它有助于长期维护,因为我们可以控制暴露给模板的内容,并跟踪定义模板属性的位置。现在,我们开始了: image.png

2.4 Using Vue 3 with Vue 2

值得注意的是,您今天可以使用此插件将Vue 3 Composition API与Vue 2结合使用。在Vue 2应用程序上安装并配置它之后,您将使用上面教过的相同语法,但有一点点改动。代替

import { ref } from "vue";

你会写

import { ref } from "@vue/composition-api";

如果您想知道的话,这就是我测试上述所有代码的方式。 接下来,我们将学习如何使用这种新语法编写组件方法。

3. Methods

既然我们已经学会了如何创建反应式引用,那么我们组成组件的下一个构建块就是创建方法。如果您尚未下载Vue 3 Composition API速查表,那么现在是个好时机。这是我们当前的代码如下所示:

<template>
  <div>Capacity: {{ capacity }}</div>
</template>
<script>
import { ref } from "vue";
export default {
  setup() {
    const capacity = ref(3);
    return { capacity };
  }
};
</script>

如果我们想添加一个方法来允许我们通过按钮增加容量,则可以在常规组件语法中编写以下内容:

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将它们创建为函数即可。但是,您认为我们如何从设置方法内部增加容量?您可能会猜测:

function increaseCapacity() { 
  capacity++;
}

这行不通,并且会出错。请记住,它capacity是一个反应式引用,一个封装我们整数的对象。增加一个对象将不起作用。在这种情况下,我们需要增加value反应性引用封装的内部整数。我们可以通过访问来做到这一点capacity.value

function increaseCapacity() { 
  capacity.value++;
}

如果我们在浏览器中签入,现在一切正常: image.png 这里的所有都是它的。但是,如果看一下模板,您会注意到在打印容量时:

<p>Capacity: {{ capacity }}</p>

我们不必写信capacity.value,您可能想知道为什么。 事实证明,当Vue ref在模板中找到时,它会自动公开内部值,因此您永远不需要.value在模板内部调用。

4. Computed Properties

让我们学习如何使用新的Composition API语法创建计算属性。首先,尽管我们需要将其添加到示例应用程序中,所以现在我们有了参加活动的人员列表。

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

请注意,我们现在有了新的参与者阵列,并且我们将每个参与者打印出来。我们的网站看起来像: image.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>

如您在上面的代码中所见,我们是computed从Vue API 导入的,然后使用它,传入一个匿名函数并将其设置为等于的常量spacesLeft。然后,我们从setup函数将其返回到对象中,以便我们的模板可以访问它。现在在浏览器中,我们看到的是: image.png

5. The Reactive Syntax

到目前为止,我们一直在使用反应式引用将JavaScript原语包装在对象中以使其具有反应性。但是,还有另一种方法可以将这些原语包装在对象中。具体使用reactive语法。 在下面,您可以在左侧看到我们的示例,其中使用了Reactive References,在右侧使用了其他reactive语法。 image.png

如右图所示,我们创建了一个新event常量,该常量接受一个普通的JavaScript对象并返回一个反应对象。data在常规组件语法中使用该选项可能看起来很熟悉,在该语法中我们还发送了一个对象。但是,如您在上面看到的,我们也可以将计算的属性发送到该对象中。您还应该注意,使用这种语法.value时,在访问属性时不再需要编写。这是因为我们只是访问对象上的event对象属性。您还应该注意,我们在函数event末尾返回了整个对象setup。 请注意,这两种语法都是完全有效的,并且都不被视为“最佳实践”。 为了使我们的代码正常工作,我们需要如下更新模板代码:

<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.以访问属性。

5.1 Destructuring?

当我第一次看到以下代码时:

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

这些都不起作用,因为拆分该对象将删除其反应性。为了使这项工作有效,我们需要能够将此对象拆分为Reactive References,以能够保持反应性。

5.2 Introducing toRefs

幸运的是,有一种方法可以使用该toRefs方法执行此操作。此方法将反应对象转换为普通对象,其中每个属性都是指向原始对象上属性的Reactive Reference。这是我们使用此方法完成的代码:

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语句中使用它,然后销毁该对象。这很棒!

5.3 Aside

在继续之前,我想提一下,如果我们的代码不需要同时increaseCapacity以返回值返回该函数,那么我可以简单地编写:

return toRefs(event);

这是因为我们的setup方法希望我们返回一个对象,这也正是返回的对象toRefs

6. Modularizing

我们可能使用组件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>

6.1 Extracting into a Composition Function

听起来很简单:

<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()现在,该方法成为将合成功能绑定在一起的地方。

6.2 Extracting into a file to reuse the code

如果useEventSpace()是我可能想在多个组件中使用的一段代码,我要做的就是将这个函数提取到自己的文件中,并使用导出默认值: 📃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为合成功能调用了文件夹,但是您可以随意调用它。composableshooks其他好名字。 现在,我的组件代码只需导入此合成函数并使用它。

<template>
      ...
    </template>
    <script>
    import useEventSpace from "@/use/event-space";
    export default {
      setup() {
        return useEventSpace();
      }
    };
    </script>

6.3 Adding another Composition Function

如果我们还有另一个组合功能(可能在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提取的事件数据。

6.4 Vue 3 Best Practice

在上面的代码中,尽管它非常高效,但是却引入了一个问题。现在还不清楚哪些对象来自哪些合成函数。这是我们离开Mixins的原因的一部分,后者可以隐藏哪些对象来自哪些代码段。由于这个原因,我们可能想使用本地对象以不同的方式编写:

<template>
      ...
    </template>
    <script>
    import useEventSpace from "@/use/event-space";
    import useMapping from "@/use/mapping";
    export default {
      setup() {
        const { capacity, attending, spacesLeft, increaseCapacity } = useEventSpace();
        const { map, embedId } = useMapping();

        return { capacity, attending, spacesLeft, increaseCapacity, map, embedId };
      }
    };
    </script>

现在可以清楚地知道对象从何而来。

7. Lifecycle Hooks

您可能对Vue生命周期挂钩很熟悉,它使我们能够在组件达到执行中的特定状态时运行代码。让我们回顾一下典型的LifeCycle挂钩:

  • **beforeCreate -**在实例初始化之后,处理选项之前立即调用。
  • created-创建实例后调用。
  • **beforeMount-**在开始安装DOM之前
  • mounted-挂载实例(浏览器已更新)时调用。
  • **beforeUpdate-**在重新呈现DOM之前,当反应性数据已更改时调用。
  • **updated-**当反应性数据已更改且DOM已重新呈现时调用。
  • **beforeDestroy-**在销毁Vue实例之前调用。
  • **destroyed-**在Vue实例销毁后调用。

您可能不熟悉两种新的Vue 2 LifeCycle方法:

  • **activated-**用于,当组件内部 开启。
  • **deactivated-**用于,当组件内部 被关闭。
  • **errorCaptured-**捕获到任何后代组件中的错误时调用。

有关更多详细说明,请查看LifeCycle挂钩上的API文档。

7.1 Unmounting in Vue 3

在Vue 3 beforeDestroy()中,也可以写成beforeUnmount()destroyed()也可以写成unmounted()。当我向Evan You询问这些更改时,他提到这只是更好的命名约定,因为Vue会_挂载_和_卸载_组件。

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!");
    });
  }
};

您可能会注意到缺少两个钩子。 beforeCreate并且created在使用Composition API时不需要。这是因为beforeCreate()在之前setup()和之后都created()被称为setup()。因此,我们只需setup()将通常放在这些挂钩中的代码放入其中,例如API调用。

7.2 Two New Vue 3 LifeCycle Methods

Vue 3中还有另外两个观察者。Vue2 Composition API插件尚未实现这些观察者(在我写这篇文章时),因此如果不使用Vue 3源代码就无法使用它们。

  • **onRenderTracked-**在渲染期间首次在渲染函数中访问反应式依赖项时调用。现在将跟踪此依存关系。这有助于查看要跟踪的依赖项以进行调试。
  • **onRenderTriggered-**触发新的渲染时调用,允许您检查是什么依赖性触发了组件进行重新渲染。

我很高兴看到可以使用这两个挂钩创建什么样的优化工具。

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

使用此代码,当我们使用表单时会发生以下情况: image.png 如您所见,它似乎没有用。这是因为我们的API调用代码results.value = eventApi.getEventCount(searchInput.value); 在第一次setup()运行时仅被调用一次。当我们searchInput 更新时,它不知道会再次触发。

8.1 Solution: watchEffect

要解决此问题,我们需要使用watchEffect。这将在下一个刻度上运行我们的函数,同时以被动方式跟踪其依赖关系,并在依赖关系发生更改时重新运行它。像这样:

setup() {
  const searchInput = ref("");
  const results = ref(0);

  watchEffect(() => {
    results.value = eventApi.getEventCount(searchInput.value);
  });

  return { searchInput, results };
}

因此,第一次运行它时,它将使用反应性来开始跟踪searchInput,并且在更新时,它将重新运行我们的API调用,该更新将进行更新results。由于results已在我们的模板中使用,我们的模板将被重新渲染。 image.png 如果我想更具体地了解要监视的源,可以使用watch代替watchEffect,例如:

watch(searchInput, () => {
  ...
});

另外,如果我需要访问被监视项的新值和旧值,我可以编写:

watch(searchInput, (newVal, oldVal) => {
  ...
});

8.2 Watching Multiple Sources

如果要观看两个反应式引用,可以将它们发送到数组中:

watch([firstName, lastName], () => {
  ...  
});

现在,如果其中任何一个被更改,则其中的代码将重新运行。我还可以通过以下方式访问它们的旧值和新值:

watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
  ...   
});

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

在浏览器中看它看起来像: image.png

9.1 Now with Shared State

这是在Vue应用程序中非常常见的模式,在该应用程序中我有一个API调用,我需要考虑结果,加载和错误状态。我如何提取它以使用组合API?首先,我可以创建一个新文件并提取常用功能。 📄/ composables/ use- promise.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调用时,我可以使用承诺。

9.2 Caveat

当我由Vue核心团队的成员执行此操作时,他们呼吁注意…getEvents。特别是我不应该破坏对象。在不破坏数据的情况下getEvents,对数据进行命名空间,使其更易于封装,并清楚使用该数据的组件中的数据来源。它可能看起来像:

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

但是,当我在浏览器中运行时,得到以下结果: image.png 似乎带有composition API的Vue 2无法正确识别我的反应式引用并.value像应有的那样调用。我可以.value通过使用Vue 3手动添加orr 来解决此问题。我使用Vue 3测试了代码,果然,它看到了Reactive References并正确显示了.value

10. Suspense

当我们编写Vue应用程序的代码时,我们会大量使用API调用来加载后端数据。当我们等待该API数据加载时,让用户知道数据正在加载是一种很好的用户界面做法。如果用户的互联网连接速度较慢,则尤其需要这样做。 通常在Vue中,我们在等待数据加载时使用了很多v-ifand 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>。如果在任何时候找到具有setup返回诺言的函数的组件,或者找到异步组件(这是Vue 3的新功能),它将替代呈现,<template #fallback>直到所有诺言得到解决。 让我们看一个非常基本的例子:

<template>
  <Suspense>
    <template #default>
      <Event />
    </template>
    <template #fallback>
      Loading...
    </template>
  </Suspense>
</template>
<script>
import Event from "@/components/Event.vue";
export default {
  components: { Event },
};
</script>

在这里您可以看到我正在加载事件组件。它看起来与以前的课程类似:

<template>
...
</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调用,我将等待返回。 现在,当我加载页面时,我会看到正在加载…消息,直到解析了API调用承诺,然后显示了生成的模板。 image.png image.png

10.1 Multiple Async Calls

Suspense的优点是我可以进行多个异步调用,而Suspense将等待所有这些调用被解析以显示任何内容。所以,如果我把:

<template>
  <Suspense>
    <template #default>
      <Event />
      <Event />
    </template>
    <template #fallback>
      Loading...
    </template>
  </Suspense>
</template>

注意这两个事件吗?现在,Suspense将等待它们都解决,然后再显示。 image.png image.png

10.2 Deeply Nested Async Calls

更强大的是,我可能拥有一个具有异步调用的深层嵌套组件。挂起将等待所有异步调用完成,然后再加载模板。因此,您可以在应用程序上拥有一个加载屏幕,该屏幕等待应用程序的多个部分加载。

10.3 What about errors?

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

注意顶部的DIV,和v-else悬疑的标签。还要注意onErrorCapturedsetup方法中的回调。如果您想知道,true从返回onErrorCaptured是为了防止错误进一步传播。这样,我们的用户就不会在其浏览器控制台中看到错误。

10.4 Creating Skeleton Loading Screens

使用Suspense标记使创建骨架加载屏幕之类的事情变得非常简单。你知道,像这样: image.png image.png 您的骨架将进入您的框架,<template #fallback>而渲染的HTML将进入您的框架<template #default>。很简单!

11. Teleport

Vue的组件体系结构使我们能够将用户界面构建到可以很好地组织业务逻辑和表示层的组件中。但是,在某些情况下,一个组件中的某些html需要在其他位置呈现。例如:

  1. 需要固定或绝对定位和z-index的样式。例如,一种常见的模式是将UI组件(如模式)</body>放在标记之前,以确保将其正确放置在网页的所有其他部分之前。
  2. 当我们的Vue应用程序在网页的一小部分(或窗口小部件)上运行时,有时我们可能希望将组件移至Vue应用程序之外的DOM中的其他位置。

11.1 Solution

Vue 3提供的解决方案是Teleport组件。以前,它被称为“门户”,但是名称已更改为Teleport,以免与将来<portal>可能会成为HTML标准一部分的未来元素发生冲突。Teleport组件允许我们指定可以发送到DOM另一部分的模板html(可能包括子组件)。我将向您展示一些非常基本的用法,然后向您展示我们如何在更高级的方法中使用它。让我们从div在基本的Vue CLI生成的应用程序的Vue应用程序外部添加标签开始: /public/index.html

...
    <div id="app"></div>
    <div id="end-of-body"></div>
  </body>
</html>

然后,让我们尝试将一些文本#end-of-body从我们的Vue应用程序内部传送到该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>

注意,在传送线中,我们指定了要将模板代码移至的div,如果正确执行此操作,则顶部的文本应移至底部。果然,它可以: (宽度= 300) image.png

11.2 Teleport Options for To

我们的to属性只需要是一个有效的DOM查询选择器即可。除了使用id我上面所做的操作之外,这里还有三个示例。 类选择器

<teleport to=".someClass">

数据选择器

<teleport to="[data-modal]">

使用data属性,我们的目标div可能如下所示: 动态选择器 如果需要,您甚至可以绑定动态选择器,添加冒号。

<teleport :to="reactiveProperty">

11.3 Disabled State

模式和其他弹出窗口通常开始隐藏,直到它们显示在屏幕上为止。因此,传送处于禁用状态,其中内容保留在原始组件内。直到启用传送功能,它才会移动到目标位置。让我们更新代码以使其能够切换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>

如您所见,随着切换,传送内部的内容将从组件内部移动到组件外部: <01-disable.gif宽度= 250> image.png image.png 如果我们实时检查源,我们可以看到内容实际上是在DOM中从一个地方移到另一个地方。 <02-devtools.gif宽度= 367> image.png image.png image.png

11.4 Automatically Saving the State

当传送从禁用变为启用时,将重新使用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>

正如您在下面的视频中看到的那样,视频在位置之间移动时的状态保持不变。 <03-video.gif宽度= 266> image.png image.png

11.5 Hiding the Text

如果我们在瞬移传送中拥有的内容是模态的,那么我们可能不希望在其处于活动状态之前对其进行显示。现在“这应该在结尾。” 即使在showText为false的情况下也在组件内部显示。我们可以通过添加v-if来禁止显示。

<template>
  <teleport to="#end-of-body" :disabled="!showText" v-if="showText">
      This should be at the end.
  </teleport>
  ...

现在,仅当showText为true时,我们的文本才会显示,并因此被传送到页面底部。 <04-v-如果宽度= 250> image.png image.png

11.6 Multiple Teleports into the Same Place

这让我想知道,当您将两件事传送到同一位置时会发生什么?我可以看到(尤其是使用模态)您可能想传送多件事情。让我们尝试一个简单的示例,简单地创建一个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>

您可以从下面的视频中看到它按预期运行,并在切换后添加内容。有趣的是,它只是根据首先单击的元素添加元素。 <width = 300px> image.png image.png image.png

11.7 Conclusion

如您所见,使用Teleport为您提供了一种将代码保留在同一组件中,同时将代码段移至页面其他部分的方法。除了将其用于模态的显而易见的解决方案(需要显示在页面其余部分的顶部,并置于</body>标签上方)之外,我很高兴看到在实践中还可以使用此Vue 3功能。 有关更详细的书面说明,请参阅RFC。 感谢您完成我们的第一个Vue 3课程。接下来,我将研究Vue 3反应性课程,解释Vue 3的新反应性引擎的一些核心概念。