前言
用vue3做开发也有一段时间了,之前也就大致学了一下,了解了一些用法就上手写了,最近抽时间去完整的阅读了一下官方文档,发现了很多之前疏忽或遗漏的知识点,还是挺有意思的,就记录了一下,我只是以我个人的角度去记录了,比较主观性,所有不会很全,有兴趣的同学可以看看,查漏补缺。
其中我觉的比较实用的一些点,例如属性穿透,组件generic 属性声明泛型类型参数,生命周期钩子的调用栈的同步性等,知道一下对开发过程中有实际好处的。
列举
1 为什么推荐使用ref,而不是reactive
reactive存在一些局限性:
- 有限的值类型:它只能用于对象类型 (对象、数组和如 Map、Set 这样的集合类型)。它不能持有如 string、number 或 boolean 这样的原始类型。
- 不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地"替换"响应式对象,因为这样的话与第一个引用的响应性连接将丢失:
- 对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接:
2 ref 解包
在模板渲染上下文中,只有顶级的 ref 属性才会被解包。 与 reactive 对象不同的是,当 ref 作为响应式数组或原生集合类型(如 Map
) 中的元素被访问时,它不会被解包
3 父子组件生命周期执行顺序
父setup -》 父 onBeforeMount =〉子 setup -》子onBeforeMount -〉子 onMounted -》 父onMounted
4 watchPostEffect 调用时机问题
默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。 如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,你需要指明 flush: 'post' 选项:
javascript
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',
},
);
经测试,2者的差别在,使用pre的时候,页面第一次去document拿dom是null。而用post,拿dom是拿的到的。
watchEffect的返回值
watcheffect 和watch的返回值都是一个停止函数,执行这个函数后,将停止监听。
5 原生html解析为vue组件
某些 HTML 元素对于放在其中的元素类型有限制,例如 <ul>
,<ol>
,<table>
和 <select>
,相应的,某些元素仅在放置于特定元素中时才会显示,例如 <li>
,<tr>
和 <option>
。
这将导致在使用带有此类限制元素的组件时出现问题。例如:
sql
<table>
<blog-post-row></blog-post-row>
</table>
自定义的组件 <blog-post-row>
将作为无效的内容被忽略,因而在最终呈现的输出中造成错误。我们可以使用特殊的 [is
attribute](cn.vuejs.org/api/built-i...) 作为一种解决方案:
xml
<table>
<tr is="vue:blog-post-row"></tr>
</table>
- 当使用在原生 HTML 元素上时,
is
的值必须加上前缀vue:
才可以被解析为一个 Vue 组件。这一点是必要的,为了避免和原生的自定义内置元素相混淆。
6 defineProps是vue3的一种宏声明,什么是宏声明
宏声明是一种在编程语言中常见的概念,它通常指的是在代码中用特定的语法来声明一个宏。宏可以理解为一种代码片段的替代规则,当程序在编译阶段被处理时,宏会被展开或替换成实际的代码。
宏处理是指在编译阶段对宏进行的处理过程。当程序代码中出现宏声明时,编译器会根据宏的定义,在编译阶段将宏的使用处替换为实际的代码。这个过程类似于在代码中执行文本替换。
在 Vue 3 的情境下,defineProps
是一个宏声明。它被用于在子组件中声明接收的属性,并将这些属性转化为具有响应式能力的数据。宏处理的过程会在编译阶段进行,当 Vue 编译子组件的代码时,会根据 defineProps
的声明将属性转化为响应式属性。
这里是一个更具体的解释:
- 宏声明(Macro Declaration) :在 Vue 3 中,
defineProps
是一种宏声明。你在子组件的setup
函数中使用defineProps
来声明组件接收的属性,告诉 Vue 编译器这些属性应该具有响应式能力。 - 宏处理(Macro Processing) :在编译阶段,Vue 编译器会处理子组件的代码。当遇到使用了
defineProps
的地方,编译器会将属性声明转化为响应式属性的定义,使其具有在运行时可以自动追踪变化的能力。
7 v-bind 对象等价效果
使用一个对象绑定多个 prop
如果你想要将一个对象的所有属性都当作 props 传入,你可以使用**没有参数的 v-bind
**,即只使用 v-bind
而非 :prop-name
。例如,这里有一个 post
对象:
ini
const post = { id: 1, title: 'My Journey with Vue'}
以及下面的模板:
template
ini
<BlogPost v-bind="post" />
而这实际上等价于:
template
ini
<BlogPost :id="post.id" :title="post.title" />
8 onMounted调用栈的同步性
当调用 onMounted
时,Vue 会自动将回调函数注册到当前正被初始化的组件实例上。这意味着这些钩子应当在组件初始化时被同步注册。例如,请不要这样做:
scss
setTimeout(() => {
onMounted(() => {
// 异步注册时当前组件实例已丢失
// 这将不会正常工作
})
}, 100)
注意这并不意味着对 onMounted
的调用必须放在 setup()
或 <script setup>
内的词法上下文中。onMounted()
也可以在一个外部函数中调用,只要调用栈是同步的,且最终起源自 setup()
就可以
9 suspense 和异步组件
怎么算异步组件
- 通过defineAsyncComponent函数定义的组件
- 带有异步
setup()
钩子的组件。这也包含了使用<script setup>
时有顶层await
表达式的组件。
suspense的三个事件
pending
事件是在进入挂起状态时触发。
fallback
事件则是在 fallback
插槽的内容显示时触发。
resolve
事件是在 default
插槽完成获取新内容时触发。
执行顺序如下pending
-》fallback
-〉resolve
什么是挂起
挂起就是识别suspense内部的组件有异步依赖,等等异步依赖执行完成,此时叫完成状态。
如果在初次渲染时没有遇到异步依赖,那么就不会进入挂起状态<Suspense>
会直接进入完成状态。
suspense的timeout参数
官方文档中是这么描述的:
发生回退时,后备内容不会立即展示出来。相反,<Suspense>
在等待新内容和异步依赖完成时,会展示之前 #default
插槽的内容。这个行为可以通过一个 timeout
prop 进行配置:在等待渲染新内容耗时超过 timeout
之后,<Suspense>
将会切换为展示后备内容。若 timeout
值为 0
将导致在替换默认内容时立即显示后备内容。
什么意思?就是比如我第一次已经完成异步组件的加载后,default里面的内容发生了替换,又要加载异步依赖资源了,比如要3秒才能完成,此时如果我设置timeout为1000 ms,那么原先default的内容会在页面停留1000ms,然后渲染fallback插槽里的内容,当异步任务完成,再显示会default的内容。
xml
<Suspense
@pending="pending"
@fallback="fallback"
@resolve="resolve"
:timeout="1000"
>
<AsyncComp v-if="show"></AsyncComp>
<div v-else>AsyncComp else 项目</div>
<template #fallback>
<div>loading...</div>
</template>
</Suspense>
异步组件
<template>
<div>sleep</div>
</template>
<script setup lang="ts">
function sleep(time: number) {
return new Promise<void>((resolve, reject) => {
setTimeout(() => {
resolve();
}, time);
});
}
await sleep(3000);
10 props属性做v-model绑定写法
ini
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">
11 自定义修饰符modelModifiers
修饰符除了官方设定的几个,也可以自己实现。
例如下个例子,就可以根据props.modelModifiers中的capitalize存在与否做不同的逻辑判断
xml
<!-- 父级 -->
<MyComponent v-model.capitalize="myText" />
<!-- 子级 -->
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) }
})
defineEmits(['update:modelValue'])
console.log(props.modelModifiers) // { capitalize: true }
<template>
<input
type="text"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
另一种写法
typescript
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}
<Paypal v-model.aaa="xxxx" ></Paypal>
12 属性穿透
"透传 attribute"指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on
事件监听器。最常见的例子就是 class
、style
和 id
。
当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上。举例来说,假如我们有一个 <MyButton>
组件,它的模板长这样:
xml
<!-- <MyButton> 的模板 -->
<button class="aa">click me</button>
<!-- 使用的时候 -->
<MyButton class="large" />
那么此时这个button被真实渲染的时候,这个node节点上的class属性为"aa large"2个。
案例
其他的属性也是如此,比如最近我司的一个基于vue-demi实现的vue2,vue3的通用组件库里面,一个同事遇到一个问题,他给一个input标签加了readonly
这个属性,在打包出的vue3产物中是生效的,但是在vue2产物中是不生效的,正是因为这个vue3属性穿透的特性。
禁用 Attributes 继承
从 3.3 开始你也可以直接在 <script setup>
中使用 [defineOptions](<https://cn.vuejs.org/api/sfc-script-setup.html#defineoptions>)
xml
<script setup>
defineOptions({
inheritAttrs: false
})
</script>
同样的规则也适用于 v-on
事件监听器
click
监听器会被添加到 <MyButton>
的根元素,即那个原生的 <button>
元素之上。当原生的 <button>
被点击,会触发父组件的 onClick
方法。同样的,如果原生 button
元素自身也通过 v-on
绑定了一个事件监听器,则这个监听器和从父组件继承的监听器都会被触发。
在 JavaScript 中访问透传 Attributes
xml
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>
13 provide注入使用的原则
当提供 / 注入响应式的数据时,建议尽可能将任何对响应式状态的变更都保持在供给方组件中。这样可以确保所提供状态的声明和变更操作都内聚在同一个组件内,使其更容易维护
有的时候,我们可能需要在注入方组件中更改数据。在这种情况下,我们推荐在供给方组件内声明并提供一个更改数据的方法函数
xml
<!-- 在供给方组件内 -->
<script setup>
import { provide, ref } from 'vue'
const location = ref('North Pole')
function updateLocation() {
location.value = 'South Pole'
}
provide('location', {
location,
updateLocation
})
</script>
使用 Symbol 作注入名
如果你正在构建大型的应用,包含非常多的依赖提供,或者你正在编写提供给其他开发者使用的组件库,建议最好使用 Symbol 来作为注入名以避免潜在的冲突。
javascript
// keys.js
export const myInjectionKey = Symbol()
js
// 在供给方组件中
import { provide } from 'vue'
import { myInjectionKey } from './keys.js'
provide(myInjectionKey, { /*
要提供的数据
*/ });
js
// 注入方组件
import { inject } from 'vue'
import { myInjectionKey } from './keys.js'
const injected = inject(myInjectionKey)
14 编译构建步骤和组件模板
当你使用 Vue 时,有两种主要的方式来处理组件模板:一种是无构建步骤方式,另一种是使用了构建步骤。
- 无构建步骤方式: 在无构建步骤的情况下,你可以将组件模板直接写在页面的 HTML 代码中,或者将其作为内联的 JavaScript 字符串。但是,在这种情况下,为了让动态模板能够正常运行,Vue 需要在浏览器中运行模板编译器。这意味着 Vue 在浏览器中会动态地编译模板,然后执行渲染。
- 使用构建步骤: 当你使用构建步骤时,模板会被预先编译,而不需要在浏览器中运行模板编译器。这可以减小客户端代码的体积,提升性能。为了适应不同的优化需求,Vue 提供了多种格式的"构建文件"。
- 前缀为
vue.runtime.*
的文件是只包含运行时的版本,不包含编译器。如果你使用这个版本,所有的模板都必须在构建步骤中预先编译。 - 不包含
.runtime
的文件是完全版,包含编译器,并且可以在浏览器中直接编译模板。但由于包含了编译器,文件体积会增加约 14kb。
默认情况下,工具链会使用只包含运行时的版本,因为在使用构建步骤的情况下,所有单文件组件 (SFC) 中的模板都已经被预编译了。然而,如果你在有构建步骤的情况下仍然需要浏览器内的模板编译,你可以更改构建工具的配置,将 Vue 的版本更改为相应的版本,如 vue/dist/vue.esm-bundler.js
。
总结来说,这段话解释了在不同情况下如何处理 Vue 组件模板,以及如何根据是否使用构建步骤来选择适合的 Vue 版本,以达到优化客户端代码体积和性能的目的。
15 vue3中 方法处理器和内联处理器
@click="foo()" 也是调用方法和直接 @click="foo" 有什么区别
foo() 最后会变成一个 () => foo() 这样的函数,这就导致 foo 拿不到 event 参数 而直接 foo,foo 可以收到 event 参数
16 组件<script>
标签上的 generic
属性声明泛型类型参数
在编写子组件的时候,不确定外界传入的参数是什么类型,可以使用generic属性声明范型。
这样在外界使用子组件的时候,他传入的数据类型他是确定的,子组件里拿到这个类型再抛给他,这样在使用作用域插槽的时候就可以拿到类型了
interface IList { name: string; age: number; } const list = ref<IList[]>([ { name: "a", age: 1, }, { name: "b", age: 2, }, ]);
xml
子组件
<template>
<div>
list
<slot name="header" :expos="props.list"></slot>
</div>
</template>
<script setup lang="ts" generic="T">
const props = defineProps<{
list: T[];
}>();
</script>
17 为组件模板引用标注类型
有时,你可能需要为一个子组件添加一个模板引用,以便调用它公开的方法。举例来说,我们有一个 MyModal
子组件,它有一个打开模态框的方法:
xml
<!-- 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
工具类型来获取其实例类型:
xml
<!-- 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>
18 组合式api的好处
- 更好的逻辑复用
组合式 API 最基本的优势是它使我们能够通过组合函数来实现更加简洁高效的逻辑复用。在选项式 API 中我们主要的逻辑复用机制是 mixins,而组合式 API 解决了 mixins 的所有缺陷。
- mixins 的缺陷: 不清晰的数据来源、命名空间冲突、隐式的跨 mixin 交流
- 更灵活的代码组织
在选项时中,不同功能业务的代码在单文件中可能分布在不同的地方,当复杂程度上升时需要在文件中反复上下滚动。而在组合式中,可以组织聚焦在同一块区域
- IDE(Integrated Development Environment)更好的类型推导
- 更小的生产包体积
搭配 <script setup>
使用组合式 API 比等价情况下的选项式 API 更高效,对代码压缩也更友好。这是由于<script setup>
形式书写的组件模板被编译为了一个内联函数,和 <script setup>
中的代码位于同一作用域。不像选项式 API 需要依赖 this 上下文对象访问属性,被编译的模板可以直接访问<script setup>
中定义的变量,无需从实例中代理。这对代码压缩更友好,因为本地变量的名字可以被压缩,但对象的属性名则不能。
后续
暂时列举这些,还没有列完,主要还有一些没有看完,并且在阅读的过程中还遇到了一些疑问,暂时没有想清楚,后续的内容会有空的时候再出一版。