组件上v-model用法
在 Vue 2.0 发布后,开发者使用 v-model
指令必须使用为 value
的 prop
。如果开发者出于不同的目的需要使用其他的 prop
,他们就不得不使用 v-bind:propName.sync
。此外,由于 v-model
和 value
之间的这种硬编码关系的原因,产生了如何处理原生元素和自定义元素的问题。
在 Vue 2.2 中,我们引入了 model
组件选项,允许组件自定义用于 v-model
的 prop
和 事件 。但是,这仍然只允许在组件上使用一个 model
。
在 Vue 3 中,双向数据绑定的 API 已经标准化,减少了开发者在使用 v-model
指令时的混淆并且在使用 v-model
指令时可以更加灵活。
首先让我们回忆一下 v-model
在原生元素上的用法:
html
<input v-model="text" />
在代码背后,模板编译器会对 v-model
进行更冗长的等价展开。因此上面的代码其实等价于下面这段:
html
<input :value="text" @input="text = $event.target.value" />
接下来我们看下在自定义组件上的用法。
2.x语法
html
<ChildComponent v-model="text" />
<!-- 去除 v-model 语法糖后的写法 -->
<ChildComponent :value="text" @input="text = $event" />
ChildComponent.vue
html
<template>
<input :value="value" @input="($event) => $emit('input', $event.target.value)" />
</template>
<script>
export default {
props: ['value'],
}
</script>
如果要将属性或事件名称更改为其他名称,则需要在 ChildComponent 组件中添加 model
选项:
ParentComponent.vue
html
<myComponent v-model="isChecked" />
ChildComponent.vue
html
<template>
<input type="checkbox" :checked="checked" @change="($event) => $emit('change', $event.target.checked)" />
</template>
<script>
export default {
model: {
// 使用 `checked` 代替 `value` 作为 model 的 prop
prop: 'checked',
// 使用 `change` 代替 `input` 作为 model 的 event
event: 'change'
},
props: {
checked: Boolean
}
}
</script>
在这个例子中,父组件 v-model
的实际内部处理如下:
html
<ChildComponent :value="text" @change="text = $event" />
使用 v-bind.sync
在某些情况下,我们可能需要对某一个 prop
进行"双向绑定"(除了前面用 v-model
绑定 prop
的情况)。为此,我们建议使用 update:myPropName
抛出事件。
假设 ChildComponent 带有 title prop
,我们可以通过下面的方式将分配新 value 的意图传达给父级:
js
this.$emit('update:title', newValue)
如果需要的话,父级可以监听该事件并更新本地 data property。例如:
html
<ChildComponent :title="text" @update:title="text = $event" />
为了方便起见,我们可以使用 .sync
修饰符来缩写,如下所示:
html
<ChildComponent :title.sync="text" />
3.x语法
当使用在一个组件上时,v-model 会被展开为如下的形式:
html
<template>
<myComponent model-value="text" @update:model-value="($event) => text = $event" />
<div>{{ text }}</div>
</template>
<script setup>
import { ref } from 'vue'
import myComponent from './components/myComponent.vue'
const text = ref('')
</script>
要让这个例子实际工作起来,<myComponent>
组件内部需要做两件事:
- 将内部原生
<input>
元素的value
attribute 绑定到modelValue
prop - 当原生的
input
事件触发时,触发一个携带了新值的update:modelValue
自定义事件
这里是相应的代码:
myComponent.vue
html
<template>
<input :value="modelValue" @input="(e) => $emit('update:modelValue', e.target.value)" />
</template>
<script setup>
const props = defineProps(['modelValue'])
const emits = defineEmits(['update:modelValue'])
</script>
现在 v-model 可以在这个组件上正常工作了:
html
<myComponent v-model="text" />
另一种在组件内实现 v-model 的方式是使用一个可写的,同时具有 getter
和 setter
的 computed
属性。get
方法需返回 modelValue prop,而 set
方法需触发相应的事件:
myComponent.vue
html
<template>
<input v-model="value" />
</template>
<script setup>
import { computed } from 'vue'
const { modelValue } = defineProps(['modelValue'])
const emits = defineEmits(['update:modelValue'])
const value = computed({
get() {
return modelValue
},
set(newVal) {
return emits('update:modelValue', newVal)
}
})
</script>
v-model 的参数
组件上的 v-model 也可以接受一个参数:
html
<MyComponent v-model:title="bookTitle" />
在这种情况下,子组件应该使用 title prop 和 update:title 事件来更新父组件的值,而非默认的 modelValue
prop 和 update:modelValue
事件:
myComponent.vue
html
<template>
<input :value="title" @input="(e) => $emit('update:title', e.target.value)" />
</template>
<script setup>
const { title } = defineProps(['title'])
const emits = defineEmits(['update:title'])
</script>
多个 v-model 绑定
利用刚才在 v-model 的参数小节中学到的指定参数与事件名的技巧,我们可以在单个组件实例上创建多个 v-model 双向绑定。
组件上的每一个 v-model 都会同步不同的 prop,而无需额外的选项:
html
<template>
<myComponent v-model:book-name="bookName" v-model:book-auther="bookAuther" />
<div>bookName:{{ bookName }}、bookAuther:{{ bookAuther }}</div>
</template>
<script setup>
import { ref } from 'vue'
import myComponent from './components/myComponent.vue'
const bookName = ref('')
const bookAuther = ref('')
</script>
在这种情况下,子组件应该使用 bookName 和 bookAuther prop,以及 update:bookName 和 update:bookAuther 事件来更新父组件的值:
myComponent.vue
html
<template>
<input :value="bookName" @input="(e) => $emit('update:bookName', e.target.value)" />
<input :value="bookAuther" @input="(e) => $emit('update:bookAuther', e.target.value)" />
</template>
<script setup>
const { bookName, bookAuther } = defineProps(['bookName', 'bookAuther'])
const emits = defineEmits(['update:bookName', 'update:bookAuther'])
</script>
ref与reactive
在 Vue 3 中,响应式数据的创建主要依赖于 ref
和 reactive
两个 API。它们各自有不同的用途和适用场景。
ref
用于创建基本数据类型的响应式数据,而 reactive
用于创建复杂数据结构(如对象和数组)的响应式数据。
ref
这个方法需要在顶部引入:import { ref } from 'vue'
。通常使用它定义响应式数据,不限数据类型。
let xxx = ref(初始值)
返回值: 传入的是基本数据类型 ,则返回 RefImpl
实例对象(简称ref)。如果传的是引用数据类型 ,则内部是通过 reactive
方法处理,最后形成了一个 Proxy
类型的对象。ref
对象的 value
属性是响应式的。
ref 创建的数据,js 中需要 .value
,template
中可省略(自动解包)。
html
<script setup>
import { ref, reactive } from 'vue';
const text = ref('')
console.log('ref text', text) // ref text RefImpl {dep: Dep, __v_isRef: true, __v_isShallow: false, _rawValue: '', _value: ''}
const obj = reactive({
name: 'caoyuan'
})
console.log('reactive obj', obj) // reactive obj Proxy(Object) {name: 'caoyuan'}
</script>
我们打印 obj,你会发现,它不再是 RefImpl
实例对象,变成了 Proxy
实例对象,vue3 底层把对象都变成了 Proxy
实例对象,对于基本数据类型就是按照 Object.defineProperty
里面的 get
和 set
进行数据劫持然后进行响应式,但是如果是对象类型的话,是用到的 Proxy
。vue3把它封装在新函数 reactive
里,就相当于,ref
中是对象,自动会调用 reactive
。
那为什么定义一个响应式数据,偏偏要用 .value
去操作呢,满篇的 .value
有什么必要吗,像 Vue2 里直接拿着变量名称处理不是很好?
在官网中得到了解答:
将值封装在一个对象中,看似没有必要,但为了保持 JavaScript 中不同数据类型的行为统一,这是必须的。这是因为在 JavaScript 中,Number
或 String
等基本类型是通过值而非引用传递的。
按引用传递与按值传递

在任何值周围都有一个封装对象,这样我们就可以在整个应用中安全地传递它,而不必担心在某个地方失去它的响应性。
提示: 换句话说,ref 为我们的值创建了一个响应式引用。在整个组合式 API 中会经常使用引用的概念。
注:安装 Vue - Official 插件后,搜索 Dot Value,勾选对应选项,插件会在使用 ref 创建的变量时自动添加 .value
reactive
前边提到,ref 可以返回任意类型的变量的响应式副本,那 reactive
还有什么必要吗?
当然是有必要的。上一段对 ref 了解,他在操作该响应式变量的时候,需要 .value
去取值,那有没有一个方法,可以避开 Number
和 String
等基本类型,操作时候无需 .value
呢?答案是有的。也就是 reactive
函数。
作用:定义一个对象类型的响应式数据,不能定义基本数据类型。
语法:const 代理对象 = reactive(源对象)
- 接收一个对象(或数组),返回一个代理对象(
Proxy
的实例对象,简称proxy
对象) reactive
定义的响应式数据是深层次的,意思是不管对象嵌套多少层,整个对象都是响应式的- 内部基于 ES6 的
Proxy
实现,通过代理对象操作源对象内部数据进行操作
html
<template>
<div>{{ arr.toString() }}</div>
<div>{{ obj.info.schoolInfo.location }}</div>
</template>
<script setup>
import { reactive } from 'vue';
const arr = reactive([1,2,3])
// 3秒后值变化
setTimeout(() => {
arr.push(4)
}, 3000);
const obj = reactive({
info: {
name: 'caoyuan',
schoolInfo: {
location: 'henan'
}
}
})
// 6秒后值变化
setTimeout(() => {
obj.info.schoolInfo.location = 'shanghai'
}, 6000);
</script>
ref与reactive的区别
从定义数据角度:
ref
用来定义基本类型数据、引用类型数据。定义引用数据类型时,内部会调用reactive
转为代理对象reactive
用来定义引用类型数据,不支持基本数据类型
从原理角度:
ref
通过Object.defineProperty()
的get
与set
来实现响应式(数据劫持)reactive
通过使用Proxy
来实现响应式(数据劫持), 并通过Reflect
操作源对象内部的数据ref
遇到引用数据类型时,它的内部会自动通过reactive
转为代理对象
从使用角度:
ref
定义的数据:操作数据需要.value
,读取数据时模板中直接读取不需要.value
reactive
定义的数据:操作数据与读取数据:均不需要.value
使用原则:
- 若需要一个基本类型的响应式数据,必须使用
ref
- 若需要一个响应式对象,层级不深,
ref
、reactive
都可以 - 若需要一个响应式对象,且层级较深,推荐使用
reactive
ref模板引用
在某些情况下,我们仍然需要直接访问底层 DOM 元素。要实现这一点,我们可以使用特殊的 ref
attribute:
html
<input ref="input">
ref
是一个特殊的 attribute,它允许我们在一个特定的 DOM 元素 或子组件实例被挂载后,获得对它的直接引用。这可能很有用,比如说在组件挂载时将焦点设置到一个 input 元素上,或在一个元素上初始化一个第三方库。
访问模板引用
要在组合式 API 中获取引用,我们可以使用辅助函数 useTemplateRef()
html
<template>
<input ref="my-input" />
</template>
<script setup>
import { useTemplateRef, onMounted } from 'vue'
// 第一个参数必须与模板中的 ref 值匹配
const inputRef = useTemplateRef('my-input')
onMounted(() => {
inputRef.value.focus()
})
</script>
注意,你只可以在组件挂载后才能访问模板引用。如果你想在模板中的表达式上访问 input,在初次渲染时会是 null。这是因为在初次渲染前这个元素还不存在呢!
如果你需要侦听一个模板引用 ref
的变化,确保考虑到其值为 null 的情况:
js
watchEffect(() => {
if (inputRef.value) {
inputRef.value.focus()
} else {
// 此时还未挂载,或此元素已经被卸载(例如通过 v-if 控制)
}
})
组件上的 ref
模板引用也可以被用在一个子组件上。这种情况下引用中获得的值是组件实例:
html
<template>
<Child ref="child" />
</template>
<script setup>
import { useTemplateRef, onMounted } from 'vue'
import Child from './Child.vue'
const childRef = useTemplateRef('child')
onMounted(() => {
// childRef.value 将持有 <Child /> 的实例
})
</script>
如果一个子组件使用的是选项式 API 或没有使用 <script setup>
,被引用的组件实例和该子组件的 this
完全一致,这意味着父组件对子组件的每一个属性和方法都有完全的访问权。这使得在父组件和子组件之间创建紧密耦合的实现细节变得很容易,当然也因此,应该只在绝对需要时才使用组件引用。大多数情况下,你应该首先使用标准的 props
和 emit
接口来实现父子组件交互。
当使用了 <script setup>
的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup>
的子组件中的任何东西,除非子组件在其中通过 defineExpose
宏显式暴露:
js
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
// 像 defineExpose 这样的编译器宏不需要导入
defineExpose({
a,
b
})
</script>
当父组件通过模板引用获取到了该组件的实例时,得到的实例类型为 { a: number, b: number }
(ref 都会自动解包,和一般的实例一样)。
请注意,defineExpose
必须在任何顶层 await
操作之前调用。否则,在 await
操作后暴露的属性和方法将无法访问。
v-for 中的模板引用
当在 v-for
中使用模板引用时,对应的 ref 中包含的值是一个数组,它将在元素被挂载后包含对应整个列表的所有元素:
html
<template>
<ul>
<li v-for="item in list" ref="items">
{{ item }}
</li>
</ul>
</template>
<script setup>
import { ref, useTemplateRef, onMounted } from 'vue'
const list = ref([
/* ... */
])
const itemRefs = useTemplateRef('items')
onMounted(() => console.log(itemRefs.value))
</script>
应该注意的是,ref 数组并不保证与源数组相同的顺序。
函数模板引用
除了使用字符串值作名字,ref
attribute 还可以绑定为一个函数,会在每次组件更新时都被调用。该函数会收到元素引用作为其第一个参数:
<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">
注意我们这里需要使用动态的 :ref
绑定才能够传入一个函数。当绑定的元素被卸载时,函数也会被调用一次,此时的 el
参数会是 null
。你当然也可以绑定一个组件方法而不是内联函数。