生命周期
- 初始化事件修饰符、生命周期
beforeCreate
:创建实例之前,数据代理还未开始,获取不到 dom 节点- 初始化响应式系统,数据检测、数据代理
created
:创建实例之后,methods、data 等已被绑定至 vue 实例,dom 还未生成,获取不到 dom 节点- 实例中是否有 template 选项:
- 有:使用 render 函数渲染 template
- 否:将挂载的 el 元素的 innerHtml 编译为 template
beforeMount
:组件挂载之前,虚拟 dom 已经生成,真实 dom 还未生成- 将通过 template 编译好的 html 替换至挂载的 el(如 #app)的 innerHtml,即将虚拟 dom 转化为真实 dom
mounted
:真实 dom 已经生成,模板编译已完成,页面已经展示,可以获取到 dom 节点- 当数据发生改变时:
beforeUpdate
:真实 dom 未被替换,页面未被更新,虚拟 dom 已经更新完毕- 对比新旧不同,根据不同节点,虚拟 dom 渲染为真实 dom
updated
:dom 更新之后,修改后的页面已经被展示
- 当页面被卸载时,即
app.unmount()
被调用时,即完全销毁一个实例,解绑事件,清除和其他组件的链接beforeUnmount
:还未进行销毁,data、methods 等仍可以被调用unmounted
:实例销毁之后
组合式 api
组合式 api 被使用的位置为 setup
组件选项,其组件创建之前执行,其执行位置早于 beforeCreate
。
在 vue3 中,其取代了 beforeCreate
与 created
。因此,在 setUp 方法中,不存在 beforeCreate
与 created
的钩子函数。
模板语法
模板中的表达式仅能够访问到有限的全局对象列表。
没有显式包含在列表中的全局对象将不能在模板内表达式中访问,例如用户附加在 window 上的属性。
可以通过自行在 app.config.globalProperties
上显式地添加它们,供所有的 Vue 表达式使用。
v-bind
1 | const objectOfAttrs = { |
使用如下方式向 div 绑定 id
与 class
。
1 | <div v-bind="objectOfAttrs"></div> |
指令动态参数
使用 []
来将指令参数设置为表达式。
1 | <a v-bind:[attributeName]="url"> ... </a> |
动态参数存在以下限制:
- 仅允许为字符串*或
null
; - 不允许使用空格和引号等;
- 使用内嵌模板即
template
属性时,避免使用大写字符,浏览器会强制转化为小写导致出错。.vue
单文件组件不受此限制。
模板调用函数
1 | <span :title="toTitleDate(date)"> |
当在 template 中使用组件抛出的函数时,该函数不应该产生任何副作用,如改变数据或触发异步操作。
这是因为绑定在表达式中的方法在组件每次更新时都会被重新调用。
class 绑定
可以给 :class
绑定一个数组来渲染多个 CSS class,且在数组中支持以嵌套对象方式来决定是否启用指定 class。
1 | <div :class="[{ active: isActive }, errorClass]"></div> |
style 绑定
允许为 :style
绑定一个包含多个样式对象的数组。他们将会被合并后渲染至同一个元素。
1 | <div :style="[baseStyles, overridingStyles]"></div> |
自动前缀
在 :style
中使用了需要浏览器特殊前缀的 CSS 属性时,Vue 会在运行时检查该属性是否支持在当前浏览器中使用。如果浏览器不支持某个属性,那么将尝试加上各个浏览器特殊前缀,以找到哪一个是被支持的。
样式多值
可以对同一个样式属性以数组形式提供多个值,数组将会渲染浏览器支持的最后一个值。
1 | <div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div> |
在上面示例中,在支持不需要特别前缀的浏览器中都会渲染为 display: flex
。
v-if
v-if 是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。
v-for
遍历对象
除了遍历数组外,还可以使用 v-for
对一个对象的所有属性进行遍历,此时存在三个参数:
- 参数 1:对象的键值
- 参数 2:对象的键名
- 参数 3:位置索引
遍历的顺序会基于该对象的 Object.keys()
的返回值。
1 | const AuthorInfo = reactive({ |
1 | <p v-for="( value, key, index ) in AuthorInfo">{{ index }}. {{ key }}: {{ value }}</p> |
上面的 template 将会会被渲染为:
1 | 0. name: AsukaMari |
使用范围值
v-for
可以直接接受一个整数值,此时会基于 1~给定值 的范围重复多次,与普通 v-for 遍历数组一样接收两个参数:当前值与位置索引,当前值初始值从 1
开始而非 0
。
1 | <p v-for="( value, index ) in 10">{{ index }}. {{ value }}</p> |
替换渲染数组
存在如下直接替换将响应式对象的值替换为新数组的方式:
1 | // `items` 是一个数组的 ref |
此时 Vue 并不会丢弃现有的 DOM 并重新渲染整个列表。Vue 实现了一些巧妙的方法来最大化对 DOM 元素的重用,因此用另一个包含部分重叠对象的数组来做替换,仍会是一种非常高效的操作。
v-if 与 v-for
当这俩存在于同一个元素上时,v-if
会被首先执行。
需要注意的是,这俩同时使用是不推荐的。
响应式相关
DOM 更新时机
更改响应式状态后,DOM 会自动更新。然而并不会立即更新,而是缓冲至更新周期的 “下个时机” 以确保无论进行多次修改,每个组件都只更新一次。
若要保证 DOM 更新完成再执行操作,可以使用 nextTick()
全局 API。
深层响应式
Vue 3 中,状态都是默认深层响应式的,及时改变深层次的对象或数组,改动也依然能被检测到。
reactive()
reactive()
返回的是原始对象的 Proxy 代理对象(没错就是 ES6 的 Proxy),同时响应式对象的深层嵌套对象也依然是代理对象。
为了保证访问代理的一致性,存在如下规则:
- 当对同一个对象多次执行
reactive()
时,返回同一个 Proxy 对象; - 当对对象的 Proxy 代理对象执行
reactive()
时,返回其自身;
局限性
由于 JavaScript 的引用机制,reactive 存在些许局限性。
reactive()
仅对对象类型有效,即 对象、数组和 Map、Set 这样的集合类型。- 不能随意的修改对响应式对象的引用,如将响应式对象指向其他的变量、作为函数实参传入、解构赋值,均会失去原变量的响应式。
ref()
在 vue3 中,通过 ref()
方法来将变量转化为响应式对象,该对象包含一个 value
属性指向赋予响应时对象的值,它可以创建任意类型的响应式 ref,弥补了 reactive()
对类型的限制。。
这么做的用意是为了保证 JavaScript 中不同数据类型的行为统一。
我们知道 JavaScript 分为引用类型和基本类型,而对象作为引用类型,使用对象进行包装基本类型后可以安全的为其传递值而不用担心失去响应式的问题。
当 ref 对象的 .value
为对象类型时,将会用 reactive()
自动转换该值。
相比 reactive() 的优越性
一个包含对象类型值的 ref 可以响应式的替换整个对象。
1 | const objectRef = ref({ count: 0 }); |
ref 被传递给函数或是从一般对象上被解构时,不会丢失响应性:
1 | const obj = { |
在 template 中的解包
当 ref 在 template 中作为顶层属性被使用时,会被自动 “解包”,不需要使用 .value
。
而对于非顶层属性,则不会被自动解包,如下面的 object:
1 | const obj = { foo: ref(1) }; |
对于如下 template,不会正常工作,将得到 [object Object]1
1 | <span>{{ obj.foo + 1 }}</span> |
应手动补齐 .value
才能得到正确结果 2
1 | <span>{{ obj.foo.value + 1 }}</span> |
而当文本插值即 {{ }}
内表达式的计算最终值为一个 ref 时,它同样将会被自动解包,这是文本插值的一个遍历功能。
如对于上面的 obj,如下 template 写法等同于 {{ obj.foo.value }}
,最终渲染结果为 1
,。
1 | <span>{{ obj.foo }}</span> |
在响应式对象中的解包
当 ref 被套在响应式对象中,作为属性访问并修改时,会被自动解包,像一般属性一样不需要 .value
1 | const count = ref(0); |
需要注意的是,对于数组和集合类型的响应式对象,其中的 ref 不会被自动捷豹。
1 | const books = reactive([ref('Vue 3 Guide')]); |
toRefs
将对象解析为新的对象,该对象结构后不会失去响应式。
可以看作该方法对对象的每个属性进行了一次类似 ref()
的转化,将其转化为一个包含 value
属性的对象。
计算属性
计算属性返回了一个计算属性 ref,与其他一般的 ref 类似。
计算属性的 getter 中应该只进行计算,不应存在任何的副作用。使用会改变原数组的行为如 reverse()
和 sort()
时应创建一个原数组的副本。
1 | // 错误 |
应该避免在任何情况下修改计算属性返回值,该值应被视为已读的。
与方法的不同之处
虽然使用方法也能达到与计算属性同样的效果,但不同的是计算属性值会基于其响应式依赖被缓存。
计算属性仅会在的响应式依赖发生改变时才会重新计算,否则计算属性无论访问多少次都会立刻返回先前的计算结果,不必重复执行 getter 函数。
因此,对于非响应式依赖,计算属性永远都不会更新。例如下面示例中的 Date.now()
:
1 | const now = computed( () => Date.now() ); |
相比之下,每次重渲染发生时方法总是会被再次执行。
可写的计算属性
计算属性默认是只读的,尝试修改计算属性时会收到一个运行时警告。但我们可以通过配置 computed option,提供 getter 和 setter 来创建一个可写的计算属性。
下面的案例中,fullName 在页面中的渲染将会在一秒后从 Asuka Mari
变更为 Tachibana Kanade
。
1 | const firstName = ref("Asuka"); |
侦听器
watch
watch 第一个参数为监听数据源,允许的类型为:ref(包括计算属性)、响应式对象、getter 函数,多个数据源组成的数组。
1 | const x = ref(0); |
需要注意的是,对于响应式对象的属性值,仅当该属性值为对象时可以直接作为数据源监听。否则即使其被 ref
包装也无法用于 watch 监听,因为 ref
在 reactive
中会被自动解包为普通类型。
1 | const authorInfo = reactive({ |
而对于非对象的属性值,应使用 getter 的方式进行监听。
1 | watch(() => authorInfo.age, () => {}); // 可以监听 |
深层监听
深层监听即:该回调函数在所有嵌套的变更时都会被触发。
当 watch 侦听的数据源为响应对象时,将会隐式的创建一个深层侦听器。
1 | const obj = reactive({ count: 0 }); |
而当数据源为 getter 函数时,则只有在返回不同对象时才会触发回调,为浅层监听。此时若希望使用深层监听,需要开启 deep
选项。
1 | // 浅层监听 |
由于深层监听需要遍历侦听对象中的所有嵌套属性,因此开销较大,若无必要需求请尽量使用 getter 的方式来实现浅层监听。
watchEffect
详细内容可参考 watchEffect。
watch 在监听中存在如下情况:
1 | const state = reactive( { test: 1 } ); |
一秒后页面上打印结果如下:
1 | Proxy {test: 2} Proxy {test: 2} |
可以见到,新旧两值是完全相同的,watch 并不能对具体的属性值做出新旧值的记录,这时候可以使用 watchEffect
。
watchEffect 在组件初始化时会立即运行一次回调函数,它会响应式地追踪回调函数中所依赖的响应式属性,并在依赖更改时重新执行回调函数。
对于回调函数,它还存在一个参数,该参数为一个函数,用来注册清理回调。清理回调会在该副作用下一次执行前与侦听器被清除时被调用。
1 | const state = reactive( { test: 1 } ); |
以上 demo 执行结果为
1 | 1 |
回调的触发时机
watch 与 watchEffect 均有对此的可选配置项。
默认情况下,被创建的侦听器回调都会在 Vue 组件更新之前被调用,此时访问的 DOM 将是被 Vue 更新之前的状态。
此行为可通过修改可选配置项中的 flush
进行修改,默认情况下,该值为 pre
。
1 | watch( source, callback, { |
而无论是 pre
还是 post
,watch 均会等待到下一个渲染周期(即 nextTick
被调用之前)执行回调函数,来起到效率提高与防抖的作用。
对于如下示例,控制台中将会仅打印一次 2
。
1 | const a = ref(0); |
若期望更改时即立刻调用回调函数,则可以使用 flush
的 "sync"
可选值。不过由于这样影响效率并失去了防抖效果,通常情况下 vue 不建议使用此可选值。
watch 执行顺序
对于如下示例:
1 | const [a, b, c] = [ref(0), ref(0), ref(0)]; |
可以看到,终端打印顺序为 c b a
。而使用 post
模式时,打印顺序又变为了正常的 a b c
。
先打一个 todo 在这里,具体原因需要翻阅源码查看
停止侦听器
在 setup
中使用同步语句创建的侦听器,会自动绑定到组件实例上,并在组件卸载时自动停止,因此无需关心怎样去停止它。
而当侦听器在异步回调中创建时,则不会绑定到当前组件上,为了防止内存泄漏,必须要手动停止它。
1 | import { watchEffect } from 'vue' |
使用 watch
与 watchEffect
返回的函数来手动停止侦听器
1 | const unwatch = watchEffect(() => {}); |
组件相关
对于注册组件的组件名时,当并未使用 DOM 模板 时,
推荐使用 PascalCase,这代表这并非是一个 HTML 组件。
下面将仅针对组合式 api 的 setup
语法糖进行整理,需要注意的是 <script setup>
不能和 src
attribute 一起使用。
模板引用 ref
v-for 中的模板引用
在 v-for
中使用模板引用 ref 时,对应 ref 的值为一个数组,其包含列表中的所有元素。
但需要注意的是,该数组并不能保证与 v-for 所遍历的数据保持一致的顺序。
函数模板引用
模板引用 ref 的值允许为一个函数,此时需要使用动态的 v-bind
绑定即 :ref
,该函数将在每次组件更新与组件被卸载时调用。
组件 ref
与 vue2
不同的是,当组件使用了 setup
语法糖时,组件是默认私有的,此时父组件无法通过模板引用 ref 来访问到子组件的任何东西。
此时需要子组件通过 defineExpose
宏显式抛出,这类宏函数不需要显式引入,官网示例。
props
声明
在 setup 语法糖中,使用 defineProps
宏来进行 props 的声明,其参数与选项式 api 的 props 参数没有区别。
defineProps
宏函数默认支持自动通过参数推导类型,此时被称为运行时声明。
1 | const props = defineProps({ |
可以通过泛型来更直接精确的为 props 定义类型,这被称为类型声明:
1 | const props = defineProps<{ |
该类型定义可以使用现成的类型接口,但需要注意的是,暂时不支持外部导入类型的使用。
1 | // 正确 |
基于类型的声明并不拥有为 props 设定默认值的能力,此时需要额外的 withDefaults
宏来实现此能力:
1 | interface IProps { |
注:运行时声明与类型声明只能两者取其一,同时使用会报错,后面的 emits 的声明同理。
传递
对于 props 的传递,使用 PascalCase 并没有多大优势,为了和 HTML attribute 对齐,通常会将其写为 kebab-case 形式。
1 | <AsukaMari author-name="AsukaMari" /> |
props 允许使用对象的方式来一次性传递多个 props,例如有如下数据:
1 | const author = { |
下面两种渲染方式是完全等价的:
1 | <AsukaMari v-bind="author" /> |
emits 声明
在 setup 语法糖中,使用 defineEmits
宏来进行 props 的声明,其同样存在 运行时声明
与 类型声明
两种方式。
1 | // 运行时声明 |
v-model
默认情况下,v-model
在组件上使用 modelValue
作为 prop,并以 update:modelValue
作为对应的事件。在自定义组件上使用 v-model 时,将会被解析为如下形式:
1 | <CustomInput :modelValue="searchText" @update:modelValue="newValue => searchText = newValue" /> |
可以通过给 v-model
指定参数来更改默认行为:
1 | <AsukaMari v-model:name="authorName" /> |
介于指定参数的特性,我们得以为单个组件使用多个 v-model
,彼此之前不会发生冲突:
1 | <AsukaMari v-model="value" v-model:name="authorName" v-model:age="authorAge" /> |
自定义 v-model 修饰符
可以为 v-model 指定自定义修饰符,例如指定 .upper
来将值自动转为大写:
1 | <AsukaMari v-model.upper="value" /> |
它将会为组件传递一个名为 modelModifiers
的 props,其默认值为一个空对象。此时在该对象中存在 upper
属性,值为 true
。
1 | const props = withDefaults(defineProps<{ |
我们可以根据此值来进行一些我们所需要的操作,如这里的自动转为大写:
template:
1 | <template> |
script:
1 | import {computed} from "vue"; |
而对于 v-model 指定了参数如 v-model:name.upper
,自动生成的 props 名称将从 modelModifiers
改为 arg + "Modifiers"
,在这里为 nameModifiers
。
1 | const props = withDefaults(defineProps<{ |
透传 Attributes
单根元素组件
对于以单个元素为根节点渲染的组件,当为它传递其并未声明为 props
或 emits
的属性时,将会被直接添加到根元素上,这就是属性透传。属性如 style
、class
、placeholder
等,v-on
事件同样支持透传。
对于 style
和 class
,当子组件已存在该值时,将会与父组件传递的值合并。
当子组件 A 根节点为另一个子组件 B 时,将会继续透传至 B 子组件。需要注意的是,若 A 中以 props
或 emits
的形式声明了该属性,则该属性会被 A 吃掉
,不会继续向 B 透传。
禁用透传
可以通过 inheritAttrs: false
禁用透传。
1 | import { defineComponent } from "vue"; |
需要注意的是,对于
<script setup>
语法糖,需要额外定义一个script
标签来进行此属性的声明,同时这两个<script>
标签必须使用相同的语言,如lang="ts"
。
多根元素组件
对于存在多个根元素的组件,不会自动触发属性透传。此时若传递了未声明的 props
或 emits
,将会得到一个 vue 的警告:
1 | [Vue warn]: Extraneous non-props attributes (placeholder) were passed to component but could not be automatically inherited because component renders fragment or text root nodes. |
使用 $attrs
指定透传位置
当自动透传被禁止时(inheritAttrs: false
或为多根元素组件),可以使用 $attrs
来自行指定透传位置。
1 | <template> |
此时当多根元素组件传递未声明的 props
或 emits
时,不会出现警告。
在 JavaScript 中访问透传属性
在 <script setup>
中可以使用 useAttrs()
来访问所有的透传属性。
1 | import {computed, useAttrs} from "vue"; |
或在 setup()
的上下文对象中获取。
1 | import { defineComponent } from "vue"; |
需要注意的是,为了节省性能,
attrs
并不是响应式的。
插槽 slot
对于具名插槽,需要使用一个含指定名称 v-slot
指令的 <template>
标签,默认插槽无需使用。
当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非 <template>
节点都被隐式地视为默认插槽的内容。如下:
1 | <AsukaMari> |
插槽允许设置动态指令参数,格式为:
1 | <AsukaMari> |
插槽上的 name
是一个保留属性,不会作为 props 传递给插槽。下面的具名作用域插槽实际接收到的内容为 { author: "AsukaMari" }
1 | <slot name="header" author="AsukaMari"></slot> |