Published on

译: 你不知道的Vue3细节

目录

为什么推荐 ref 而不是 reactive ?

  • 限制值类型:只能用于对象类型(对象、数组以及Map、Set等集合类型)。它不能保存字符串、数字或布尔值等基本类型。

  • 无法替换整个对象:因为Vue的响应式跟踪是通过属性访问实现的,所以我们必须始终维护对反应式对象的相同引用。这意味着我们无法轻松地“替换”响应性对象,因为这会失去与初始引用的响应性连接:

  • 对解构操作不友好:当我们将响应式对象的原始类型属性解构为局部变量,或将属性传递给函数时,将丢失响应式连接。

watchPostEffect 的计时问题

默认情况下,用户创建的监听器回调将在 Vue 组件更新之前调用。这意味着监听器回调中访问的 DOM 将处于 Vue 更新之前的状态。

如果你希望在侦听器回调中访问Vue 更新后的DOM,则需要指定 flash: ‘post’ 选项。

请看下列例子:

默认配置:

<template>
  <div id="pel">
    {{ state.count }}
  </div>
</template>

<script lang='ts' setup>
import { reactive, watchEffect } from 'vue';

const state = reactive({count: 1});

watchEffect(
  (onCleanup) => {
    console.log(document.getElementById('pel'));
    console.log('WatchEffect: Count changed:', state.count);
    // console.log('WatchEffect: pel changed:', pel.value);
    onCleanup(() => {
      console.log('WatchEffect: onCleanup');
    });
  },
  {
    flush: 'pre',
  },
);
</script>

首次执行会打印:

null
Post.vue:15 WatchEffect: Count changed: 1

document.getElementById('pel')null原因是因为副作用回调执行时,新的DOM并未生成,故为null

flush选项的值改为flush: 'post'后就能正常打印DOM了。

watchEffect的返回值:

watchEffectwatch 都返回一个停止函数。一旦执行该函数,监听就会停止。

将原生 HTML 解析为 Vue 组件

某些 HTML 元素对放置在其中的元素类型有限制,例如 <ul><ol><table><select> 。相应地,某些元素仅在放置在特定元素内时才会显示,例如 <li><tr><option>

当使用具有此类受限元素的组件时,这可能会导致问题。例如:

<table>
  <blog-post-row></blog-post-row>
</table>

自定义组件 将被视为无效内容而被忽略,从而导致最终渲染输出出现错误。我们可以使用特殊的 is 属性作为解决方案:

<table>
  <tr is="vue:blog-post-row"></tr>
</table>

当用于原生 HTML 元素时,“is”的值必须以“vue:”为前缀才能解析为 Vue 组件。为了避免与本机自定义内置元素混淆,这是必要的。

DefineProps 是Vue 3中的宏声明,什么是宏声明?

宏声明是编程语言中的一个常见概念,通常是指在代码中使用特定语法对宏进行声明。宏可以理解为代码片段的替换规则,在编译过程中将其扩展或替换为实际代码。

宏处理是指编译阶段对宏进行处理的过程。当程序代码中出现宏声明时,编译器会根据宏的定义,在编译阶段将宏的用法替换为实际代码。此过程类似于在代码中执行文本替换。

在 Vue 3 的上下文中, defineProps 是一个宏声明。它用于声明子组件中接收到的属性,并将这些属性转换为具有反应性的数据。

宏处理的过程发生在编译阶段,当 Vue 编译子组件的代码时,它会根据 defineProps 的声明将属性转换为响应式属性。

v-bind 绑定对象

如果要将对象的所有属性作为 props 传递,可以使用不带参数的 v-bind,即使用 v-bind 而不是 :prop-name。例如,这是一个 post 对象:

const post = {  id: 1,  title: 'My Journey with Vue'}

以及以下模板:

<BlogPost v-bind="post" />

然而,这实际上相当于:

<BlogPost :id="post.id" :title="post.title" />

onMounted 同步调用栈

当调用 onMounted 时,Vue 会自动在当前正在初始化的组件实例上注册回调函数。这意味着这些钩子应该在组件初始化时同步注册。

例如,不要这样做:

setTimeout(() => {
  onMounted(() => {
    // ...
  })
}, 100)

请注意,这并不意味着对 onMounted 的调用必须放在 setup()<script setup> 的词法上下文中。 onMounted() 也可以从外部函数调用,前提是调用堆栈是同步的并且最终源自 setup()

使用自定义组件实现 v-model

const props = defineProps<{
  visible: boolean;
}>();

const visible = computed({
  set: (val) => emit('update:visible', val),
  get: () => props.visible,
});
<a-modal v-model:visible="visible" centered :footer="null">

自定义修饰符 modelModifiers

除了官方设定的少数之外,修饰符也可以自己实现。

比如下面的例子,可以根据props.modelModifiers中是否存在‘capitalize’来做出不同的逻辑判断。

<template>
  <input
    type="text"
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

<!-- Parent -->
<script lang='ts' setup>
<MyComponent v-model.capitalize="myText" />

<!-- Child -->
const props = defineProps({
  modelValue: String,
  modelModifiers: { default: () => ({}) }
})

defineEmits(['update:modelValue'])

console.log(props.modelModifiers) // { capitalize: true }
</script>

或者

<template>
  <Paypal v-model.aaa="xxxx" ></Paypal>
</template>

<script lang='ts' setup>
interface IProps {
  // selectPlan: 'Yearly' | ' Monthly';
  selectPlan: string;
  modelModifiers?: { default: () => Record<string, unknown> };
}
const props = withDefaults(defineProps<IProps>(), {
  selectPlan: 'Yearly',
});
console.log(props.modelModifiers); // {aaa:true}
</script>

属性穿透

“传递属性”是指传递给组件但未声明为 props 或由组件发出的属性或 v-on 事件侦听器。最常见的示例是类、样式和 id。

当组件呈现为单个元素根时,传递的属性会自动添加到根元素。例如,如果我们有一个 <MyButton> 组件,它的模板如下所示:

<!-- <MyButton> temp-->
<button class="aa">click me</button>

<!-- use -->
<MyButton class="large" />

这时,当这个按钮真正渲染出来的时候,这个node节点的class属性就是“aa”和“large”。

禁用属性继承

从版本3.3开始,您还可以在<script setup>中直接使用defineOptions

相同的规则也适用于 v-on 事件侦听器

<script setup>
defineOptions({
  inheritAttrs: false
})
</script>

单击侦听器将添加到<MyButton>的根元素,即本机<button>元素上。当原生<button>被点击时,会触发父组件的onClick方法。同样,如果原生按钮元素本身也通过v-on绑定了一个事件监听器,那么这个监听器和继承自父组件的监听器都会被触发。

在 JavaScript 中访问传递属性

<script setup>
import { useAttrs } from 'vue'

const attrs = useAttrs()
</script>

Provide and Inject

当使用提供/注入反应性数据时,建议尽可能在Provide组件内保留对反应性状态的任何更改。这可以确保提供的状态的声明和修改操作都包含在同一个组件内,从而更易于维护。

有时,我们可能需要更改注入组件中的数据。在这种情况下,我们建议声明并提供一个方法函数来更改提供程序组件中的数据。

<script setup>
import { provide, ref } from 'vue'

const location = ref('North Pole')

function updateLocation() {
  location.value = 'South Pole'
}

provide('location', {
  location,
  updateLocation
})
</script>

使用Symbol作为注入名称

如果你正在构建具有许多依赖项提供程序的大型应用程序,或者您正在为其他开发人员编写组件库,则最好使用 Symbols 作为注入名称以避免潜在的冲突。

// keys.js
export const myInjectionKey = Symbol()

// provide Comp
import { provide } from 'vue'
import { myInjectionKey } from './keys.js'
provide( myInjectionKey,  { /* data */ });

// inject Comp
import { inject } from 'vue'
import { myInjectionKey } from './keys.js'

const injected = inject(myInjectionKey)

Vue3 中的事件处理程序和内联处理程序

通过 @click=”foo()” 调用方法和直接使用 @click=”foo” 调用方法有什么区别?

foo() 最终会变成 () => foo() 这样的函数,这意味着 foo 无法获取事件参数。

<script> 标签上的通用属性声明通用类型参数

编写子组件时,如果不确定外部传入的参数类型,可以使用generic属性来声明泛型。

这样,当外部世界使用子组件时,传入的数据类型是确定的。子组件获取此类型并将其返回,因此在使用作用域插槽时可以获取类型。

// Child
<template>
  <div>
    list
    <slot name="header" :expos="props.list"></slot>
  </div>
</template>

<script setup lang="ts" generic="T">
const props = defineProps<{
  list: T[];
}>();
</script>

组件模板引用的类型注解

有时,您可能需要添加对子组件的模板引用才能调用其公共方法。例如,我们有一个 MyModal 子组件,它有一个打开模态框的方法:

<!-- MyModal.vue -->
<script setup lang="ts">
import { ref } from 'vue'

const isContentShown = ref(false)
const open = () => (isContentShown.value = true)

defineExpose({
  open
})
</script>

要获取 MyModal 的类型,我们首先需要使用 typeof 获取其类型,然后使用 TypeScript 内置工具类型 InstanceType 获取其实例类型。

<!-- App.vue -->
<script setup lang="ts">
import MyModal from './MyModal.vue'

const modal = ref<InstanceType<typeof MyModal> | null>(null)

const openModal = () => {
  modal.value?.open()
}
</script>