- Published on
译: 你不知道的Vue3细节
目录
- 为什么推荐 ref 而不是 reactive ?
- watchPostEffect 的计时问题
- 将原生 HTML 解析为 Vue 组件
- DefineProps 是Vue 3中的宏声明,什么是宏声明?
- v-bind 绑定对象
- onMounted 同步调用栈
- 使用自定义组件实现 v-model
- 自定义修饰符 modelModifiers
- 属性穿透
- Provide and Inject
- Vue3 中的事件处理程序和内联处理程序
- <script> 标签上的通用属性声明通用类型参数
- 组件模板引用的类型注解
为什么推荐 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的返回值:
watchEffect
和 watch
都返回一个停止函数。一旦执行该函数,监听就会停止。
将原生 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>