Vue3学习笔记


前言

本片笔记仅结合个人理解,记录本人所生疏/不了解的部分。

生命周期

  1. 初始化事件修饰符、生命周期
  2. beforeCreate:创建实例之前,数据代理还未开始,获取不到 dom 节点
  3. 初始化响应式系统,数据检测、数据代理
  4. created:创建实例之后,methods、data 等已被绑定至 vue 实例,dom 还未生成,获取不到 dom 节点
  5. 实例中是否有 template 选项:
    1. 有:使用 render 函数渲染 template
    2. 否:将挂载的 el 元素的 innerHtml 编译为 template
  6. beforeMount:组件挂载之前,虚拟 dom 已经生成,真实 dom 还未生成
  7. 将通过 template 编译好的 html 替换至挂载的 el(如 #app)的 innerHtml,即将虚拟 dom 转化为真实 dom
  8. mounted:真实 dom 已经生成,模板编译已完成,页面已经展示,可以获取到 dom 节点
  9. 当数据发生改变时:
    1. beforeUpdate:真实 dom 未被替换,页面未被更新,虚拟 dom 已经更新完毕
    2. 对比新旧不同,根据不同节点,虚拟 dom 渲染为真实 dom
    3. updated:dom 更新之后,修改后的页面已经被展示
  10. 当页面被卸载时,即 app.unmount() 被调用时,即完全销毁一个实例,解绑事件,清除和其他组件的链接
    1. beforeUnmount:还未进行销毁,data、methods 等仍可以被调用
    2. unmounted:实例销毁之后

组合式 api

组合式 api 被使用的位置为 setup 组件选项,其组件创建之前执行,其执行位置早于 beforeCreate

在 vue3 中,其取代了 beforeCreatecreated。因此,在 setUp 方法中,不存在 beforeCreatecreated 的钩子函数。

多个应用实例

文档地址

可通过 css 选择器针对页面上的多个元素挂载不同的 vue 实例

1
2
Vue.createApp( { /* 选项 */ } ).mount( "#countainer-1" );
Vue.createApp( { /* 选项 */ } ).mount( "#countainer-2" );

这对于只想使用 vue 控制一个大型项目中的一小部分模块来说,是非常有效的。

v-bind

1
2
3
4
const objectOfAttrs = {
id: 'container',
class: 'wrapper'
}

使用如下方式向 div 绑定 idclass

1
2

<div v-bind="objectOfAttrs"></div>

同名简写

文档地址

attribute 的名称与 JS 变量的名称相同,可以省略 attribute 的值

1
2
3
4

<div :id></div>
<!-- 等同于 -->
<div :id="id"></div>

被自动忽略的布尔型 Attr

文档地址

我们知道,在 html 中,对于一些诸如 disabled 这样的 html 中的布尔型 attribute
不论其被设置为什么值,只要该属性存在,就会被认为是 true

而在 vue 中,当我们为这种 attribute 绑定的值为 false时,该属性会在最终生成的 html 中被自动忽略。

1
2
3
4

<button :disabled="false">按钮</button>
<!-- 渲染结果 -->
<button>按钮</button>

受限的全局访问

文档地址

模板表达式的环境是被沙盒化处理的,仅能使用一些被 vue 定义允许使用的全局对象
自行在 window 上添加的对象是不可使用的。

可以通过 app.config.globalProperties 来手动添加供模板表达式使用。

1
2
// 设置一个全局变量 author,其值为 mari
app.config.globalProperties.author = "mari"
1
2
<!-- 将会被渲染为 mari -->
<div>{{ author }}</div>

需要注意的是,如果通过这种方法设置的全局属性与组件自己的属性存在冲突,则以后者为准

平日以为全局变量只能在 <script> 中通过 getCurrentInstance 获取,步骤繁琐还没用。原来一直都误解了(记录的原因)

以及记录下在 <script> 中获取全局变量的方式

1
const authorProp = getCurrentInstance()?.appContext.config.globalProperties.author;

指令动态参数

文档地址

使用 [] 来将指令参数设置为表达式。

1
2
3
4
<a v-bind:[attributeName]="url"> ... </a>

<!-- 简写 -->
<a :[attributeName]="url"> ... </a>

动态参数存在以下限制:

  • 仅允许为字符串null,当为 null 时,意为显式移除该绑定;
  • 不允许使用空格和引号等;
  • 使用内嵌模板即 template 属性时,避免使用大写字符,浏览器会强制转化为小写导致出错。.vue 单文件组件不受此限制。

模板调用函数

1
2
3
<span :title="toTitleDate(date)">
{{ formatDate(date) }}
</span

当在 template 中使用组件抛出的函数时,该函数应该产生任何副作用,如改变数据或触发异步操作。

这是因为绑定在表达式中的方法在组件每次更新时都会被重新调用。

class 绑定

可以给 :class 绑定一个数组来渲染多个 CSS class,且在数组中支持以嵌套对象方式来决定是否启用指定 class。

1
2

<div :class="[{ active: isActive }, errorClass]"></div>

style 绑定

允许为 :style 绑定一个包含多个样式对象的数组。他们将会被合并后渲染至同一个元素。

1
2

<div :style="[baseStyles, overridingStyles]"></div>

自动前缀

:style 中使用了需要浏览器特殊前缀的 CSS 属性时,Vue 会在运行时检查该属性是否支持在当前浏览器中使用。如果浏览器不支持某个属性,那么将尝试加上各个浏览器特殊前缀,以找到哪一个是被支持的。

样式多值

可以对同一个样式属性以数组形式提供多个值,数组将会渲染浏览器支持的最后一个值。

1
2

<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
2
3
4
const AuthorInfo = reactive( {
name: "AsukaMari",
age: 18
} );
1
<p v-for="( value, key, index ) in AuthorInfo">{{ index }}. {{ key }}: {{ value }}</p>

上面的 template 将会会被渲染为:

1
2
0. name: AsukaMari
1. age: 18

使用范围值

v-for 可以直接接受一个整数值,此时会基于 1 ~ 给定值 的范围重复多次,与普通 v-for 遍历数组一样接收两个参数:当前值
位置索引,当前值初始值从 1 开始而非 0

1
<p v-for="( value, index ) in 10">{{ index }}. {{ value }}</p>

替换渲染数组

存在如下直接将响应式对象的值替换为新数组的方式:

1
2
// `items` 是一个数组的 ref
items.value = items.value.filter( ( item ) => item.message.match( /Foo/ ) );

此时 Vue 并不会丢弃现有的 DOM 并重新渲染整个列表。Vue 实现了一些巧妙的方法来最大化对 DOM 元素的重用,因此用另一个包含部分重叠对象的数组来做替换,仍会是一种非常高效的操作。

v-if 与 v-for

当这俩存在于同一个元素上时,v-if 会被首先执行。

需要注意的是,这俩同时使用是不推荐的。

响应式相关

DOM 更新时机

更改响应式状态后,DOM 会自动更新。然而并不会立即更新,而是缓冲至更新周期的 “下个时机” 以确保无论进行多次修改,每个组件都只更新一次。

若要保证 DOM 更新完成再执行操作,可以使用 nextTick() 全局 API。

深层响应式与浅层响应式

Vue 3 中,refreactive 都是默认深层响应式的,即使改变深层次的对象或数组,改动也依然能被检测到。

而针对希望使用浅层响应式的场景,可以分别使用 shallowRefshallowReactive 来进行对应的包装。

1
2
3
4
5
6
7
// 深层响应式
ref( /* 内容 */ );
reactive( /* 内容 */ );

// 浅层响应式
shallowRef( /* 内容 */ );
shallowReactive( /* 内容 */ );

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
2
3
4
const objectRef = ref( { count: 0 } );

// 这是响应式的替换
objectRef.value = { count: 1 };

ref 被传递给函数或是从一般对象上被解构时,不会丢失响应性:

1
2
3
4
5
6
7
8
9
10
11
12
const obj = {
foo: ref( 1 ),
bar: ref( 2 )
};

// 该函数接收一个 ref
// 需要通过 .value 取值
// 但它会保持响应性
callSomeFunction( obj.foo );

// 仍然是响应式的
const { foo, bar } = 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
2
3
4
5
6
7
const count = ref( 0 );
const state = reactive( { count } );

console.log( state.count ); // 0

state.count = 1;
console.log( count.value ); // 1

需要注意的是,对于数组集合类型的响应式对象,其中的 ref 不会被自动解包。

1
2
3
4
5
6
7
const books = reactive( [ ref( "Vue 3 Guide" ) ] );
// 这里需要 .value
console.log( books[0].value );

const map = reactive( new Map( [ [ "count", ref( 0 ) ] ] ) );
// 这里需要 .value
console.log( map.get( "count" ).value );

toRefs

将对象解析为新的对象,该对象解构后不会失去响应式。

可以看作该方法对对象的每个属性进行了一次类似 ref() 的转化,将其转化为一个包含 value 属性的对象。

计算属性

计算属性返回了一个计算属性 ref,与其他一般的 ref 类似。

计算属性的 getter 中应该只进行计算,不应存在任何的副作用。使用会改变原数组的行为如 reverse()sort() 时应创建一个原数组的副本。

1
2
3
4
// 错误
return numbers.reverse()
// 正确
return [ ...numbers ].reverse()

应该避免在任何情况下修改计算属性返回值,该值应被视为已读的。

与方法的不同之处

虽然使用方法也能达到与计算属性同样的效果,但不同的是计算属性值会基于其响应式依赖被缓存

计算属性仅会在响应式依赖发生改变时才会重新计算,否则计算属性无论访问多少次都会立刻返回先前的计算结果,不必重复执行 getter 函数。

因此,对于非响应式依赖,计算属性永远都不会更新。例如下面示例中的 Date.now()

1
const now = computed( () => Date.now() );

相比之下,每次重渲染发生时方法总是会被再次执行。

可写的计算属性

计算属性默认是只读的,尝试修改计算属性时会收到一个运行时警告。但我们可以通过配置 computed option,提供 getter 和 setter 来创建一个可写的计算属性。

下面的案例中,fullName 在页面中的渲染将会在一秒后从 Asuka Mari 变更为 Tachibana Kanade

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const firstName = ref( "Asuka" );
const lastName = ref( "Mari" );

const fullName = computed( {
get() {
return `${ firstName.value } ${ lastName.value }`;
},
set( newValue ) {
// 当赋值时触发,修改了 getter 的响应式依赖,使得 getter 被重新计算
[ firstName.value, lastName.value ] = newValue.split( " " );
}
} );

setTimeout( () => {
fullName.value = "Tachibana Kanade";
}, 1000 );

事件处理

事件处理器分为内联事件处理器方法事件处理器

内联事件处理器接受一段 JavaScript 语句,与 onclick 类似。而方法事件处理器则接受一个方法名,指代对某个方法的调用。

1
2
3
4
<!-- 内联事件处理器 -->
<button @click="count++">Add 1</button>
<!-- 方法事件处理器 -->
<button @click="increment">Add 1</button>

需要注意的是,也是笔者在此记录的原因。即我们常用的向方法内传入一个参数的写法,根据写法不同,其本质为不同的事件处理器。

1
2
3
4
<!-- 内联事件处理器 -->
<button @click="increment( 1 )">Add 1</button>
<!-- 方法事件处理器 -->
<button @click="() => increment( 1 )">Add 1</button>

exact 修饰符

官方文档

exact 修饰符用于确保精确匹配,即只有在精确匹配时才会触发事件。

1
2
3
4
<!-- 只要按下 ctrl 时点击就能触发,不论是否同时还按有其他的键 -->
<div @click.ctrl="console.log(1)">login</div>
<!-- 仅在只按下 ctrl 时点击才能触发 -->
<div @click.ctrl.exact="console.log(1)">login</div>

表单输入绑定

IME(输入法)的影响

官方文档

输入框等使用需要使用 IME 的语言输入时,默认 v-model(非组件自定义) 并不会在拼字阶段触发更新。
若需要在拼字阶段触发,需要手动拆分为 value@input 的绑定。

选择器未匹配时的情况

官方文档

v-model 绑定的值未匹配到任何绑定值时,<select> 会被渲染为一个未选择的状态。而在 IOS 上,这会导致用户无法选择第一项。
此时建议提供一个空值的禁用选项。

1
2
3
4
5
<select v-model="selected">
<option disabled value="">请选择</option>
<option>A</option>
<option>B</option>
</select>

选择器选项的值

选择器的选项可以使用非字符串类型,例如引用类型

1
2
3
<select v-model="selected">
<option :value="{ number: 123 }">123</option>
</select>

当选择后,selected 会被设置为响应式对象类型 { number: 123 }

.lazy 修饰符

对于 <input> 输入框,默认情况下可以理解(因为 IME 拼字阶段并不会触发)其绑定的事件为 @input,即每次输入均会触发。
使用 .lazy 修饰符标记将其改为 @change 事件触发,这样将会在输入完毕失去焦点的时候更新数据。

1
<input v-model.lazy="msg" />

侦听器

watch

watch 第一个参数为监听数据源,允许的类型为:ref(包括计算属性)、响应式对象、getter 函数,多个数据源组成的数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const x = ref( 0 );
const y = ref( 0 );

// getter 函数
watch(
() => x.value + y.value,
( sum ) => {
console.log( `sum of x + y is: ${ sum }` );
}
);

// 多个来源组成的数组
watch( [ x, () => y.value ], ( [ newX, newY ] ) => {
console.log( `x is ${ newX } and y is ${ newY }` );
} );

需要注意的是,对于响应式对象的属性值,仅当该属性值为对象时可以直接作为数据源监听。否则即使其被 ref 包装也无法用于 watch 监听,因为 refreactive 中会被自动解包为普通类型。

1
2
3
4
5
6
7
8
9
10
11
const authorInfo = reactive( {
age: 18,
ageRef: ref( 18 ),
info: {
age: 18
}
} );

watch( authorInfo.age, () => {} ); // 无法监听
watch( authorInfo.ageRef, () => {} ); // 无法监听
watch( authorInfo.info, () => {} ); // 可以监听

而对于非对象的属性值,应使用 getter 的方式进行监听。

1
2
watch( () => authorInfo.age, () => {
} ); // 可以监听

深层监听

深层监听即:该回调函数在所有嵌套的变更时都会被触发。

当 watch 侦听的数据源为响应对象时,将会隐式的创建一个深层侦听器。

1
2
3
4
const obj = reactive( { count: 0 } );

// 深层监听
watch( obj, () => {} );

而当数据源为 getter 函数时,则只有在返回不同对象时才会触发回调,为浅层监听。此时若希望使用深层监听,需要开启 deep 选项。

1
2
3
4
// 浅层监听
watch( () => obj, () => {} );
// 深层监听
watch( () => obj, () => {}, { deep: true } );

由于深层监听需要遍历侦听对象中的所有嵌套属性,因此开销较大,若无必要需求请尽量使用 getter 的方式来实现浅层监听。

watchEffect

官方文档

如果我们只是想监控所用到的响应式对象,当状态发生变化时进行一些副作用操作,且在初次加载时就执行一次(这是很常见的一个需求),那使用 watch 来实现的话可能会有些繁琐了。
尤其在监听多个响应式对象时,我们可能会不得不写成如下几种方式。

1
2
3
4
5
6
7
8
9
10
11
watch( [ v1, v2 ], () => {
// do something ...
}, { immediate: true } );

watch( () => v1.value + v2.value, () => {
// do something ...
}, { immediate: true } );

watch( () => [ v1.value, v2.value ], () => {
// do something ...
}, { immediate: true } );

此时我们可以使用 watchEffect 来简化这个需求:

1
2
3
4
watchEffect( () => {
v1.value, v2.value;
// do something ...
} );

watchEffect 类似计算属性,只要回调函数内使用到的响应式对象发生变化,就会触发一次回调函数。且在组件初始化时会立即执行一次。

获取监听的对象具体属性值的新旧值

watch 在监听中存在如下情况:

1
2
3
4
5
6
7
8
9
const state = reactive( { test: 1 } );

watch( state, ( newVal, oldVal ) => {
console.log( newVal, oldVal );
} );

setTimeout( () => {
state.test = 2;
}, 1000 );

一秒后页面上打印结果如下:

1
Proxy {test: 2} Proxy {test: 2}

可以见到,新旧两值是完全相同的,watch 并不能对具体的属性值做出新旧值的记录,这时候可以使用 watchEffect

对于回调函数,watchEffect 存在一个参数,该参数为一个函数,用来注册清理回调。清理回调会在该副作用下一次执行前侦听器被清除时被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const state = reactive( { test: 1 } );

const stopWatch = watchEffect( ( cleanupFn ) => {
console.log( state );
cleanupFn( () => {
console.log( "执行清理回调", state );
} );
} );

setTimeout( () => {
state.test = 2;
// 停止侦听器,若不使用 nextTick 将会在触发响应之前就被清除
nextTick( stopWatch );
}, 1000 );

以上 demo 执行结果为

1
2
3
4
Proxy {test: 1}
// 一秒后
执行清理回调
Proxy {test: 2}

回调的触发时机

watchwatchEffect 均有对此的可选配置项。

默认情况下,被创建的侦听器回调都会在当前 Vue 组件更新之前被调用,此时访问的 DOM 将是被 Vue 更新之前的状态。

此行为可通过修改可选配置项中的 flush 进行修改,默认情况下,该值为 pre

1
2
3
4
5
6
7
watch( source, callback, {
flush: "post"
} );

watchEffect( callback, {
flush: "post"
} );

而无论是 pre 还是 post,watch 均会等待到下一个渲染周期(即 nextTick 被调用之前)执行回调函数,来起到效率提高与防抖的作用。

对于如下示例,控制台中将会仅打印一次 2

1
2
3
4
5
6
7
const a = ref( 0 );
watch( a, val => {
console.log( val )
} );

a.value = 1;
a.value = 2;

若期望在任何更新之前均同步触发回调函数,则可以使用 flush"sync" 可选值。不过由于这样影响效率并失去了防抖效果,通常情况下 vue 不建议使用此可选值。

对于 postsync,均有一个别名 api 可以直接使用:

1
2
3
4
watchPost( source, callback );
watchSync( source, callback );
watchEffectPost( callback );
watchEffectSync( callback );

watch 执行顺序

对于如下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const [ a, b, c ] = [ ref( 0 ), ref( 0 ), ref( 0 ) ];

watch( a, () => {
console.log( "a" )
} );

watch( b, () => {
console.log( "b" )
} );

watch( c, () => {
console.log( "c" )
} );

a.value = b.value = c.value = 1;

可以看到,终端打印顺序为 c b a。而使用 post 模式时,打印顺序又变为了正常的 a b c

先打一个 todo 在这里,具体原因需要翻阅源码查看

停止侦听器

setup 中使用同步语句创建的侦听器,会自动绑定到组件实例上,并在组件卸载时自动停止,因此无需关心怎样去停止它。

而当侦听器在异步回调中创建时,则不会绑定到当前组件上,为了防止内存泄漏,必须要手动停止它。

1
2
3
4
5
6
7
8
9
10
import { watchEffect } from 'vue'

// 它会自动停止
watchEffect( () => {
} )

// ...这个则不会!
setTimeout( () => {
watchEffect( () => {} );
}, 100 );

使用 watchwatchEffect 返回的函数来手动停止侦听器

1
2
3
4
5
const unwatch = watchEffect( () => {
} );

// 停止侦听器
unwatch();

DOM 内编写模板

官方文档

vue 在获取模板内容时,存在三种方式:单文件组件字符串模板原生 DOM 模板,这里主要讨论第三种。

当根组件没有设置 template 选项时,Vue 会自动使用容器的 innerHTML 作为模板,这就是 DOM 内模板。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!doctype html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="UTF-8" />
<title>DOM模板</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<mari-post v-for="title of postTitles" :title="title"></mari-post>
</div>
<script>
const mariPost = {
props: [ "title" ],
template: "<h1>{{ title }}</h1>"
};

Vue.createApp( {
components: {
"mari-post": mariPost
},
data() {
return {
postTitles: [ "60s新闻", "摸鱼日报" ]
}
}
} ).mount( "#app" );
</script>
</body>
</html>

组件相关

组件命名与书写规范

官方文档

默认情况下,vue 建议使用 PascalCase 形式来命名组件,这样可以与 HTML 元素区分开来。但在 DOM 中编写模板 时,则不得不遵守一些 HTML 的规则,其主要集中在三点:

不区分大小写

HTML 不区分大小写,若使用 PascalCasecamelCase 会因为无法匹配导致无法渲染组件,因此在模板中需要使用 kebab-case 形式来命名组件与 props。

1
2
3
4
<!-- 错误 -->
<BlogPost PostTitle="刀削面是什么"></BlogPost>
<!-- 正确 -->
<blog-post post-title="刀削面是什么"></blog-post>

不允许省略闭合标签

HTML 只允许一小部分特殊元素省略其关闭标签,因此模板中组件必须要手动添加闭合标签

1
2
3
4
<!-- 错误 -->
<blog-post />
<!-- 正确 -->
<blog-post></blog-post>

限制元素位置

HTML 存在元素位置限制,例如 table 元素只能包含 tr 元素,此时直接使用组件代替 tr 的位置会导致无法渲染。

此时可以使用 is 属性结合 vue:组件名 的方式来解决这个问题。

1
2
3
4
5
6
7
8
<!-- 错误 -->
<table>
<blog-post></blog-post>
</table>
<!-- 正确 -->
<table>
<tr is="vue:blog-post"></tr>
</table>

只有在 DOM 模板中才需要添加 vue: 前缀,正常单文件组件模板字符串模板中不需要

对于注册组件的组件名时,当并未使用 DOM 模板 时, 推荐使用 PascalCase,这代表这并非是一个 HTML 组件。

下面将仅针对组合式 api 的 setup 语法糖进行整理,需要注意的是 <script setup> 不能和 src attribute 一起使用。

组件注册

组件其实就是一个对象或者单文件组件,通过 App.component() 可以被注册为全局组件,而通过 components 选项可以注册为局部组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const App = {
components: {
// 局部注册
"component-a": {
// template、props、data 等选项,或一个单文件组件
}
}
};

const app = Vue.createApp( App );
// 全局注册
app.component( "component-b", {
// template、props、data 等选项,或一个单文件组件
} );

模板引用 ref

官方文档

v-for 中的模板引用

v-for 中使用模板引用 ref 时,对应 ref 的值为一个数组,其包含列表中的所有元素。

但需要注意的是,该数组并不能保证与 v-for 所遍历的数据保持一致的顺序。

函数模板引用

模板引用 ref 的值允许为一个函数,此时需要使用动态的 v-bind 绑定即 :ref,该函数将在被绑定的元素每次更新被卸载时调用。

函数接收一个参数,即被绑定的 DOM 元素(不是 vNode),当元素被卸载触发时,参数值为 null。参考如下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div v-if="state" :ref="divRef">测试</div>
</template>

<script setup>
const state = ref( false );

function testRef( el ) {
console.log( el );
}

setInterval( () => {
state.value = !state.value;
}, 1000 );
</script>

将会间隔一秒循环打印如下结果:

1
2
<div>测试</div>
null

组件 ref

vue2 不同的是,当组件使用了 setup 语法糖时,组件是默认私有的,此时父组件无法通过模板引用 ref 来访问到子组件的任何东西。

此时需要子组件通过 defineExpose
宏显式抛出,这类宏函数不需要显式引入,官网示例

props

官方文档

声明

在 setup 语法糖中,使用 defineProps 宏来进行 props 的声明,其参数与选项式 api 的 props 参数没有区别。该宏函数只能在 <script setup> 的顶级作用域下使用。

defineProps 宏函数默认支持自动通过参数推导类型,此时被称为运行时声明

1
2
3
4
5
6
7
const props = defineProps( {
name: { type: String, required: true },
age: Number
} );

props.name; // string
props.age; // number | undefined

可以通过泛型来更直接精确的为 props 定义类型,这被称为类型声明

1
2
3
4
const props = defineProps<{
name: string;
age?: number;
}>();

该类型定义可以使用现成的类型接口,但需要注意的是,暂时不支持外部导入类型的使用。

3.3 版本后开始支持外部类型导入,但不支持对类型整体的条件判定/泛型等复杂类型

1
2
3
4
5
6
7
8
9
10
11
12
// 正确
interface IProps {
name: string;
age?: number;
}

const props = defineProps<IProps>();

// 错误
import { IProps } from "type/common";

const props = defineProps<IProps>();

基于类型的声明并不拥有为 props 设定默认值的能力,此时需要额外的 withDefaults 宏来实现此能力:

1
2
3
4
5
6
7
8
9
interface IProps {
foo: string;
bar?: number;
}

const props = withDefaults( defineProps<IProps>(), {
name: "AsukaMari",
age: 18
} );

注:运行时声明与类型声明只能两者取其一,同时使用会报错,后面的 emits 的声明同理。

传递

对于 props 的传递,使用 PascalCase 并没有多大优势,为了和 HTML attribute 对齐,通常会将其写为 kebab-case 形式。

1
2

<MariPost author-name="AsukaMari" />

props 允许使用对象的方式来一次性传递多个 props,例如有如下数据:

1
2
3
4
const author = {
name: "AsukaMari",
age: 18
}

下面两种渲染方式是完全等价的:

1
2
3
4

<MariPost v-bind="author" />
<!-- 等价于 -->
<MariPost :name="author.name" :age="author.age" />

emits 声明

在 setup 语法糖中,使用 defineEmits 宏来进行 props 的声明,其同样存在 运行时声明类型声明 两种方式。该宏函数只能在 <script setup> 的顶级作用域下使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 运行时声明
const emit = defineEmits( [ "setName", "setAge" ] );

// 类型声明
const emit = defineEmits<{
( e: "setName", name: string ): void;
( e: "setAge", age: number ): void;
}>();

// 3.3+ 更简洁的类型声明
const emit = defineEmits<{
setName: [ name: string ];
setAge: [ age: number ];
}>();

props 与 emits 的名称格式转换

对于 v-onv-bind 绑定的名称,vue 会自动将其转换为 camelCase 形式,因此即使子组件中定义的是 camelCase 形式的名称,父组件中也可以使用 kebab-case 形式来绑定。

1
2
3
4
5
6
7
<!-- 父组件 -->
<TestData msg-data="测试内容" @test-click="console.log( 114514 )" />
<!-- 子组件 -->
<script setup>
defineProps<{ msgData: string }>();
defineEmits<{ ( e: "testClick" ): void }>();
</script>

v-model

默认情况下,v-model 在组件上使用 modelValue 作为 prop,并以 update:modelValue 作为对应的事件。在自定义组件上使用 v-model 时,将会被解析为如下形式:

1
2

<CustomInput :modelValue="searchText" @update:modelValue="newValue => searchText = newValue" />

可以通过给 v-model 指定参数来更改默认行为:

1
2
3
4

<MariPost v-model:name="authorName" />
<!-- 等价于 -->
<MariPost :name="authorName" @update:name="newValue => authorName = newValue" />

介于指定参数的特性,我们得以为单个组件使用多个 v-model,彼此之前不会发生冲突:

1
<MariPost v-model="value" v-model:name="authorName" v-model:age="authorAge" />

自定义 v-model 修饰符

可以为 v-model 指定自定义修饰符,例如指定 .upper 来将值自动转为大写:

1
<MariPost v-model.upper="value" />

它将会为组件传递一个名为 modelModifiers 的 props,其默认值为一个空对象。此时在该对象中存在 upper 属性,值为 true

1
2
3
4
5
6
7
8
9
10
11
const props = withDefaults( defineProps<{
modelValue: string;
modelModifiers: {
upper?: boolean;
}
}>(), {
modelValue: "",
modelModifiers: () => ( {} )
} );

console.log( props.modelModifiers ); // { upper: true }

我们可以根据此值来进行一些我们所需要的操作,如这里的自动转为大写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<script setup>
import { computed } from "vue";

const props = withDefaults( defineProps<{
modelValue: string;
modelModifiers: {
upper?: boolean;
}
}>(), {
modelValue: "",
modelModifiers: () => ( {} )
} );

const emit = defineEmits<{
( e: "update:modelValue", name: string ): void;
}>();

const value = computed( {
get() {
return props.modelValue;
},
set( val ) {
if ( props.modelModifiers.upper ) {
val = val.toUpperCase();
}
emit( "update:modelValue", val );
}
} )
</script>

<template>
<input type="text" v-model="value">
</template>

而对于 v-model 指定了参数如 v-model:name.upper,自动生成的 props 名称将从 modelModifiers 改为 arg + "Modifiers",在这里为 nameModifiers

1
2
3
4
5
6
7
8
9
10
11
const props = withDefaults( defineProps<{
name: string;
nameModifiers: {
upper?: boolean;
}
}>(), {
name: "",
nameModifiers: () => ( {} )
} );

console.log( props.nameModifiers ); // { upper: true }

defineModel

鉴于上面的实现方式过于复杂,从 Vue 3.4 版本开始,Vue 提供了 defineModel() 宏来便捷的实现 v-model

本质上还是 moduleValueupdate:modelValue 那一套的语法糖,其使用方式与 3.4- 的对照如下:

1
2
3
4
5
6
7
8
9
10
11
// 声明
const val = defineModel(); // 3.4 +
// 太麻烦了就不贴出来了,反正上面有写 // 3.4 -

// 触发 model 值更改
val.value = "test"; // 3.4+
emit( "update:modelValue", "test" ); // 3.4-

// 使用 model 值
val.value; // 3.4+
props.modelValue; // 3.4-

而当我们期望其接受一个参数时,实现也非常简单:

1
2
// 组件上可以直接使用 v-model:name=""
const nameVal = defineModel<string>( "name" );

defineModel 还支持接受一个配置对象,该对象为 PropOptionsprops 配置项) 与 DefineModelOptionsgetset 函数) 两个类型的集合。
其中 getset 函数与常规取值函数完全一致:读 model 值时触发 get(),修改 model 值时触发 set()

手动修改 get() 的返回值仅会影响读取获取的值,并不会影响到 model 本身的值。

1
2
3
4
5
6
7
8
9
10
11
12
const val = defineModel<string>( {
required: true,
dafault: "",
set( val ) {
return val;
},
get( val ) {
return val;
}
} );
// 对于自定义参数,将配置项至于第二项即可
const nameVal = defineModel<string>( "name", { required: true } );

而对于自定义 v-model 修饰符,可以通过对 defineModel 的返回值进行解构。
其中我们可以通过TS 泛型的第二个参数来标明可选的修饰符,多个可选修饰符使用联合类型标记:

1
2
3
const [ val, modifiers ] = defineModel<string, "upper" | "lower">();
// 以 v-model.upper 为例
console.log( modifiers ); // { upper: true }

为此,我们可以很容易的简化 自定义 v-model 修饰符 中将输入值转为大写的例子:

1
2
3
4
5
6
// 二十多行代码简化成五行,赞美
const [ val, modifiers ] = defineModel<string, "upper">( {
set( value ) {
return modifiers.upper ? value.toUpperCase() : value;
}
} );

透传 Attributes

文档地址

单根元素组件

对于以单个元素为根节点渲染的组件,当为它传递其并未声明为 propsemits 的属性时,
将会被直接添加到根元素上,这就是属性透传。属性如 styleclassplaceholder 等,v-on 事件同样支持透传。

对于 styleclass,当子组件已存在该值时,将会与父组件传递的值合并。

当子组件 A 根节点为另一个子组件 B 时,将会继续透传至 B 子组件。需要注意的是,若 A 中以 propsemits 的形式声明了该属性,则该属性会被 A 吃掉,不会继续向 B 透传。

禁用透传

在 vue 3.3 之前,可以通过设置 defineComponent() 所接受对象的 inheritAttrsfalse 来禁用透传。

而由于 <script setup> 语法糖不需要使用 defineComponent(),啧需要额外定义一个 script 标签来进行此属性的声明,同时这两个 <script> 标签必须使用相同的语言,如 lang="ts"

1
2
3
4
5
6
7
8
9
10
11
<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent( {
inheritAttrs: false
} );
</script>

<script lang="ts" setup>
// ...setup 逻辑
</script>

而在 3.3 之后,我们可以直接使用 defineOptions 宏来进行声明。

1
2
3
4
5
6
<script lang="ts" setup>
defineOptions( {
inheritAttrs: false
} );
// ...setup 逻辑
</script>

多根元素组件

对于存在多个根元素的组件,不会自动触发属性透传。此时若传递了未声明的 propsemits,将会得到一个 vue 的警告:

1
2
3
[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.
at <MariPost name="" onUpdate:name=fn nameModifiers= {upper: true} ... >
at <App>

使用 $attrs 指定透传位置

当自动透传被禁止时(inheritAttrs: false 或为多根元素组件),可以使用 $attrs 来自行指定透传位置。

1
2
3
4
5

<template>
<p>这是一个输入框:</p>
<input type="text" v-bind="$attrs" v-model="value">
</template>

此时当多根元素组件传递未声明的 propsemits 时,不会出现警告。

这个 $attrs 包含了除手动声明的 propsemits 之外的所有的被附加在组件上的 attribute,包括 v-on 监听器。

有几点需要注意一下:

  • $attrs 中保留了透传 attr 的原始大小写,像 author-name 这样的 props 只能使用 $attrs['author-name'] 来获取,这点与组件 props 不同
  • 对于诸如 @click 这样的 v-on 绑定的事件,被暴露为 $attrs 上的一个名为 onClick 的属性,其他事件同理。

在 JavaScript 中访问透传属性

<script setup> 中可以使用 useAttrs() 来访问所有的透传属性。

1
2
3
4
import { computed, useAttrs } from "vue";

const attrs = useAttrs();
console.log( attrs ); // Proxy(Object) {placeholder: '请输入', __vInternal: 1}

或在 setup() 的上下文对象中获取。

1
2
3
4
5
6
7
import { defineComponent } from "vue";

export default defineComponent( {
setup( props, ctx ) {
console.log( ctx.attrs ); // Proxy(Object) {placeholder: '请输入', __vInternal: 1}
}
} );

需要注意的是,为了节省性能,attrs 并不是响应式的。

插槽 slot

文档地址

具名插槽

对于具名插槽,需要使用一个含指定名称 v-slot 指令的 <template> 标签,默认插槽无需使用。

当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非 <template> 节点都被隐式地视为默认插槽的内容。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<MariPost>
<template #name>
<p>AsukaMari</p>
</template>
<template #default>
<p>默认插槽内容</p>
</template>
</MariPost>
<!-- 等价为 -->
<MariPost>
<template #name>
<p>AsukaMari</p>
</template>
<p>默认插槽内容</p>
</MariPost>

条件插槽

vue3 提供了一个表示父组件所传入的插槽的对象:$slots,其类型定义如下:

1
2
3
interface ComponentPublicInstance {
$slots: { [name: string]: Slot }
}

对象键值仅包含父组件所使用到的插槽名,借此,我们可以结合 v-if 来实现根据插槽是否存在来渲染对应内容。

1
2
3
4
5
6
7
8
9
10
<template>
<div v-if="$slots.author">
<h3>author 插槽:</h3>
<slot name="author"></slot>
</div>
<div v-if="$slots.banner">
<h3>banner 插槽:</h3>
<slot name="banner"></slot>
</div>
</template>

动态插槽名

插槽允许设置动态指令参数,格式为:

1
2
3
4
5
<MariPost>
<template #[diySlotName]>
...
</template>
</MariPost>

插槽上的 name 是一个保留属性,不会作为 props 传递给插槽。下面的具名作用域插槽实际接收到的内容为 { author: "AsukaMari" }

1
<slot name="header" author="AsukaMari"></slot>

defineSlots

文档地址

通过 defineSlots() 可以对组建的插槽手动定义 TypeScript 类型,包括所有插槽的名称作用域插槽所提供的 props 内容

其接受一个类型字面量。字面量键名为插槽 name;字面量键值为一个函数类型,接受一个 props 参数,用于标记所提供的 props 内容。

其返回值与手动调用 useSlot() 的结果一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 子组件 -->
<script setup lang="ts">
import { useSlots } from "vue";

const slots = defineSlots<{
default(): any;
header( props: { author: string } ): any;
}>();

// 两者一致
console.log( slots === useSlots() ); // true
</script>

<template>
<slot />
<slot name="header" author="AsukaMari"></slot>
</template>

使用 defineSlots() 注明类型后,在子组件插槽中定义 props 与在父组件中使用时均会获得类型提示。

插槽复用

同一个 name 的插槽可以多次使用,并支持传入不同的作用域属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 子组件 MariPost -->
<div>
<slot name="post" author="mari"></slot>
<slot name="post" author="seto"></slot>
</div>
<!-- 父组件 -->
<MariPost>
<template #post="{ author }">
<p>{{ author }}</p>
</template>
</MariPost>

<!-- 渲染结果 -->
<div>
<p>mari</p>
<p>seto</p>
</div>

依赖注入

在跨组件通讯时,若传递的组件层数过多,操作会变得非常繁琐且难以维护。为此 vue 提供了 provideinject 两个 api 解决这个问题。

1
2
3
4
5
6
7
8
// 父级组件
import { provide } from "vue";
provide( "provide-data", "mari" );

// 后代组件
import { inject } from "vue";
const data = inject( "provide-data" );
console.log( data ); // "mari"

后代组件使用 inject 时,必须确保相关 key 值的 provide 已经被调用,这就要求两个组件必须为祖先后代关系

默认值

当父级组件未提供 provie() 时,子组件直接通过 inject() 尝试获取会得到一个 vue 警告:

1
[Vue warn]: injection "xxx" not found.

此时我们可以通过为 inject() 设置一个默认值来解决这个问题。

1
const data = inject( "provide-data", "default-value" );

有的时候,我们需要提供一个诸如实例化类的默认值。此时即时该默认值未被使用到,也会不可避免地进行一次实例化,造成性能损耗。

这时我们可以使用工厂函数来创建默认值,此时需要传递第三个参数为 true,即告诉 inject() 默认值为一个工厂函数,否则会被当作普通方法处理。

1
const data = inject( "provide-data", () => new Data(), true );

禁止修改

尽管后代组件是无法修改通过 inject() 获取到的值的,但如果 provide() 传递的是一个引用类型的值(对象数组、响应式对象),此时后代组件扔然可以修改其中的属性。

1
2
3
4
5
6
7
8
9
// 父级组件
const data = { name: "mari" };
provide( "provide-data", data );
setTimeout( () => {
console.log( data ); // { name: "seto" }
}, 2000 );
// 后代组件
const data = inject( "provide-data" );
data.name = "seto";

上面例子可以发现,data 被后代组件从 { name: "mari" } 修改为了 { name: "seto" }。为了避免这种情况,
我们可以使用 readonly 来防止修改。

1
2
3
import { readonly, provide } from "vue";
const data = { name: "mari" };
provide( "provide-data", readonly( data ) );

此时后代组件再尝试修改属性是,会得到一个 vue 警告:

1
[Vue warn] Set operation on key "name" failed: target is readonly.

TypeScript 类型标记

Vue 提供了一个 InjectionKey 类型来供我们标记 provide()inject() 的 key 值的类型。
使用这个类型的 key 值被提供给 provide()inject() 使用时,将会获得相应的类型提示与限制。

1
2
3
4
5
6
7
8
9
10
11
// provideType.ts
import type { InjectionKey } from "vue";
export const provideKey = Symbol() as InjectionKey<string>;

// 祖先组件
import type { provideKey } from "./provideType";
provide( provideKey, "mari" ); // 这里将只能提供 string 类型的值

// 后代组件
import type { provideKey } from "./provideType";
const data = inject( provideKey ); // data 的类型为 string | undefined

该类型同样会对 inject() 的默认值做出约束,当 inject() 提供默认值时,得到的数据类型中将不再包含 undefined

1
2
3
// 后代组件
import type { provideKey } from "./provideType";
const data = inject( provideKey, "seto" ); // data 的类型为 string

需要注意的是,处于未知原因,目前并不会对工厂函数方式的默认值进行校验

当使用字符串类型的 key 时,需要手动通过泛型的方式为 inject() 定义类型:

1
2
3
4
// 祖先组件
provide( "stringKey", "mari" );
// 后代组件
const data = inject<string>( "stringKey" ); // data 的类型为 string | undefined

异步组件

文档地址

开发中我们可能会遇到一些诸如从远程获取的组件,vue 提供了 defineAsyncComponent() 来实现这种异步组件的加载。

1
2
3
4
5
const AsyncComp = defineAsyncComponent( () => {
return new Promise( resolve => {
reslove( /* 组件内容 */ );
} );
} );

我们可以通过定时器来模拟一个异步组件的导入,例如下面的示例中,组件 AsyncComp 将在五秒后被渲染到页面上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import { defineAsyncComponent } from "vue";

const AsyncComp = defineAsyncComponent( () => {
return new Promise( resolve => {
setTimeout( () => {
import( "@/views/login/AsyncComp.vue" ).then( resolve );
}, 5000 );
} )
} );
</script>

<template>
<AsyncComp />
</template>

利用这个特性,我们还可以用来实现组件的按需加载,每个模块只有用到它了才会加载,避免无效性能开销。
我们可以结合 import() 返回 Promise 对象的特点来实现这个功能,

1
const AsyncComp = defineAsyncComponent( () => import( "@/views/login/AsyncComp.vue" ) );

加载与错误状态

既然是异步,那就永远都离不开等待潜在的报错问题
事实上,为了处理这两种情况 defineAsyncComponent() 提供了更高级的可选配置项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { defineAsyncComponent } from "vue";

const AsyncComp = defineAsyncComponent( {
// 等效于上面接受的函数
loader: () => import( "@/views/login/AsyncComp.vue" ),
// 组件未加载完成前显示的组件
loadingComponent: LoadingComponent,
// 组件加载出错时显示的组件
errorComponent: ErrorComponent,
// 展示加载组件之前的延迟,默认 200ms,为了避免组件高速切换的闪烁问题
delay: 200,
// 超时时间,默认 Infinity,触发超时一样会显示 errorComponent
timeout: 3000,
} );

为此我编写了一个小案例,项目演示地址

组合式函数

官方文档

组合式函数说白了就是拆分出来的公共逻辑,但可以使用 vue 的响应式 api,并只允许在 setup 中使用。

其有几个约定规范:

  • 命名:使用 useXXX 的格式命名
  • 参数:可以接收参数,包括响应式参数。为此可以使用 toValue() 来处理传递进来的参数。
  • 返回值:始终返回一个包含多个 ref 的非响应式对象

自定义指令

官方文档

vue 官方建议:仅在所需功能只能使用 DOM 操作来实现时才使用自定义指令,来获得更高效、对 SSR 更友好的应用。

注册

<script setup> 内部时,只要是以 v 开头的驼峰式命名的变量,均可以被用作一个自定义指令

1
2
3
const vFocus = {
mounted: el => el.focus()
};

而在不能使用 <script setup> 时,例如选项式 api 写法时,则需要使用 directives 选项来注册:

1
2
3
4
5
6
7
8
9
10
export default {
setup() {
// ...
},
directives: {
focus: {
mounted: el => el.focus()
}
}
};

在应用层级上全局注册也是一种常见的需求:

1
2
3
4
const app = createApp( App );
app.directive( "focus", {
mounted: el => el.focus()
} );

对于以上三种方式,均可以直接在模板上使用 v-focus 指令,来实现元素的自动聚焦效果:

1
2
3
<template>
<input v-focus />
</template>

简写

通常情况下,自定义指令仅需要在 mountedupdated 两个钩子上实现相同行为,其他钩子并不需要。
如果满足这样的需求,可以使用自定义指令的简写方式:

1
2
3
4
5
6
7
8
// 原写法
vFocus = {
mounted: el => el.focus(),
updated: el => el.focus()
};

// 简写
vFocus = el => el.focus()

在组件上使用

在组件上使用时,与透传 attributes 类似,会被直接用于根节点。但自定义指令并没有 $attrs 那样的可以自行指定作用区域的对象。
因此自定义指令不能被应用到多根元素的组件,此时指令会被忽略,且 vue 会抛出一个警告。

api 扫盲

isRef()

文档地址

接收一个任意参数,返回 boolean 类型。用于检验目标是否是 ref

1
2
isRef( ref( "test" ) ); // true
isRef( { value: "test" } ); // false(蒙混过关是不可能滴)

由于其返回值是一个 is 类型判定,因此可用作类型过滤。

unref()

文档地址

参数是 ref,就返回它的 value 值,反之返回本体。其实就是针对下面操作的一个语法糖:

1
2
3
value = isRef( refValue ) ? refValue.value : refValue;
// 等价于
value = unref( refValue );

toValue()

文档地址

unref() 类似,但 toValue() 还会规范化 getter 函数。
当接受一个 getter 函数时,toValue() 会将其执行并返回结果,若返回结果为响应式对象,toValue() 并不会将其解构。

1
2
3
toValue( ref( "test" ) ); // "test"
toValue( () => "test" ); // "test"
toValue( () => ref( "test" ) ); // RefImpl { _value: "test" }

性能优化

Props 稳定性

文档地址

在 Vue 中,子组件只会在其至少一个 props被改变时才会发生更新。例如有以下示例:

1
2
3
4
<ListItem
v-for="item in list"
:id="item.id"
:active-id="activeId" />

在这个示例中,它使用了 idactive-id 来让组件在内部自行判断自己是否为当前激活项。这样固然没有问题,且从开发层面上是比较直观的。
但这样会导致每次 activeId 改变时,所有的 ListItem 的 props 都发生了变化,导致所有 ListItem 都会重新渲染更新。

而理想情况下我们只需要让当前激活项重新渲染,因此我们可以略微修改,仅让当前激活项的 props 发生变化:

1
2
3
4
<ListItem
v-for="item in list"
:id="item.id"
:active="item.id === activeId" />

这样使得状态比对逻辑被移入了父组件,牺牲了一定的开发和可读性体验,但换来了更好的性能。

v-once

对于上面这种情况,有一些元素自身依赖运行时数据,但它后续无需再进行更新。此时可以使用 v-once 指令修饰,被修饰的整个子树都将会在未来的更新中被跳过。

1
2
3
4
<!-- 元素 -->
<span v-once>{{ msg }}</span>
<!-- 组件 -->
<ChildComponent v-once :msg="msg" />