Loading

vue v-model 双向绑定

回顾从 vue2 到 vue3 v-model 双向绑定的写法变化

场景

v-model 双向绑定,用于处理表单输入绑定,类似于 react 中的受控组件。

// React 受控组件
function App() {
  const [text, setText] = useState("");

  return (
    <>
      <h3>{text}</h3>
      <input
        value={text}
        onInput={(e) => {
          setText(e.target.value);
        }}
      ></input>
    </>
  );
}

vue 的 v-model 本质与 react 受控组件是一样的,只是加了一个语法糖封装。

vue2 表单 v-model

<template>
  <div>
    <h2>FullName: {{ fullName }}</h2>
    <h3>Email: {{ email }}</h3>

    <input v-model="firstName" />
    <input :value="lastName" @input="(e) => (lastName = e.target.value)" />

    <input v-model.trim="email" placeholder="your email here" />
  </div>
</template>

<script>
export default {
  name: "HelloWorld",
  data() {
    return {
      firstName: "",
      lastName: "",
      email: "",
    };
  },
  computed: {
    fullName() {
      return this.firstName + " " + this.lastName;
    },
  },
};
</script>

表单输入绑定 — Vue.js

这个例子中,firstName 使用 v-model 的基础写法,lastName 是还原 v-model 的“本来面目”。
需要注意的是,这里对 input 标签,绑定的是 value 属性和 input 事件,不同的 input 标签类型,对应的属性和事件不同,详见官方文档。

email 数据添加了修饰符,可以做一些额外的处理

vue2 父子组件 v-model

下面这个案例展示对于自定义组件,如何使用 v-model。
在组件间使用 v-model,一个隐含的场景是,数据是由父组件提供的,子组件可能会修改数据,然后通知父组件更新数据。
不管是 vue 还是 react,都是单向数据流的设计,子组件不应该直接修改父组件给过来的数据,而是通知父组件,让父组件处理,完成所谓的双向绑定。

PS 如果数据本身就是子组件产生的,那直接通过事件告知父组件即可,这种场景没有双向绑定,也就不需要 v-model。

// Foo 组件,子组件

<template>
  <div>
    <!-- <input :value="value" @input="(e) => this.$emit('input', e.target.value)" /> -->
    <input
      :value="firstName"
      @input="(e) => this.$emit('updateFristName', e.target.value)"
    />

    <input
      :value="lastName"
      @input="(e) => this.$emit('update:lastName', e.target.value)"
    />

    <input
      :value="email"
      @input="(e) => this.$emit('update:email', e.target.value.trim())"
      placeholder="your email here"
    />

    <p>{{ firstName }} {{ lastName }} {{ email }}</p>
  </div>
</template>

<script>
export default {
  name: "FooItem",
  model: {
    prop: "firstName",
    event: "updateFristName",
  },
  props: {
    // value: String,
    firstName: String,
    lastName: String,
    email: {
      type: String,
      default: "https://www.cnblogs.com/jasongrass",
    },
  },
  data() {
    return {};
  },
};
</script>

这里子组件中是没有任何 v-model 这个指令的,因为 v-model 有两个功能,一个是提供数据,一个是修改数据(在事件回调中),而子组件是不能修改父组件提供的数据的,会破坏单向数据流。
所以这里子组件只是通过 props 接受数据,需要修改数据时,只触发事件,具体的事件处理和数据的实际修改,在父组件中完成。

具体写法上,上面的子组件代码中,涉及到了三种写法。

子组件 1. 默认写法

在上面代码中被注释的部分,即默认的数据名称是 value,默认的事件名称是 input

文档:自定义事件 — Vue.js

<input :value="value" @input="(e) => this.$emit('input', e.target.value)" />

子组件 2. 修改默认写法

默认写法有两个问题,一是不够语义化,在数据比较多的时候,value 具体的业务含义会很不直观,影响代码可读性;二是在其它场景下,可能不能满足需求,如使用单选框、复选框等不同的表单元素时。

此时就可以自定义,如上面的 firstName,默认的 v-model 双向绑定属性名称,变成了 firstName, 事件变成了 updateFristName。

model: {
  prop: "firstName",
  event: "updateFristName",
}

<input
  :value="firstName"
  @input="(e) => this.$emit('updateFristName', e.target.value)"
/>

子组件 3. 多个数据的双向绑定

这里就是 lastName 和 email 两个属性,不考虑事件触发,其实这就是两个普通的属性。

修饰符 .sync — Vue.js

特殊之处在于,这里在期望数据改变时,触发 update:myPropName 事件,以通知父组件修改相关的数据。

<input
  :value="lastName"
  @input="(e) => this.$emit('update:lastName', e.target.value)"
/>

// FooContainer 组件,父组件

<template>
  <div>
    <h2>FullName: {{ fullName }}</h2>
    <h3>Email: {{ email }}</h3>
    <!-- :lastName.sync="lastName" -->
    <FooItem
      v-model="firstName"
      :lastName="lastName"
      @update:lastName="
        (e) => {
          lastName = e;
        }
      "
      :email.sync="email"
    ></FooItem>
  </div>
</template>

<script>
import FooItem from "./Foo.vue";
export default {
  name: "FooContainer",
  components: {
    FooItem,
  },
  data() {
    return {
      firstName: "",
      lastName: "",
      email: "",
    };
  },
  computed: {
    fullName() {
      return this.firstName + " " + this.lastName;
    },
  },
};
</script>

<style scoped></style>

父组件 1. 默认写法

如上面的 firstName,如果需要将父组件中的 firstName 数据,作为子组件的默认 v-model 数据绑定,直接写 v-model="firstName"
这样就会实现与子组件默认 model 的双向绑定

父组件 2. 修改默认写法

修改默认写法,是针对子组件而言的。对于父组件,只要是绑定子组件的 model(因为只有一个),写法就是 v-model="firstName"

父组件 3. 多个数据的双向绑定

如这里的 lastName 和 email 数据,多个数据的绑定,可以对 v-bind 使用 .sync 修饰符。

.sync 修饰符 — Vue.js

本质上就是以下写法的语法糖

    <FooItem
      :lastName="lastName"
      @update:lastName="
        (e) => {
          lastName = e;
        }
      "
    ></FooItem>

vue3 v-model 的变化

主要变化体现在自定义组件的 v-model 上,vue2 中一个组件只有一个 model 定义,其它的是通过 v-bind 的 .sync 修饰符来实现的。
在语法上容易混淆 v-model 和 v-bind 的用法,不是很直观。

v-model | Vue 3 迁移指南

以下是对变化的总体概述:

  • 非兼容:用于自定义组件时,v-model prop 和事件默认名称已更改:
    prop:value -> modelValue;
    事件:input -> update:modelValue;
  • 非兼容:v-bind 的 .sync 修饰符和组件的 model 选项已移除,可在 v-model 上加一个参数代替;
  • 新增:现在可以在同一个组件上使用多个 v-model 绑定;
  • 新增:现在可以自定义 v-model 修饰符。

vue3 表单 v-model

这部分没有什么变化,详见文档:表单输入绑定 | Vue.js

<template>
  <div>
    <div>
      <h2>FullName: {{ fullName }}</h2>
      <h3>Email: {{ email }}</h3>

      <input v-model="firstName" />
      <input :value="lastName" @input="(e) => (lastName = e.target.value)" />

      <input v-model.trim="email" placeholder="your email here" />
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, toRefs, computed } from "vue";

const info = reactive({
  firstName: "",
  lastName: "",
  email: "",
});
const { firstName, lastName, email } = toRefs(info);

const fullName = computed(() => {
  return firstName.value + " " + lastName.value;
});
</script>

vue3 父子组件 v-model

组件 v-model | Vue.js

在 vue 3.4 版本之后,使用了 defineModel 宏,处理 v-model 双向绑定写法上就简单多了。

// Foo 组件,子组件

<template>
  <div>
    <input :value="model" @input="(e) => (model = e.target.value)" />
    <input v-model="model" />

    <input :value="lastName" @input="(e) => (lastName = e.target.value)" />
    <input v-model="lastName" />

    <input :value="email" @input="updateEmail" placeholder="your email here" />

    <p>{{ model }} {{ lastName }} {{ email }}</p>
  </div>
</template>

<script setup>
const model = defineModel();
const lastName = defineModel("lastName");
const [email, emailModifiers] = defineModel("email");

const updateEmail = (e) => {
  const inputValue = e.target.value;
  if (emailModifiers.upper) {
    console.log(inputValue);
    email.value = inputValue ? inputValue.toUpperCase() : "";
  } else {
    email.value = inputValue;
  }
};
</script>

<style lang="less" scoped></style>

子组件 1. 默认写法

model 定义
const model = defineModel();

model 使用
<input v-model="model" />

默认写法就是在使用 defineModel 时,不指定 model 的名称,则内部默认名称是 modelValue, 对应的更新事件名称是 update:modelValue, 但这两个默认名称,都不需要体现在代码中。
代码中直接使用 defineModel 的返回值,可以自定义命名,如这里是 model,它是一个 ref, 可以直接读取或修改,如果是修改,则底层会自动调用 update:modelValue 事件,通知父组件处理。

注意,这里在子组件中,可以直接使用 v-model,而不是必须写成 <input :value="model" @input="(e) => (model = e.target.value)" /> 这样手动绑定 value 和 触发事件 的方式。因为这里 v-model 绑定的是一个 ref 代理,内部在修改数据时,没有真实修改数据,而是触发事件。

在 vue3.4 之前,不支持这样写的时候,可以自定义一个计算属性,将 input 标签的 value 绑定到这个计算属性中, 计算属性的 get 方法中返回 model, 计算属性的 set 方法中,触发 update:modelValue 事件。但这样还是需要手动添加并封装一个计算属性。

代码上省心很多,但这里仍然遵守数据单向流的设计原则(虽然看起来像是直接在修改数据),如果父组件不对事件做处理(当然,通常父组件对事件的处理,也是被自动封装在了 v-model 指令中),则子组件对数据的“修改”,也是无效的。

子组件 2&3. 修改默认写法 和 多个 v-model

在使用 defineModel 之后,不管是默认写法,还是定义多个 v-model,都进行了风格上的统一。直接使用 defineModel 定义即可。

const lastName = defineModel("lastName");

子组件,处理自定义修饰符

<script setup>
const [email, emailModifiers] = defineModel("email");

const updateEmail = (e) => {
  const inputValue = e.target.value;
  if (emailModifiers.upper) {
    console.log(inputValue);
    email.value = inputValue ? inputValue.toUpperCase() : "";
  } else {
    email.value = inputValue;
  }
};
</script>

// FooContainer 组件,父组件

<template>
  <div>
    <h2>FullName: {{ fullName }}</h2>
    <h3>Email: {{ email }}</h3>
    <Foo
      v-model="fristName"
      v-model:lastName="lastName"
      v-model:email.upper="email"
    ></Foo>
  </div>
</template>

<script setup>
import { computed, ref } from "vue";
import Foo from "./Foo.vue";

const fristName = ref("");
const lastName = ref("");
const email = ref("");

const fullName = computed(() => {
  return fristName.value + " " + lastName.value;
});
</script>

父组件的写法也简单直接了很多,对于默认 model, 直接使用 v-model="fristName" 这样的方式绑定,对于其它命名的 model, 使用 v-model:lastName="lastName" 进行绑定。
v-model 内部自动处理了监听子组件对应事件,并修改对应数据的操作。


总结

vue 3.4 之后,对 v-model 进行了很多优化,引入 defineModel 统一了 vue2 各种 model 的写法,方便地支持了多个 v-model。
但仍然需要注意,本质上 v-model 还是没有改变单向数据流这个设计原则,只是实现细节被封装起来了,在开发中需要有这个意识。

参考文档

Vue2
表单输入绑定 — Vue.js
组件 v-model | Vue.js
自定义组件的 v-model & .sync 修饰符 — Vue.js

Vue3
v-model | Vue 3 迁移指南
表单输入绑定 | Vue.js
组件 v-model | Vue.js

原文链接:https://www.cnblogs.com/jasongrass/p/18148695

posted @ 2024-04-21 16:14  J.晒太阳的猫  阅读(361)  评论(0编辑  收藏  举报