Vue3(结合TS基础知识)
Vue3
本文档学习的内容如下:
1.Vite
2.Volar插件
Volar 重大更新:改名为Vue - Official;支持Vue3.4;Take over模式被弃用;TypeScript Vue Plugin被弃用;
3.响应式基础
ref的自动解包
为ref和reactive标注类型的方法
ref和reactive的区别
怎么选择ref和reactive
4.计算属性
5.watch监视
计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。
在组合式 API 中,我们可以使用 watch
函数在每次响应式状态发生变化时触发回调函数:
1 | <script setup> |
侦听数据源类型
watch
的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:
1 | const x = ref(0) |
注意,你不能直接侦听响应式对象的属性值,例如:
1 | const obj = reactive({ count: 0 }) |
这里需要用一个返回该属性的 getter 函数:
1 | // 提供一个 getter 函数 |
深层侦听器
直接给 watch()
传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发:
1 | const obj = reactive({ count: 0 }) |
相比之下,一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调:
1 | watch( |
你也可以给上面这个例子显式地加上 deep
选项,强制转成深层侦听器:
1 | watch( |
谨慎使用
深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。
即时回调的侦听器
watch
默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。举例来说,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。
我们可以通过传入 immediate: true
选项来强制侦听器的回调立即执行:
1 | watch( |
一次性侦听器
每当被侦听源发生变化时,侦听器的回调就会执行。如果希望回调只在源变化时触发一次,请使用 once: true
选项。
1 | watch( |
watchEffect()
侦听器的回调使用与源完全相同的响应式状态是很常见的。例如下面的代码,在每当 todoId
的引用发生变化时使用侦听器来加载一个远程资源:
1 | const todoId = ref(1) |
特别是注意侦听器是如何两次使用 todoId
的,一次是作为源,另一次是在回调中。
我们可以用 watchEffect
函数 来简化上面的代码。watchEffect()
允许我们自动跟踪回调的响应式依赖。上面的侦听器可以重写为:
1 | watchEffect(async () => { |
这个例子中,回调会立即执行,不需要指定 immediate: true
。在执行期间,它会自动追踪 todoId.value
作为依赖(和计算属性类似)。每当 todoId.value
变化时,回调会再次执行。有了 watchEffect()
,我们不再需要明确传递 todoId
作为源值。
你可以参考一下这个例子的 watchEffect
和响应式的数据请求的操作。
对于这种只有一个依赖项的例子来说,watchEffect()
的好处相对较小。但是对于有多个依赖项的侦听器来说,使用 watchEffect()
可以消除手动维护依赖列表的负担。此外,如果你需要侦听一个嵌套数据结构中的几个属性,watchEffect()
可能会比深度侦听器更有效,因为它将只跟踪回调中被使用到的属性,而不是递归地跟踪所有的属性。
watchEffect
仅会在其同步执行期间,才追踪依赖。在使用异步回调时,只有在第一个 await
正常工作前访问到的属性才会被追踪。
watch
vs. watchEffect
watch
和 watchEffect
都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:
watch
只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch
会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。watchEffect
,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。
回调的触发时机
当你更改了响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。
类似于组件更新,用户创建的侦听器回调函数也会被批量处理以避免重复调用。例如,如果我们同步将一千个项目推入被侦听的数组中,我们可能不希望侦听器触发一千次。
默认情况下,侦听器回调会在父组件更新 (如有) 之后、所属组件的 DOM 更新之前被调用。这意味着如果你尝试在侦听器回调中访问所属组件的 DOM,那么 DOM 将处于更新前的状态。
Post Watchers
如果想在侦听器回调中能访问被 Vue 更新之后的所属组件的 DOM,你需要指明 flush: 'post'
选项:
1 | watch(source, callback, { |
后置刷新的 watchEffect()
有个更方便的别名 watchPostEffect()
:
1 | import { watchPostEffect } from 'vue' |
同步侦听器
你还可以创建一个同步触发的侦听器,它会在 Vue 进行任何更新之前触发:
1 | watch(source, callback, { |
同步触发的 watchEffect()
有个更方便的别名 watchSyncEffect()
:
1 | import { watchSyncEffect } from 'vue' |
6.事件处理
监听事件
我们可以使用 v-on
指令 (简写为 @
) 来监听 DOM 事件,并在事件触发时执行对应的 JavaScript。用法:v-on:click="handler"
或 @click="handler"
。
事件处理器 (handler) 的值可以是:
- 内联事件处理器:事件被触发时执行的内联 JavaScript 语句 (与
onclick
类似)。 - 方法事件处理器:一个指向组件上定义的方法的属性名或是路径。
内联事件处理器
内联事件处理器通常用于简单场景,例如:
1 | const count = ref(0) |
1 | <button @click="count++">Add 1</button> |
方法事件处理器
随着事件处理器的逻辑变得愈发复杂,内联代码方式变得不够灵活。因此 v-on
也可以接受一个方法名或对某个方法的调用。
举例来说:
1 | const name = ref('Vue.js') |
1 | <!-- `greet` 是上面定义过的方法名 --> |
方法事件处理器会自动接收原生 DOM 事件并触发执行。在上面的例子中,我们能够通过被触发事件的 event.target.tagName
访问到该 DOM 元素。
你也可以看看为事件处理器标注类型这一章了解更多。
方法与内联事件判断
模板编译器会通过检查 v-on
的值是否是合法的 JavaScript 标识符或属性访问路径来断定是何种形式的事件处理器。举例来说,foo
、foo.bar
和 foo['bar']
会被视为方法事件处理器,而 foo()
和 count++
会被视为内联事件处理器。
在内联处理器中调用方法
除了直接绑定方法名,你还可以在内联事件处理器中调用方法。这允许我们向方法传入自定义参数以代替原生事件:
1 | function say(message) { |
1 | <button @click="say('hello')">Say hello</button> |
事件修饰符
在处理事件时调用 event.preventDefault()
或 event.stopPropagation()
是很常见的。尽管我们可以直接在方法内调用,但如果方法能更专注于数据逻辑而不用去处理 DOM 事件的细节会更好。
为解决这一问题,Vue 为 v-on
提供了事件修饰符。修饰符是用 .
表示的指令后缀,包含以下这些:
.stop
.prevent
.self
.capture
.once
.passive
1 | <!-- 单击事件将停止传递 --> |
使用修饰符时需要注意调用顺序,因为相关代码是以相同的顺序生成的。因此使用 @click.prevent.self
会阻止元素及其子元素的所有点击事件的默认行为,而 @click.self.prevent
则只会阻止对元素本身的点击事件的默认行为。
.capture
、.once
和 .passive
修饰符与原生 addEventListener
事件相对应:
1 | <!-- 添加事件监听器时,使用 `capture` 捕获模式 --> |
.passive
修饰符一般用于触摸事件的监听器,可以用来改善移动端设备的滚屏性能。
7.表单输入绑定
在前端处理表单时,我们常常需要将表单输入框的内容同步给 JavaScript 中相应的变量。手动连接值绑定和更改事件监听器可能会很麻烦:
1 | <input |
v-model
指令帮我们简化了这一步骤:
1 | <input v-model="text"> |
另外,v-model
还可以用于各种不同类型的输入,<textarea>
、<select>
元素。它会根据所使用的元素自动使用对应的 DOM 属性和事件组合:
- 文本类型的
<input>
和<textarea>
元素会绑定value
property 并侦听input
事件; <input type="checkbox">
和<input type="radio">
会绑定checked
property 并侦听change
事件;<select>
会绑定value
property 并侦听change
事件。
修饰符
.lazy
默认情况下,v-model
会在每次 input
事件后更新数据 (IME 拼字阶段的状态例外)。你可以添加 lazy
修饰符来改为在每次 change
事件后更新数据:
1 | <!-- 在 "change" 事件后同步更新而不是 "input" --> |
.number
如果你想让用户输入自动转换为数字,你可以在 v-model
后添加 .number
修饰符来管理输入:
1 | <input v-model.number="age" /> |
如果该值无法被 parseFloat()
处理,那么将返回原始值。
number
修饰符会在输入框有 type="number"
时自动启用。
.trim
如果你想要默认自动去除用户输入内容中两端的空格,你可以在 v-model
后添加 .trim
修饰符:
1 | <input v-model.trim="msg" /> |
8.生命周期钩子
每个 Vue 组件实例在创建时都需要经历一系列的初始化步骤,比如设置好数据侦听,编译模板,挂载实例到 DOM,以及在数据改变时更新 DOM
注册周期钩子
举例来说,onMounted
钩子可以用来在组件完成初始渲染并创建 DOM 节点后运行代码:
1 | <script setup> |
还有其他一些钩子,会在实例生命周期的不同阶段被调用,最常用的是 onMounted
、onUpdated
和 onUnmounted
。所有生命周期钩子的完整参考及其用法请参考 API 索引。
当调用 onMounted
时,Vue 会自动将回调函数注册到当前正被初始化的组件实例上。这意味着这些钩子应当在组件初始化时被同步注册。例如,请不要这样做:
1 | setTimeout(() => { |
注意这并不意味着对 onMounted
的调用必须放在 setup()
或 <script setup>
内的词法上下文中。onMounted()
也可以在一个外部函数中调用,只要调用栈是同步的,且最终起源自 setup()
就可以。
生命周期图示
9.ref
虽然 Vue 的声明性渲染模型为你抽象了大部分对 DOM 的直接操作,但在某些情况下,我们仍然需要直接访问底层 DOM 元素。要实现这一点,我们可以使用特殊的 ref
attribute:
1 | <input ref="input"> |
ref
是一个特殊的 attribute,和 v-for
章节中提到的 key
类似。它允许我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用。这可能很有用,比如说在组件挂载时将焦点设置到一个 input 元素上,或在一个元素上初始化一个第三方库。
访问模板引用
为了通过组合式 API 获得该模板引用,我们需要声明一个匹配模板 ref attribute 值的 ref:
1 | <script setup> |
如果不使用 <script setup>
,需确保从 setup()
返回 ref:
1 | export default { |
注意,你只可以在组件挂载后才能访问模板引用。如果你想在模板中的表达式上访问 input
,在初次渲染时会是 null
。这是因为在初次渲染前这个元素还不存在呢!
如果你需要侦听一个模板引用 ref 的变化,确保考虑到其值为 null
的情况:
1 | watchEffect(() => { |
组件上的 ref
模板引用也可以被用在一个子组件上。这种情况下引用中获得的值是组件实例:
1 | <script setup> |
如果一个子组件使用的是选项式 API 或没有使用 <script setup>
,被引用的组件实例和该子组件的 this
完全一致,这意味着父组件对子组件的每一个属性和方法都有完全的访问权。这使得在父组件和子组件之间创建紧密耦合的实现细节变得很容易,当然也因此,应该只在绝对需要时才使用组件引用。大多数情况下,你应该首先使用标准的 props 和 emit 接口来实现父子组件交互。
有一个例外的情况,使用了 <script setup>
的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup>
的子组件中的任何东西,除非子组件在其中通过 defineExpose
宏显式暴露:
1 | <script setup> |
当父组件通过模板引用获取到了该组件的实例时,得到的实例类型为 { a: number, b: number }
(ref 都会自动解包,和一般的实例一样)。
10 组件基础
定义一个组件
当使用构建步骤时,我们一般会将 Vue 组件定义在一个单独的 .vue
文件中,这被叫做单文件组件 (简称 SFC):
1 | <script setup> |
当不使用构建步骤时,一个 Vue 组件以一个包含 Vue 特定选项的 JavaScript 对象来定义:
1 | import { ref } from 'vue' |
这里的模板是一个内联的 JavaScript 字符串,Vue 将会在运行时编译它。你也可以使用 ID 选择器来指向一个元素 (通常是原生的 <template>
元素),Vue 将会使用其内容作为模板来源。
上面的例子中定义了一个组件,并在一个 .js
文件里默认导出了它自己,但你也可以通过具名导出在一个文件中导出多个组件。
使用组件。
要使用一个子组件,我们需要在父组件中导入它。假设我们把计数器组件放在了一个叫做 ButtonCounter.vue
的文件中,这个组件将会以默认导出的形式被暴露给外部。
1 | <script setup> |
通过 <script setup>
,导入的组件都在模板中直接可用。
当然,你也可以全局地注册一个组件,使得它在当前应用中的任何组件上都可以使用,而不需要额外再导入。关于组件的全局注册和局部注册两种方式的利弊,我们放在了组件注册这一章节中专门讨论。
组件可以被重用任意多次:
1 | <h1>Here is a child component!</h1> |
你会注意到,每当点击这些按钮时,每一个组件都维护着自己的状态,是不同的 count
。这是因为每当你使用一个组件,就创建了一个新的实例。
在单文件组件中,推荐为子组件使用 PascalCase
的标签名,以此来和原生的 HTML 元素作区分。虽然原生 HTML 标签名是不区分大小写的,但 Vue 单文件组件是可以在编译中区分大小写的。我们也可以使用 />
来关闭一个标签。
如果你是直接在 DOM 中书写模板 (例如原生 <template>
元素的内容),模板的编译需要遵从浏览器中 HTML 的解析行为。在这种情况下,你应该需要使用 kebab-case
形式并显式地关闭这些组件的标签。
1 | <!-- 如果是在 DOM 中书写该模板 --> |
请看 DOM 内模板解析注意事项了解更多细节。
传递 props
如果我们正在构建一个博客,我们可能需要一个表示博客文章的组件。我们希望所有的博客文章分享相同的视觉布局,但有不同的内容。要实现这样的效果自然必须向组件中传递数据,例如每篇文章标题和内容,这就会使用到 props。
Props 是一种特别的 attributes,你可以在组件上声明注册。要传递给博客文章组件一个标题,我们必须在组件的 props 列表上声明它。这里要用到 defineProps
宏:
1 | <!-- BlogPost.vue --> |
defineProps
是一个仅 <script setup>
中可用的编译宏命令,并不需要显式地导入。声明的 props 会自动暴露给模板。defineProps
会返回一个对象,其中包含了可以传递给组件的所有 props:
1 | const props = defineProps(['title']) |
TypeScript 用户请参考:为组件 props 标注类型
如果你没有使用 <script setup>
,props 必须以 props
选项的方式声明,props 对象会作为 setup()
函数的第一个参数被传入:
1 | export default { |
一个组件可以有任意多的 props,默认情况下,所有 prop 都接受任意类型的值。
当一个 prop 被注册后,可以像这样以自定义 attribute 的形式传递数据给它:
1 | <BlogPost title="My journey with Vue" /> |
在实际应用中,我们可能在父组件中会有如下的一个博客文章数组:
1 | const posts = ref([ |
这种情况下,我们可以使用 v-for
来渲染它们:
1 | <BlogPost |
留意我们是如何使用 v-bind
来传递动态 prop 值的。当事先不知道要渲染的确切内容时,这一点特别有用。
监听事件
让我们继续关注我们的 <BlogPost>
组件。我们会发现有时候它需要与父组件进行交互。例如,要在此处实现无障碍访问的需求,将博客文章的文字能够放大,而页面的其余部分仍使用默认字号。
在父组件中,我们可以添加一个 postFontSize
ref 来实现这个效果:
1 | const posts = ref([ |
在模板中用它来控制所有博客文章的字体大小:
1 | <div :style="{ fontSize: postFontSize + 'em' }"> |
然后,给 <BlogPost>
组件添加一个按钮:
1 | <!-- BlogPost.vue, 省略了 <script> --> |
这个按钮目前还没有做任何事情,我们想要点击这个按钮来告诉父组件它应该放大所有博客文章的文字。要解决这个问题,组件实例提供了一个自定义事件系统。父组件可以通过 v-on
或 @
来选择性地监听子组件上抛的事件,就像监听原生 DOM 事件那样:
1 | <BlogPost |
子组件可以通过调用内置的 $emit
方法,通过传入事件名称来抛出一个事件:
1 | <!-- BlogPost.vue, 省略了 <script> --> |
因为有了 @enlarge-text="postFontSize += 0.1"
的监听,父组件会接收这一事件,从而更新 postFontSize
的值。
我们可以通过 defineEmits
宏来声明需要抛出的事件:
1 | <!-- BlogPost.vue --> |
这声明了一个组件可能触发的所有事件,还可以对事件的参数进行验证。同时,这还可以让 Vue 避免将它们作为原生事件监听器隐式地应用于子组件的根元素。
和 defineProps
类似,defineEmits
仅可用于 <script setup>
之中,并且不需要导入,它返回一个等同于 $emit
方法的 emit
函数。它可以被用于在组件的 <script setup>
中抛出事件,因为此处无法直接访问 $emit
:
1 | <script setup> |
如果你没有在使用 <script setup>
,你可以通过 emits
选项定义组件会抛出的事件。你可以从 setup()
函数的第二个参数,即 setup 上下文对象上访问到 emit
函数:
1 | export default { |
11.插槽
通过插槽来分配内容
一些情况下我们会希望能和 HTML 元素一样向组件中传递内容:
1 | <AlertBox> |
我们期望能渲染成这样:
This is an Error for Demo Purposes
Something bad happened.
这可以通过 Vue 的自定义 <slot>
元素来实现:
1 | <template> |
如上所示,我们使用 <slot>
作为一个占位符,父组件传递进来的内容就会渲染在这里。
12.自定义hook函数
什么是hook?—— 本质是一个函数,把setup函数中使用的Composition API进行了封装。
类似于vue2.x中的mixin。
自定义hook的优势: 复用代码, 让setup中的逻辑更清楚易懂。
举个例子:
1 | import { onMounted, onBeforeUnmount, isRef, watch, unref } from 'vue' |
13.Composition API
shallowReactive 与 shallowRef
shallowReactive : 只处理了对象内最外层属性的响应式(也就是浅响应式)
shallowRef: 只处理了value的响应式, 不进行对象的reactive处理
什么时候用浅响应式呢?
- 一般情况下使用ref和reactive即可
- 如果有一个对象数据, 结构比较深, 但变化时只是外层属性变化 ==> shallowReactive
- 如果有一个对象数据, 后面会产生新的对象来替换 ==> shallowRef
ref 的 内部
shallowRef的 内部
1 | <template> |
readonly 与 shallowReadonly
- readonly:
- 深度只读数据
- 获取一个对象 (响应式或纯对象) 或 ref 并返回原始代理的只读代理。
- 只读代理是深层的:访问的任何嵌套 property 也是只读的。
- shallowReadonly
- 浅只读数据
- 创建一个代理,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换
- 应用场景:
- 在某些特定情况下, 我们可能不希望对数据进行更新的操作, 那就可以包装生成一个只读代理对象来读取数据, 而不能修改或删除
1 | <template> |
toRaw 与 markRaw
- toRaw
- 返回由
reactive
或readonly
方法转换成响应式代理的普通对象。 - 这是一个还原方法,可用于临时读取,访问不会被代理/跟踪,写入时也不会触发界面更新。
- 返回由
- markRaw
- 标记一个对象,使其永远不会转换为代理。返回对象本身
- 应用场景:
- 有些值不应被设置为响应式的,例如复杂的第三方类实例或 Vue 组件对象。
- 当渲染具有不可变数据源的大列表时,跳过代理转换可以提高性能。
1 | <template> |
toRef
- 为源响应式对象上的某个属性创建一个 ref对象, 二者内部操作的是同一个数据值, 更新时二者是同步的
- 区别ref: 拷贝了一份新的数据值单独操作, 更新时相互不影响
- 应用: 当要将 某个prop 的 ref 传递给复合函数时,toRef 很有用
1 | <template> |
customRef
- 创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制
- 需求: 使用 customRef 实现 debounce也就是防抖的示例
1 | <template> |
在Vue.js中,customRef
是一个新的响应式引用工具,它允许您创建自定义的响应式引用,并在其读取和写入时执行自定义逻辑。这个功能是在Vue.js 3.1.0版本中引入的。
通常情况下,在Vue中,响应式引用是通过ref
函数创建的,它会将普通的JavaScript值转换为响应式引用。而customRef
允许您更灵活地定义响应式引用的行为。
使用customRef
,您可以在引用被读取或写入时执行自定义的逻辑。例如,您可以在读取时执行惰性计算,或在写入时执行一些额外的验证或副作用操作。
下面是customRef
的基本用法:
1 | import { customRef } from 'vue'; |
在上面的示例中,customRef
接受一个工厂函数作为参数,该工厂函数返回一个对象,包含get
和set
方法。get
方法用于读取引用时执行的逻辑,set
方法用于写入引用时执行的逻辑。在这两个方法中,您可以执行自定义的逻辑,并使用track
和trigger
函数告诉Vue要追踪或重新计算引用的依赖。
总之,customRef
提供了一种灵活的方式来创建自定义的响应式引用,并在其读取和写入时执行自定义的逻辑。
provide 与 inject
- provide
和
inject提供依赖注入,功能类似 2.x 的
provide/inject - 实现跨层级组件(祖孙)间通信
1 | <template> |
1 | <template> |
1 | <template> |
响应式数据的判断
- isRef: 检查一个值是否为一个 ref 对象
- isReactive: 检查一个对象是否是由
reactive
创建的响应式代理 - isReadonly: 检查一个对象是否是由
readonly
创建的只读代理 - isProxy: 检查一个对象是否是由
reactive
或者readonly
方法创建的代理
14.渲染函数
在绝大多数情况下,Vue 推荐使用模板语法来创建应用。然而在某些使用场景下,我们真的需要用到 JavaScript 完全的编程能力。
基本用法
创建 Vnodes
Vue 提供了一个 h()
函数用于创建 vnodes:
1 | import { h } from 'vue' |
h()
是 hyperscript 的简称——意思是“能生成 HTML (超文本标记语言) 的 JavaScript”。这个名字来源于许多虚拟 DOM 实现默认形成的约定。一个更准确的名称应该是 createVnode()
,但当你需要多次使用渲染函数时,一个简短的名字会更省力。
h()
函数的使用方式非常的灵活:
1 | // 除了类型必填以外,其他的参数都是可选的 |
1 | const vnode = h('div', { id: 'foo' }, []) |
声明渲染函数
当组合式 API 与模板一起使用时,setup()
钩子的返回值是用于暴露数据给模板。然而当我们使用渲染函数时,可以直接把渲染函数返回:
1 | import { ref, h } from 'vue' |
在 setup()
内部声明的渲染函数天生能够访问在同一范围内声明的 props 和许多响应式状态。
除了返回一个 vnode,你还可以返回字符串或数组:
1 | export default { |
1 | import { h } from 'vue' |
在Vue 3中,模板的渲染逻辑已经从组件本身中分离出来,转移到了render
函数中。render
函数是一个返回VNode(虚拟节点)的函数,它描述了组件的结构和行为。
因此,为了将模板的渲染逻辑从setup()
函数中分离出来,并且确保setup()
函数只执行一次,我们需要将渲染逻辑封装在一个函数中,并将这个函数作为setup()
函数的返回值。这样做可以确保每次组件重新渲染时,都可以获取到最新的状态和数据。
如果setup()
函数直接返回一个值而不是一个函数,那么这个值将会被视为组件的初始渲染结果,并且不会随着组件状态的变化而更新。因此,为了实现动态的、响应式的渲染逻辑,需要将渲染逻辑封装在一个函数中,并在需要时调用这个函数来获取最新的渲染结果。
Vnodes 必须唯一
组件树中的 vnodes 必须是唯一的
1 | //错误示范: |
如果你真的非常想在页面上渲染多个重复的元素或者组件,你可以使用一个工厂函数来做这件事。比如下面的这个渲染函数就可以完美渲染出 20 个相同的段落:
1 | function render() { |
15.JSX / TSX
JSX 是 JavaScript 的一个类似 XML 的扩展,有了它,我们可以用以下的方式来书写代码:
1 | const vnode = <div>hello</div> |
在 JSX 表达式中,使用大括号来嵌入动态值:
1 | const vnode = <div id={dynamicId}>hello, {userName}</div> |
create-vue
和 Vue CLI 都有预置的 JSX 语法支持。
如果你之前使用过 JSX 语法,那么请注意 Vue 的 JSX 转换方式与 React 中 JSX 的转换方式不同,因此你不能在 Vue 应用中使用 React 的 JSX 转换。与 React JSX 语法的一些明显区别包括:
- 可以使用 HTML attributes 比如
class
和for
作为 props - 不需要使用className
或htmlFor
。 - 传递子元素给组件 (比如 slots) 的方式不同。
Vue 的类型定义也提供了 TSX 语法的类型推导支持。当使用 TSX 语法时,确保在 tsconfig.json
中配置了 "jsx": "preserve"
,这样的 TypeScript 就能保证 Vue JSX 语法转换过程中的完整性。
渲染函数案例
v-if
模板:
1 | <div> |
等价于使用如下渲染函数 / JSX 语法:
1 | h('div', [ok.value ? h('div', 'yes') : h('span', 'no')]) |
1 | <div>{ok.value ? <div>yes</div> : <span>no</span>}</div> |
v-for
模板:
1 | <ul> |
等价于使用如下渲染函数 / JSX 语法:
1 | h( |
1 | <ul> |
v-on
以 on
开头,并跟着大写字母的 props 会被当作事件监听器。比如,onClick
与模板中的 @click
等价。
1 | h( |
1 | <button |
事件修饰符
对于 .passive
、.capture
和 .once
事件修饰符,可以使用驼峰写法将他们拼接在事件名后面:
实例:
1 | h('input', { |
1 | <input |
对于事件和按键修饰符,可以使用 withModifiers
函数:
1 | import { withModifiers } from 'vue' |
1 | <div onClick={withModifiers(() => {}, ['self'])} /> |
组件
在给组件创建 vnode 时,传递给 h()
函数的第一个参数应当是组件的定义。这意味着使用渲染函数时不再需要注册组件了 —— 可以直接使用导入的组件:
1 | import Foo from './Foo.vue' |
1 | function render() { |
不管是什么类型的文件,只要从中导入的是有效的 Vue 组件,h
就能正常运作。
动态组件在渲染函数中也可直接使用:
1 | import Foo from './Foo.vue' |
1 | function render() { |
如果一个组件是用名字注册的,不能直接导入 (例如,由一个库全局注册),可以使用 resolveComponent()
来解决这个问题。
渲染插槽
在渲染函数中,插槽可以通过 setup()
的上下文来访问。每个 slots
对象中的插槽都是一个返回 vnodes 数组的函数:
1 | export default { |
等价 JSX 语法:
1 | // 默认插槽 |
传递插槽
向组件传递子元素的方式与向元素传递子元素的方式有些许不同。我们需要传递一个插槽函数或者是一个包含插槽函数的对象而非是数组,插槽函数的返回值同一个正常的渲染函数的返回值一样——并且在子组件中被访问时总是会被转化为一个 vnodes 数组。
1 | // 单个默认插槽 |
等价 JSX 语法:
1 | // 默认插槽 |
插槽以函数的形式传递使得它们可以被子组件懒调用。这能确保它被注册为子组件的依赖关系,而不是父组件。这使得更新更加准确及有效。
作用域插槽
为了在父组件中渲染作用域插槽,需要给子组件传递一个插槽。注意该插槽现在拥有一个 text
参数。该插槽将在子组件中被调用,同时子组件中的数据将向上传递给父组件。
1 | // 父组件 |
记得传递 null
以避免插槽被误认为 prop:
1 | // 子组件 |
等同于 JSX:
1 | <MyComponent>{{ |
内置组件
诸如 <KeepAlive>
、<Transition>
、<TransitionGroup>
、<Teleport>
和 <Suspense>
等内置组件在渲染函数中必须导入才能使用:
1 | import { h, KeepAlive, Teleport, Transition, TransitionGroup } from 'vue' |
v-model
v-model
指令扩展为 modelValue
和 onUpdate:modelValue
在模板编译过程中,我们必须自己提供这些 props:
1 | export default { |
自定义指令
可以使用 withDirectives
将自定义指令应用于 vnode:
1 | import { h, withDirectives } from 'vue' |
当一个指令是以名称注册并且不能被直接导入时,可以使用 resolveDirective
函数来解决这个问题。
模板引用
在组合式 API 中,模板引用通过将 ref()
本身作为一个属性传递给 vnode 来创建:
1 | import { h, ref } from 'vue' |
16.内置组件
<Teleport>
Teleport
是 Vue 3 中引入的一个新功能,它允许你将子组件的内容渲染到 DOM 中的任何位置,而不受父组件的限制。这在需要在 DOM 结构中进行复杂布局或处理特定的样式和事件时非常有用。
使用 Teleport
,你可以轻松地将子组件的内容渲染到 DOM 中的另一个位置,而不会打破 Vue 的响应性或组件结构。这在处理诸如模态框、对话框、下拉菜单等需要脱离正常文档流的组件时特别有用。
1 | <template> |
遮罩的实现
1 | .mask { |
<Suspense>
等待异步组件时渲染一些额外内容,让应用有更好的用户体验
使用步骤:
异步引入组件
1
2import {defineAsyncComponent} from 'vue'
const Child = defineAsyncComponent(()=>import('./components/Child.vue'))使用
Suspense
包裹组件,并配置好default
与fallback
1
2
3
4
5
6
7
8
9
10
11
12
13<template>
<div class="app">
<h3>我是App组件</h3>
<Suspense>
<template v-slot:default>
<Child/>
</template>
<template v-slot:fallback>
<h3>加载中.....</h3>
</template>
</Suspense>
</div>
</template>
17.异步组件
基本用法
在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。Vue 提供了 defineAsyncComponent
方法来实现此功能:
1 | import { defineAsyncComponent } from 'vue' |
如你所见,defineAsyncComponent
方法接收一个返回 Promise 的加载函数。这个 Promise 的 resolve
回调方法应该在从服务器获得组件定义时调用。你也可以调用 reject(reason)
表明加载失败。
ES 模块动态导入也会返回一个 Promise,所以多数情况下我们会将它和 defineAsyncComponent
搭配使用。类似 Vite 和 Webpack 这样的构建工具也支持此语法 (并且会将它们作为打包时的代码分割点),因此我们也可以用它来导入 Vue 单文件组件:
1 | import { defineAsyncComponent } from 'vue' |
最后得到的 AsyncComp
是一个外层包装过的组件,仅在页面需要它渲染时才会调用加载内部实际组件的函数。它会将接收到的 props 和插槽传给内部组件,所以你可以使用这个异步的包装组件无缝地替换原始组件,同时实现延迟加载。
与普通组件一样,异步组件可以使用 app.component()
全局注册:
1 | app.component('MyComponent', defineAsyncComponent(() => |
也可以直接在父组件中直接定义它们:
1 | <script setup> |
加载与错误状态
异步操作不可避免地会涉及到加载和错误状态,因此 defineAsyncComponent()
也支持在高级选项中处理这些状态:
1 | const AsyncComp = defineAsyncComponent({ |
如果提供了一个加载组件,它将在内部组件加载时先行显示。在加载组件显示之前有一个默认的 200ms 延迟——这是因为在网络状况较好时,加载完成得很快,加载组件和最终组件之间的替换太快可能产生闪烁,反而影响用户感受。
如果提供了一个报错组件,则它会在加载器函数返回的 Promise 抛错时被渲染。你还可以指定一个超时时间,在请求耗时超过指定时间时也会渲染报错组件。
搭配 Suspense 使用
异步组件可以搭配内置的 <Suspense>
组件一起使用,若想了解 <Suspense>
和异步组件之间交互,请参阅 `` 章节。****
18.函数式组件
函数式组件是一种定义自身没有任何状态的组件的方式。它们很像纯函数:接收 props,返回 vnodes。函数式组件在渲染过程中不会创建组件实例 (也就是说,没有 this
),也不会触发常规的组件生命周期钩子。
我们用一个普通的函数而不是一个选项对象来创建函数式组件。该函数实际上就是该组件的渲染函数。
函数式组件的签名与 setup()
钩子相同:
1 | function MyComponent(props, { slots, emit, attrs }) { |
大多数常规组件的配置选项在函数式组件中都不可用,除了 props
和 emits
。我们可以给函数式组件添加对应的属性来声明它们:
1 | MyComponent.props = ['value'] |
如果这个 props
选项没有被定义,那么被传入函数的 props
对象就会像 attrs
一样会包含所有 attribute。除非指定了 props
选项,否则每个 prop 的名字将不会基于驼峰命名法被一般化处理。
对于有明确 props
的函数式组件,attribute 透传的原理与普通组件基本相同。然而,对于没有明确指定 props
的函数式组件,只有 class
、style
和 onXxx
事件监听器将默认从 attrs
中继承。在这两种情况下,可以将 inheritAttrs
设置为 false
来禁用属性继承:
1 | MyComponent.inheritAttrs = false |
函数式组件可以像普通组件一样被注册和使用。如果你将一个函数作为第一个参数传入 h
,它将会被当作一个函数式组件来对待。
为函数式组件标注类型
函数式组件可以根据它们是否有命名来标注类型。在单文件组件模板中,Vue - Official 扩展还支持对正确类型化的函数式组件进行类型检查。
具名函数式组件
1 | import type { SetupContext } from 'vue' |
匿名函数式组件
1 | import type { FunctionalComponent } from 'vue' |