02.响应式
02.响应式
2.1 模板语法
- 模板语法(指令)是用于
template中的特殊属性,可以将js变量绑定到元素上,并实现根据数据变化驱动视图的更新- 这些模板会被编译为
render函数,运行时执行render函数生成虚拟DOM,并通过patch将差异同步到真实DOM,挂载到页面上 - 如果有响应式变化,实际上就是重新执行被跟踪到有相关依赖的
render函数,之后同步差异 - 因此模板的原理是通过编译,形成一些预先的信息,用于生成
render函数
- 这些模板会被编译为
2.1.1 v-text 绑定文本内容
v-text:最基本的绑定形式,对于一般的页面字符串可以使用{{}}获取变量的字符串值,也可以在标签上使用v-text属性(会使用给定的变量替换元素的内容)v-text和{{}}的原理实际上是通过解析其中的内容,作为js表达式执行获取值(表达式在编译模板时生成)- 运行时如果是
{{}}会将内容和其他静态内容拼接在一起,作为节点内容;如果是v-text则作为节点的内容,如果有子节点编译报错 - 拼接会隐式调用对应变量的
toString()方法,获取值显示在屏幕上 - 详见transformVText和transformText
- 运行时如果是
- 在
{{}}中可以使用js语法,因此也可以使用函数或运算符,类似一种需要传入的函数参数
在
script中创建的变量可以在所有模板语法中直接使用,无需也无法通过.value访问(vue做了额外解包处理)绑定页面内容<template> <div>{{ message }}</div> <!-- 效果相同 --> <div v-text="message"></div> <!-- 其中相当于是js语法,确保有返回值就行 返回值作为绑定结果 --> <div>{{ message2 + "hhh" }}</div> </template> <!-- setup 语法 --> <script setup> import { ref } from 'vue' const message = ref('Hello Vue!') </script> <!-- 选项式语法 --> <script> export default { data() { return { message2: 'Hello Vue!' } } } </script>
2.1.2 v-bind 属性单向绑定
单向绑定是
vue的一个功能特性,可以将属性变成响应式的,当变量值改变时,标签的属性值也会跟着改变v-bind:对于其他元素,将属性绑定到变量,属性只支持单向同步,变量修改能触发属性变化,但直接修改属性值不会触发变量更新支持简写:
v-bind可以省略不写,仅在属性前添加:就可实现绑定到变量展开绑定:
v-bind可以不指定属性名,表示将对象属性按字段名称展开绑定到对应的标签属性上,即v-bind="obj"对应为:id="obj.id" :class="obj.class" :title="ojb.title"同名绑定:
3.4及之后,:id可以表示将同名变量id绑定给id属性,即:id="id"命名转换:
v-bind.camel修饰符可以处理指令参数,将-命名转换成小驼峰命名,比如foo-bar会转换成fooBar原理:
- 在编译时,属性名和对应变量会被获取到,编译为一个运行时可以直接用的描述对象(
{ props: [createObjectProperty(arg, exp!)], }) - 运行时会先查看
DOM对象上是否有对应属性名,如果有则作为DOM对象属性绑定,如果没有则作为标签属性绑定 vue使用渲染函数render生成虚拟DOM,并进行跟踪- 为了更好地处理绑定情况,
v-bind有两个修饰符.prop和.attr用于强制绑定为标签属性或对应的DOM对象属性 - 详见vBind
绑定表单内容<template> <span>v-bind: {{ b }}</span> <div :style="{ color: 'red' }">hh</div> </template> <script setup> import { ref } from 'vue' const b = ref((new Date()).toISOString()) function setTime() { window.setTimeout(() => { b.value = (new Date()).toISOString(); setTime() }, 1000) } setTime() </script>- 在编译时,属性名和对应变量会被获取到,编译为一个运行时可以直接用的描述对象(
vue中的指令语法
对于一个
vue指令来说,完整的指令语法实际是指令:参数.修饰符="值"指令:指令名称,用于指定指令的名称,比如v-text="message"中的v-text即为指令名称参数:指令参数,用于指定指令的参数,比如v-bind:id="uid"中的id即为参数修饰符:指令修饰符,用于指定指令的修饰符,仅部分指令支持修饰符,比如v-model.lazy="message"中的lazy即为修饰符值:指令值,用于指定指令的值,最终会被解析为js符号表达式

vue中的指令语法
动态参数绑定:同样在指令参数上也可以使用一个
js表达式,需要包含在中括号里,比如v-bind:[key]="value"- 这里的
key会作为js表达式被动态执行,计算得到的值会被用作最终的参数 - 得到的值应该是一个字符串或
null,如果是null表示移除绑定,其他值会触发警告 - 由于
html语法限制,在参数表达式中使用空格和引号是不合法的,建议这种场景:['my' + key]通过计算属性代替复杂的表达式
在
html文件中直接使用模板的情况下,浏览器会强制将表达式名称转换为全小写,需要避免使用大写名称- 这里的
2.1.3 v-model 属性双向绑定
- 双向绑定是指让标签属性和变量保持修改同步,对于表单等输入组件的值会变化时,需要使用双向绑定,以便用户输入时自动更新变量值
v-model:如果属性本身被修改了,会自动更新变量的值底层将变量用
v-bind绑定到属性上,帮助添加事件监听,但v-model不会在输入法输入阶段更新变量,如果绑定希望在输入法输入框出现时触发更新需要自己添加事件监听代替- 最终底层生成和
v-bind一致的描述对象(多了一个和更新相关的事件) - 对于组件标签而言,生成的自定义事件名称为
update:属性名,在组件内触发这个事件可以修改传入的变量值 - 对于原生的标签,事件是原生浏览器事件,在值变化时会自动触发同样生成的自定义事件,修改绑定的变量,原生标签无法手动触发自定义事件
- 更新的事件处理器
props[1]会被缓存起来,重新渲染时会复用处理器
// 生成的描述对象 props const props = [ // modelValue: foo createObjectProperty(propName, dir.exp!), // "onUpdate:modelValue": $event => (foo = $event) createObjectProperty(eventName, assignmentExp), ]- 最终底层生成和
默认绑定:
v-model支持省略需要绑定的属性名,默认的绑定和侦听变化时机如下:- 文本类型的
<input>和<textarea>元素会绑定value并侦听input事件 <input type="checkbox">和<input type="radio">会绑定checked并侦听change事件<select>会绑定value并侦听change事件- 子组件会绑定
value,需要手动触发update:modelValue事件更新 - 其他标签无法使用
v-model,包括<input type="file">
如果需要绑定的属性和
v-model同时存在时始终将v-model绑定的内容作为正确来源- 文本类型的
变量限制:
v-model要求属性对应的变量必须是可变的,否则会报错,依次检查了:- 是否是常量
- 是否来自
props(不可直接修改) - 是否是无法赋值的
js表达式 - 是否是来自
v-for等的作用域变量
详见compiler-core/vModel和compiler-dom/vModel
绑定表单内容<template> <input v-model="message" /> <span>{{ message }}</span> <input v-model="message2" type="number" /> <span>{{ message2 }}</span> <input v-model="message3" type="checkbox" /> <span>{{ message3 }}</span> </template> <script setup> import { ref } from 'vue' const message = ref('Hello Vue!') const message2 = ref(0) const message3 = ref(false) </script>
v-model支持额外修饰符v-model.lazy:默认情况下,v-model会以input事件触发,但是可以通过lazy属性将事件改为change事件,从而实现懒加载,即只有输入框失去焦点时才触发更新v-model.number:将输入自动转换为数字,此修饰符会在type=number时自动启用,如果值无法进行前缀数字转换会返回原始值,比如在输入为空时返回空字符串v-model.trim:将输入自动去除两端空格,在取消焦点后触发
2.1.4 v-html 渲染原始 HTML
v-html:此属性和v-text类似,但v-html会解析字符串为html标签,并将内容替换插入到元素中html注入<template> <div v-html="html"></div> </template> <script setup> const html = '<div class="num">123</div>' </script> <style scoped> .num { color: red; } </style>使用html注入可能导致xss漏洞,需要确保注入内容是安全可信的
2.1.5 v-show 元素可见性切换
- 控制元素是否显示
v-show:v-show会根据绑定的表达式的值,将元素显示或隐藏,返回值为true时显示v-show会在DOM渲染中保留该元素,仅切换了该元素上名为display的css属性之后3.1 条件渲染中会介绍的
v-if也可以实现元素显隐,按条件渲染,可确保在切换时元素和事件监听被完全销毁和重建,且在初次渲染为false时不会创建内容- 在按条件显示节点上,一般来说,
v-if有更高的切换开销,v-show有更高的初始渲染开销 - 对于需要频繁切换的节点,
v-show性能更好,对于状态很少改变的情况,v-if性能更好
v-show必须用于真实节点上,不能用于template标签上,而v-if可以v-show<script setup> import { ref } from 'vue' const show = ref(true) const message = ref('Hello Vue!') </script> <template> <div v-show="show">{{ message }}</div> <button @click="show = !show">toggle</button> </template> <style scoped> div[v-cloak] { color: green; } </style>template 标签
- 在
vue中提供了部分特殊元素,template标签就是其中一种,当我们想要使用内置指令而不在DOM中渲染元素时,template标签可以作为占位符使用 - 对
template标签的特殊处理只有在于v-if/v-for/v-slot同时使用时才会触发,如果不存在这些属性,会被渲染为原生的template标签 - 除了上述指令和
key属性外,其他的属性和指令都将被丢弃,没有相应的元素,其他属性和指令都没有意义 - 单文件主键的顶层
template标签是用来包裹整个模板的,与上面描述的使用方式是有区别的,该顶层标签不是模板本身的一部分,不支持指令等模板语法
- 在按条件显示节点上,一般来说,
2.1.6 v-cloak 隐藏尚未完成编译的模板
- 确保未初始化内容不显示
v-cloak:v-cloak是一个会在vue初始化完毕后被移除的属性,该指令只在没有构建步骤的环境下需要使用在非构建环境,比如直接在
html引入vue包时,由于js需要加载时间,节点也需要被重新编译,用户先看到的是使用绑定语法的内容直接显示在页面上,过了一段时间才被替换为实际内容,称为未编译模板闪现这时可以使用
v-cloak,配合[v-cloak] {display: none;}这样的CSS样式,实现内容隐藏v-cloak会保留在所绑定的元素上,直到相关组件实例被挂载后才移除v-cloak<div id="app" v-clock>{{ message }}</div>[v-cloak] { display: none; }
2.1.7 v-once 仅渲染一次
不会在之后触发更新
v-once:用于只渲染一次的场景,每次渲染都会对比dom的变化,使用v-once减少不必要的比较开销,可以用来优化更新时的性能v-once<template> <!-- 渲染一次 --> <div v-once>{{ message }}</div> <!-- 渲染多次 --> <div>{{ message }}</div> <button @click="message += 1">增加</button> </template> <script setup> import { ref } from 'vue' const message = ref(1) </script>
2.1.8 v-pre 跳过元素
元素内的内容和子元素不使用响应式
v-pre:表明此元素内不会出现需要响应的内容,不希望vue处理其中的内容,最常见的用例就是显示原始双大括号标签及内容<template> <div v-pre>{{ message }}</div> <div>{{ message }}</div> </template> <script setup> import { ref } from 'vue' const message = ref(1) </script>
2.1.9 v-memo 缓存元素
v-memo可缓存一个模板的子树,为了实现缓存,该指令需要传入一个固定长度的依赖值数组变量进行比较- 如果数组里的每个值都与最后一次的渲染相同,那么整个子树的更新将被跳过,甚至虚拟
DOM的创建也将被跳过,因为缓存的子树副本可以被重新使用 - 正确指定缓存数组很重要,否则应该生效的更新可能被跳过,当然也可利用避免子树出现不需要的更新
- 很少的场景需要添加
v-memo,最常见的情况可能是有助于渲染海量的v-for列表,用于性能场景的微小优化 - 如果传入的是空数组,效果和
v-once相同
- 如果数组里的每个值都与最后一次的渲染相同,那么整个子树的更新将被跳过,甚至虚拟
创建响应式变量
- 创建方法
- 如果是选项式
api,可以直接通过data选项创建,data选项是一个函数,要求返回一个对象,对象的键是变量名称name,值是响应式初始值,之后通过this.name访问变量 - 如果是组合式
api,则需要通过ref()函数创建,返回一个响应式变量,之后通过变量.value访问变量;如果是引用类型,推荐使用reactive()函数创建,reactive进行了解包,之后直接通过变量.属性就可以访问变量
- 如果是选项式
2.2.1 ref
ref:创建一个响应式变量,这个变量可以是一个原始类型值也可以是一个引用类型对象原理是当访问这个响应式变量时,会收集依赖,当变量值发生变化时,会触发更新
在
vue3中,因为响应式是通过代理完成的- 在使用组合式
api时,每个变量会单独构建代理,ref响应式属性需要通过.value获取和设置值 - 在使用选项式
api时,data的返回值统一作为reactive变量,不需要使用.value获取和设置值
- 在使用组合式
使用组合式写法如果需要额外类型标注,还需要引入
Ref类型。得益于ts的自动推断,常见的类型可以自动推断,一般仅对于数组进行标注使用选项式写法直接所有的属性是通过对象指针
this访问的,因此,需要确保this始终指向对象,在script块中使用this.message这种方法访问变量。使用function () {}创建一个具名的函数很可能会修改this的指向,因此需要使用() => {},创建匿名函数页面和数据的更新不是同步的,每个更新周期中的数据变化会同步更新,如果希望修改响应到页面后,再执行后续操作,可以使用
await nextTick()ref可以接收引用类型的数据,会使对象属性变化时触发更新。如果仅需要对完全替换引用的数据时触发响应式更新,可以使用shallowRef,仅当.value属性被直接修改替换时才触发更新ref<script setup lang="ts"> import { ref } from 'vue' import type { Ref } from 'vue' const count: Ref<number> = ref(0) function increment() { count.value++ } </script> <script lang="ts"> export default { data() { return { message: 'Hello Vue!' } }, methods: { // 相当于 changeMessage: () => {} changeMessage() { this.message = this.message === 'Hello Vue!' ? 'Goodbye Vue!' : 'Hello Vue!' } } } </script> <template> <button @click="increment"> {{ count }} </button> <button @click="changeMessage"> {{ message }} </button> </template>对数组修改的监听
在
vue2中,使用Object.defineProperty()无法监听数组方法带来的变化,因此vue2中对会修改的方法进行了监听,比如push()、pop()、shift()、unshift()、splice()、sort()、reverse()在
vue3中,得益于代理的强大特性,对数组的修改会直接触发代理监测,因此不再对方法额外进行监听,但为了高效正确且避免无意义触发(比如修改长度时导致了length变化,或是传入原始对象进行indexof查找时发现和代理对象不相等),对这些方法进行额外的包裹,包裹大致如下- 在执行这些函数之前先暂停跟踪,防止不必要的依赖收集
- 对方法的更新批量处理,避免多次更新依赖
- 执行完成后再恢复跟踪
// instrument length-altering mutation methods to avoid length being tracked // which leads to infinite loops in some cases (#2137) function noTracking( self: unknown[], method: keyof Array<any>, args: unknown[] = [], ) { pauseTracking() startBatch() const res = (toRaw(self) as any)[method].apply(self, args) endBatch() resetTracking() return res }
2.2.2 reactive
reactive:创建一个响应式对象,让对象的内的深层属性也变为响应式,仅可用于引用类型基本原理
vue2中data对象会被自动地进行递归追踪,将子属性通过Object.defineProperty()的get和set方法构建vue3中,响应式对象会被递归地创建proxy代理,递归创建是懒加载的,也就是仅当子属性被访问时才会创建代理,节省初始内存和初始化加载时间。创建好的代理对象会通过weakMap缓存,不需要重复创建,需要访问时从weakMap中通过需要的子属性对象作为键获取子代理对象- 代码详见reactive,类型代理的代理基本处理器baseHandlers和集合处理器collectionHandlers
- 懒代理的大致流程类似如下代码,在原码中通过
reactive进行浅层代理和代理处理器BaseReactiveHandler实现后续的,对循环依赖的处理未包含在内
// 缓存对象,防止重复代理 const reactiveMap = new WeakMap(); // 判断是否为对象 function isObject(val) { return typeof val === 'object' && val !== null; } // 创建深层响应式代理的函数 function deepReactive(target) { // 1. 如果不是对象,直接返回(基本类型无法代理) if (!isObject(target)) return target; // 2. 如果已经被代理过,直接返回缓存的代理对象 if (reactiveMap.has(target)) { return reactiveMap.get(target); } // 3. 创建代理 const proxy = new Proxy(target, { get(target, key, receiver) { // 收集依赖(如果有类似 Vue 的依赖收集系统) // track(target, key); console.log(`获取属性: ${String(key)}`); const res = Reflect.get(target, key, receiver); // 【关键点】:如果是对象,递归进行代理(懒代理) // 只有在用到这个属性时,才去代理它,而不是初始化时全部代理 // 对应 BaseReactiveHandler 中的 get 的以下处理逻辑,转换子属性为 reactive // if (isObject(res)) { // // 将返回值也转换为代理对象。我们在此处进行 isObject 检查,以避免出现非法值的警告 // // 此外,还需要延迟访问 readonly 和 reactive 属性,以避免出现循环依赖 // return isReadonly ? readonly(res) : reactive(res) // } if (isObject(res)) { return deepReactive(res); } return res; }, set(target, key, value, receiver) { const oldValue = Reflect.get(target, key, receiver); // 如果值没变,可能不需要触发更新 if (oldValue === value) return true; const result = Reflect.set(target, key, value, receiver); console.log(`设置属性: ${String(key)}, 新值: ${value}`); // 触发更新 // trigger(target, key); return result; } }); // 4. 存入缓存 reactiveMap.set(target, proxy); return proxy; }reactive会对深层的对象属性变化做出反应,如果希望仅对浅层属性进行响应式处理,需要使用shallowReactive对顶级属性对象使用
ref会自动在底层转换为reactive,比如ref({a:1})会自动转换为{value: reactive({a:1})}(还是需要.value访问)如果是将对象的属性作为函数参数,将无法跟踪属性变化,需要将整个
reactive对象作为参数传递给函数可以直接使用对应对象类型声明为变量提供类型
在
options写法中,data对象会被自动创建为reactive的形式reactive<script setup lang="ts"> import { reactive } from 'vue' inference State { count: number } const state:State = reactive({ count: 0 }) // 可以直接访问属性值 // state.count </script> <script lang="ts"> export default { data() { return { state1: { count: 0 } } } } </script> <template> <button @click="state.count++"> {{ state.count }} </button> <button @click="state1.count++"> {{ state1.count }} </button> </template>局限性
reactive是通过代理实现响应式的,虽然看起来和一般的对象一样,但代理对象终究不是一般对象,有以下局限性- 代理对象只能用于对象类型,原始类型无法被代理
- 代理对象和原对象不是同一个,因此会出现代理对象和原对象地址不同,比较返回
false的情况,同时对同一个对象使用reactive方法时,返回的是同一个代理对象 - 不能对代理对象本身进行修改,比如
state = {count:1},替换代理对象会导致响应丢失 - 对解包不友好,解包
const {count} = state会导致响应式连接中断,得到的值不会触发响应式变化。这也导致pinia中使用useStore导出必须完整接收,不能解包
2.4 computed
- 使用模板虽然方便,但其中的表达式不方便复用和维护,好在
vue设计了computed计算属性,可以将常用的表达式提取出来,定义为响应式变量计算属性会自动在响应式数据变化时更新,也在其中的响应式数据变化时才会重新计算,需要配合
ref/reactive使用与直接定义函数不同的是,计算属性会缓存值,因此不需要多次计算
定义计算属性至少需要提供一个
get函数,用于返回计算属性的值;同时可以提供一个set函数,用于修改计算属性的值- 如果只提供一个函数就默认为
get函数,可以获取一个参数,为修改前的值计算属性,此函数不应该修改其他的数据(无副作用),get函数的返回值最终将被ref包裹,在setup环境中需要使用.value属性获取值;在使用选项式时已经自动解包 - 在提供
set时计算属性可以被修改,set函数接收一个参数,为计算属性修改后的值,应该在set函数中修改相关数据,确保相关数据的关系不变
- 如果只提供一个函数就默认为
原理
- 在访问计算属性触发
get时,会追踪计算属性的依赖 - 在
vue的每轮事件循环结束后,渲染会重新触发计算属性的get函数,返回计算属性的值,如果依赖的数据的版本号发生变化,则重新计算 - 不可变性:为了实现仅有
get函数时,不能进行修改,底层设置了__v_isReadonly变量,当尝试修改时检查变量是否可变后再调用set函数 - 对于计算属性直接修改,还需要在
set执行后通知计算属性依赖的数据修改后触发的其他作用 - 代码在computed.ts
computed<script setup lang="ts"> import { ref, computed } from 'vue' const count = ref(0) // 一个计算属性 const stringCount = computed(() => `ref value: ${count.value}` ) // 一个可以设置值的计算属性 const doubleCountWithIncrement = computed({ get: () => count.value * 2, set(newValue) { count.value = newValue / 2 } }) </script> <script lang="ts"> export default { data() { return { count1: 0 } }, computed: { stringCount1() { return `data value: ${this.count1.value}` }, doubleCount: { get() { return this.count1.value * 2 }, set(newValue) { this.count1.value++ } } } } </script> <template> <button @click="doubleCountWithIncrement+=2"> {{doubleCountWithIncrement}} </button> <div>{{ stringCount }}</div> </template>- 在访问计算属性触发
2.5 watch
- 计算属性一般不建议执行副作用操作,但有时确实需要这样的功能,这些功能可以通过
watch实现 watch可以监听某些响应或计算的属性,在属性变化时执行回调函数支持监听一个
ref等响应式对象、一个get函数(用于监听对象的部分属性)、包含需要监听的这两种类型数据的列表- 回调函数可以获取到新值和旧值
监听需要指明属性
options语法在watch属性中添加和需要监听的属性同名的属性,如果是this.$watch动态创建的监听,第一个参数需要提供监听属性名称字符串setup风格使用watch函数,第一个参数为需要监听的数据源,数据源可以是一个ref(包括计算属性)、一个响应式对象、一个getter函数(返回reactive对象中某个属性实现单属性监听,比如() => state.count)、或多个数据源组成的数组
支持选项
flush:回调触发的时机,可选值:pre、post、sync,默认为pre(在dom变化前修改),post(在dom变化后修改),sync(同步修改,变化几次就触发几次)immediate:是否在页面create创建时执行一次回调函数。在setup风格中,可以使用watchEffect函数,此函数会在页面创建时调用回调,回调无参数,但可以直接访问对应属性获取值deep:是否监听对象属性和数组元素的深度变化,如果设置为false,只有自身被替换时能触发,setup风格会自动为对象监听添加此功能,options需要手动添加once:是否只执行一次回调函数(vue3.4+)
副作用清理(
vue3.5+):对于回调请求函数,可能出现请求未完成,数据又被更新,可以使用清理函数onWatcherCleanup(()=>{}),该函数会在watch回调函数执行完成前但再次触发时调用,可以实现副作用防抖取消监听:对于一个监听器,一般不需要取消监听,所有同步创建的监听器会绑定到组件实例上(因此监听器必须同步创建,不在延迟执行的函数中创建),组件式中
watch的返回值就是用于取消监听的函数;options中,可以使用this.$watch在created创建监听,此函数返回值也是用于取消监听的函数watchEffect(()=>{}):自动跟踪依赖并在页面创建时立即执行,仅提供一个回调函数,此回调函数无参数,但可以直接访问对应属性获取值,建立依赖。此方法在获取数据的场景更方便,但依赖关系更不清晰setupconst count = ref(0) const options = ref([]) const state = reactive({ count: 0 }) // 提供需要监听的数据源、一个回调函数、一个可选提供的选项对象 watch(count, (newValue, oldValue) => { options.value.push(newValue) }, { immediate: true }) // 仅对响应式对象的一个属性监听,需要使用 getter 函数 const unwatchC = watch(() => state.count, (newValue, oldValue) => { console.log(newValue, oldValue); }) watchEffect(() => { options.value.push(count.value) }) // 取消监听 unwatchC()optionsexport default { data() { return { count1: 0 options: [] } }, created() { const watchC = this.$watch('count1', (newValue, oldValue) => { console.log(newValue, oldValue); }) }, watch: { // 名称对应需要监听的属性 // 简写,提供一个函数作为回调函数 count1(newValue, oldValue) { this.options.push(`${newValue} -> ${oldValue}`) }, // 完整写法 options: { handler(newValue, oldValue) { console.log(newValue, oldValue) }, // 页面create过程中执行一次 immediate: true } } }监听属性<script setup> import { ref, reactive, watch } from 'vue' const options = ref([]) const obj = reactive({count: 0}) watch(() => obj.count, (newValue) => { options.value.push(newValue) }, {immediate: true}) </script> <template> <div>{{ options.toString() }}</div> <button @click="obj.count++">+</button> </template>
2.6 模板引用
- 如果需要访问
DOM元素,可以在模板中使用引用属性ref,这是一个在元素上添加的额外属性,值为元素设置的引用名字,在当前组件中唯一注意元素需要在渲染元素
mounted后才能访问使用组合式
API需要通过ref或useTemplateRef函数提供变量,这个在运行时会自动绑定元素实例- 如果是使用
ref函数创建,变量名应和组件设置的名字一致,初始值为null,在vue3.5添加的useTemplateRef函数通过传入组件设置的名称来匹配,可以不一致
- 如果是使用
使用选项式
API,引用将被注册在组件的this.$refs对象里,属性名与组件设置的名字一致vue3.5之后支持对带有v-for的元素进行引用,获取所有生成的元素实例列表,实例列表不保证顺序和创建顺序一致- 如果需要使用
ref函数创建,初始的值应该为[]
- 如果需要使用
除了使用字符串值作名字,还可以使用一个函数作为值,函数可以接收到一个参数,参数为当前元素实例,可以赋值给任意变量,在对应内容卸载时,得到的变量是
nullsetup<script setup lang="ts"> import { ref, useTemplateRef, onMounted } from 'vue' import Child from './Child.vue' // 字符串对应标签的 ref="child" const childRef = useTemplateRef<HTMLInputElement>('child') // 3.5 之前只需要设置为 null 即可 // const childRef = ref<HTMLInputElement | null>(null) const part = useTemplateRef<HTMLInputElement>('part') const inpRef = ref(null) onMounted(() => { // childRef.value 将持有 <Child /> 的实例 part.value.focus() }) </script> <template> <input ref="part"></input> <input :ref="(el) => { inpRef.value = el }"></input> <Child ref="child" /> </template>options<script> export default { mounted() { // 直接通过this.$refs访问 this.$refs.input.focus() } } </script> <template> <input ref="input" /> </template>
