vue3入门-v-model、ref和reactive讲解

组件上v-model用法

在 Vue 2.0 发布后,开发者使用 v-model 指令必须使用为 valueprop。如果开发者出于不同的目的需要使用其他的 prop,他们就不得不使用 v-bind:propName.sync。此外,由于 v-modelvalue 之间的这种硬编码关系的原因,产生了如何处理原生元素和自定义元素的问题。

在 Vue 2.2 中,我们引入了 model 组件选项,允许组件自定义用于 v-modelprop事件 。但是,这仍然只允许在组件上使用一个 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 的方式是使用一个可写的,同时具有 gettersettercomputed 属性。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>

在这种情况下,子组件应该使用 bookNamebookAuther prop,以及 update:bookNameupdate: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 中,响应式数据的创建主要依赖于 refreactive 两个 API。它们各自有不同的用途和适用场景。

ref 用于创建基本数据类型的响应式数据,而 reactive 用于创建复杂数据结构(如对象和数组)的响应式数据。

ref

这个方法需要在顶部引入:import { ref } from 'vue'。通常使用它定义响应式数据,不限数据类型。

let xxx = ref(初始值)

返回值: 传入的是基本数据类型 ,则返回 RefImpl 实例对象(简称ref)。如果传的是引用数据类型 ,则内部是通过 reactive 方法处理,最后形成了一个 Proxy 类型的对象。ref 对象的 value 属性是响应式的。

ref 创建的数据,js 中需要 .valuetemplate 中可省略(自动解包)。

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 里面的 getset 进行数据劫持然后进行响应式,但是如果是对象类型的话,是用到的 Proxy。vue3把它封装在新函数 reactive 里,就相当于,ref 中是对象,自动会调用 reactive

那为什么定义一个响应式数据,偏偏要用 .value 去操作呢,满篇的 .value 有什么必要吗,像 Vue2 里直接拿着变量名称处理不是很好?

在官网中得到了解答:

将值封装在一个对象中,看似没有必要,但为了保持 JavaScript 中不同数据类型的行为统一,这是必须的。这是因为在 JavaScript 中,NumberString 等基本类型是通过值而非引用传递的。

按引用传递与按值传递

在任何值周围都有一个封装对象,这样我们就可以在整个应用中安全地传递它,而不必担心在某个地方失去它的响应性。

提示: 换句话说,ref 为我们的值创建了一个响应式引用。在整个组合式 API 中会经常使用引用的概念。

注:安装 Vue - Official 插件后,搜索 Dot Value,勾选对应选项,插件会在使用 ref 创建的变量时自动添加 .value

reactive

前边提到,ref 可以返回任意类型的变量的响应式副本,那 reactive 还有什么必要吗?

当然是有必要的。上一段对 ref 了解,他在操作该响应式变量的时候,需要 .value 去取值,那有没有一个方法,可以避开 NumberString 等基本类型,操作时候无需 .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()getset 来实现响应式(数据劫持)
  • reactive 通过使用 Proxy 来实现响应式(数据劫持), 并通过 Reflect 操作源对象内部的数据
  • ref 遇到引用数据类型时,它的内部会自动通过 reactive 转为代理对象

从使用角度:

  • ref 定义的数据:操作数据需要 .value,读取数据时模板中直接读取不需要 .value
  • reactive 定义的数据:操作数据与读取数据:均不需要 .value

使用原则:

  • 若需要一个基本类型的响应式数据,必须使用 ref
  • 若需要一个响应式对象,层级不深,refreactive都可以
  • 若需要一个响应式对象,且层级较深,推荐使用 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 完全一致,这意味着父组件对子组件的每一个属性和方法都有完全的访问权。这使得在父组件和子组件之间创建紧密耦合的实现细节变得很容易,当然也因此,应该只在绝对需要时才使用组件引用。大多数情况下,你应该首先使用标准的 propsemit 接口来实现父子组件交互。

当使用了 <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。你当然也可以绑定一个组件方法而不是内联函数。

相关推荐
小小愿望1 小时前
前端无法获取响应头(如 Content-Disposition)的原因与解决方案
前端·后端
小小愿望1 小时前
项目启功需要添加SKIP_PREFLIGHT_CHECK=true该怎么办?
前端
烛阴1 小时前
精简之道:TypeScript 参数属性 (Parameter Properties) 详解
前端·javascript·typescript
海上彼尚2 小时前
使用 npm-run-all2 简化你的 npm 脚本工作流
前端·npm·node.js
开发者小天2 小时前
为什么 /deep/ 现在不推荐使用?
前端·javascript·node.js
如白驹过隙3 小时前
cloudflare缓存配置
前端·缓存
excel3 小时前
JavaScript 异步编程全解析:Promise、Async/Await 与进阶技巧
前端
Jerry说前后端3 小时前
Android 组件封装实践:从解耦到架构演进
android·前端·架构
步行cgn4 小时前
在 HTML 表单中,name 和 value 属性在 GET 和 POST 请求中的对应关系如下:
前端·hive·html
hrrrrb4 小时前
【Java Web 快速入门】十一、Spring Boot 原理
java·前端·spring boot