likes
comments
collection
share

Vue3 中 v-model 的改动与扩展

作者站长头像
站长
· 阅读数 59

家喻户晓的 v-model 不仅是开发中常用的指令,也是面试必问的经典。这里在回顾这个语法糖的同时,主要和大家聊聊 vue3 和 vue2 的 v-model 区别,以及如何在 TSX 中扩展 v-model。

v-model = v-bind + v-on

抛开“双向绑定”不谈,父子组件的通讯无非是传值的 v-bind改值的 v-on,大家都很熟悉:

// 父组件:(1)传了个 title;(2)注册了个 update:modelValue 事件 
<Son
    v-bind:modelValue="title"
    v-on:update:modelValue="title = $event"
>

// 子组件:(1)接收 modelValue;(2)调用 emit 修改
{
props: ['modelValue'],
emits: ['update:modelValue']
methdos: {
    change() {
        this.$emit('update:modelValue', 'haha')
    }
}
}

上例中,子组件在调用自身的 change 事件时(比如子组件将 change 事件挂载到内部原生的 input 元素),就会触发 update:modelValue,父组件就会从中拿到子组件传递过来的值,去修改父组件自身的数据。

为了方便起见,Vue 提供了指令供父组件直接使用,于是最常见的 v-model 就诞生了:

<Son v-model="title">

值得一提的是,这里的事件名 update:modelValue 虽说看着有些奇怪,但在代码背后,这正是模板编译器对 v-model 的一种冗长的等价展开,你可以理解为是一种默认的约定。这也是为什么在 Vue2 中 v-model 只能绑定一个值的原因,因为它只有一个叫 modelValue 的值。

既然如此,那除了 v-model外,如何再实现一种自定义的“双向绑定”,能让我同时绑定多个值呢?

Vue2 中的 .sync 修饰符

.sync 修饰符以前存在于 vue1.0 版本里,但是在在 2.0 中移除了 .sync 。但是在 2.0 发布之后的实际应用中,我们发现 .sync 还是有其适用之处,比如在开发可复用的组件库时。我们需要做的只是让子组件改变父组件状态的代码更容易被区分。从 2.3.0 起我们重新引入了 .sync 修饰符,但是这次它只是作为一个编译时的语法糖存在。它会被扩展为一个自动更新父组件属性的 v-on 监听器。

.sync 使我们能够实现自定义的 v-model

// 不使用 .sync
<Son
    v-bind:title="title"
    v-on:update:title="title = $event"
/>
// 使用 .sync
<Son :title.sync="title"/>

请注意,子组件内部依然要做两件事:1.接收 prop, 2.调用 emit 触发更新

// 子组件依然要自己实现更新逻辑
<script>
export default {
    props: ['title'],
    emits: ['update:title']
}
</script>
<template>
    <input
        type="text"
        :value="title"
        @input="$emit('update:title', $event.target.value)"
     />
</template>

不难看出,无论是 v-model 还是 .sync,其本质就是父子组件通讯,只是父组件的使用时写起来方便了些。当然,子组件内部的实现依然不能少。

既然 v-model.sync 两者是一个东西,那能不能整到一起去呢?帮人办法到底,送佛送到西嘛!

v-model 的指令参数

Vue3 对 v-model.sync 进行了合并,将 sync 化成了 v-model指令参数

这好吗?这很好。

我们就可以写多个 v-model 了,反正都是组件通信,怎么舒服怎么来,名字也可以随便取,就很哇撒:

// 父组件绑定多个 model
<UserName
  v-model:first-name="first"
  v-model:last-name="last"
/>
// 子组件实现:
</script>
export default {
  props: {
    firstName: String,
    lastName: String
  },
  emits: ['update:firstName', 'update:lastName']
}
</script>

<template>
  <input
    type="text"
    :value="firstName"
    @input="$emit('update:firstName', $event.target.value)"
  />
  <input
    type="text"
    :value="lastName"
    @input="$emit('update:lastName', $event.target.value)"
  />
</template>

TSX 中编写 v-model

有时,我们需要自己使用 TSX 封装组件,但是 jsx 语法并不支持这个指令,在模板编译过程中,我们必须自己提供这些 props。举个封装的小栗子,大家一眼就能看明白了:

  • 封装一个 element-plus 中的 radio 组件:
import { defineComponent, PropType } from "vue";

export default defineComponent({
  props: {
    modalValue: {
      type: [Boolean, String, Number],
      require: true
    },
    radioType: {
      type: String as PropType<"radio" | "button">,
      default: "radio"
    },
    groups: {
      type: Array as PropType<Array<any>>,
      require: true
    },
  },
  emits: ["update:modalValue"],
  setup(props, ctx) {
    return () => (
      <el-radio-group
        modalValue={props.modalValue}
        onUpdate:modalValue={value => ctx.emit("update:modalValue", value)}
      >
        {props.groups.map(({ slots, ...rProps }) => {
          return props.radioType === "button" ? (
            <el-radio-button {...rProps} v-slots={slots} />
          ) : (
            <el-radio {...rProps} v-slots={slots} />
          );
        })}
      </el-radio-group>
    );
  }
});
  • 父组件使用:
<script setup lang="ts">
import { ref } from "vue";
import RadioGroup from "./components/RadioGroup.vue"

const groups = [
  { label: "昨天", slots: { default: () => "昨天" }},
  { label: "今天", slots: { default: () => "今天" }}, 
  { label: "明天", slots: { default: () => "明天" }}
];

const radio = ref();
</script>

<template>
  <RadioGroup
    v-model="radio"
    radio-type="button"
    :groups="groups"
  />
</template>

上述例子中,element-plus 组件库提供了 model-value / v-model 两种形式给我们绑定选中项的值,所以我们使用 modelValue 和 onUpdate:modelValuev-model 指令进行了扩展,让其他父组件在调用时,可以直接使用 v-model 指令,而无需再注册事件了。

参考资料

转载自:https://juejin.cn/post/7234834284690358331
评论
请登录