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

相关推荐
爱上妖精的尾巴5 分钟前
8-5 WPS JS宏 match、search、replace、split支持正则表达式的字符串函数
开发语言·前端·javascript·wps·jsa
小温冲冲8 分钟前
通俗且全面精讲单例设计模式
开发语言·javascript·设计模式
意法半导体STM3237 分钟前
【官方原创】FDCAN数据段波特率增加后发送失败的问题分析 LAT1617
javascript·网络·stm32·单片机·嵌入式硬件·安全
为什么不问问神奇的海螺呢丶38 分钟前
n9e categraf redis监控配置
前端·redis·bootstrap
云飞云共享云桌面39 分钟前
推荐一些适合10个SolidWorks设计共享算力的服务器硬件配置
运维·服务器·前端·数据库·人工智能
Liu.7741 小时前
vue开发h5项目
vue.js
咔咔一顿操作1 小时前
轻量无依赖!autoviwe 页面自适应组件实战:从安装到源码深度解析
javascript·arcgis·npm·css3·html5
刘联其1 小时前
.net也可以用Electron开发跨平台的桌面程序了
前端·javascript·electron
韩曙亮1 小时前
【jQuery】jQuery 选择器 ④ ( jQuery 筛选方法 | 方法分类场景 - 向下找后代、向上找祖先、同级找兄弟、范围限定查找 )
前端·javascript·jquery·jquery筛选方法
前端 贾公子1 小时前
Node.js 如何处理 ES6 模块
前端·node.js·es6