Vue双向绑定

引言:看到有一些文章说到 Vue 的响应式和双向绑定,本文是阅读 Vue 文档后的一些练习和一些总结,本篇重点在于介绍 Vue 文档中提到双向绑定的部分。

响应式

首先简单说下响应式。

在 Vue 文档的深入响应式原理中说到。

Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。

也就是常说的数据驱动视图变化。就是修改数据,视图自动更新,就是页面上的内容自动更新了。

如下示例 1,通过 CDN 引入 Vue,将 Vue 实例 app 挂载到 window 上,在控制台就可以手动修改 app.message 了,修改完后,浏览器上的内容就变化了。

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

  <body>
    <div id="app">{{ message }}</div>
  </body>
  <script>
    var app = new Vue({
      el: "#app",
      data: {
        message: "Hello Vue!",
      },
    });
    window.app = app;
  </script>
</html>

效果图:

我们在控制台手动修改数据的过程,可以看做在实际项目中就是从后端获取到数据后再给 message 赋值的过程。

这样就是一个单向绑定,数据变化视图变化。

这个操作能成功的原因就是基于 Vue 的响应式系统。

那么双向绑定在文档中搜索之后可以看到如下结果:

下面具体看下。

双向绑定

双向绑定就是当数据变化时视图变化(案例 1),并且当视图变化时数据也变化,视图和数据之间双向绑定。如何让视图变化,最常见的就是表单输入操作(输入操作就是直接修改浏览器上展示的视图)。

表单输入绑定

你可以用 v-model 指令在表单 <input><textarea><select> 元素上创建双向数据绑定 。它会根据控件类型自动选取正确的方法来更新元素。尽管有些神奇,但 v-model 本质上不过是语法糖 。它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理

示例 2:

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

  <body>
    <div id="app">
      <input v-model="message" placeholder="edit me" />
      <p>Message is: {{ message }}</p>
    </div>
  </body>
  <script>
    var app = new Vue({
      el: "#app",
      data: {
        message: "Hello Vue!",
      },
    });
    window.app = app;
  </script>
</html>

当我们在输入框里输入数据时,p 标签里的 message 自动变化,与我们输入的内容是一致的。这个 message 也就是绑定在 input 框中的数据,放在 p 标签里是为了清晰的看到 message 的变化。

同样当我们在控制台修改 message 的值,输入框和 p 标签中的值都会相应变化。

接下来看下 v-model 在组件上的使用。

自定义组件的 v-model

一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件

以 input 输入框为例

有一个封装了 input 输入框的 iInput 组件,父组件使用这个组件,最后拿到 iInput 的输入值。

普通写法,示例 3-1:

js 复制代码
<!-- 父组件 -->
<template>
  <div class="about">
    <h2>v-model测试</h2>
    <!-- 在组件上绑定名为 value 的 prop 和 input 事件,onInput里拿到子组件传过来的数据再赋值给userName -->
    <!-- 这里绑定的值 value 和事件 input 可以用其他名字,和子组件中的 prop 以及 $emit 函数的事件名对应上即可-->
    <i-input :value="userName" @input="onInput"></i-input>

    <button @click="onSubmit">提交</button>
  </div>
</template>
<script>
import iInput from "@/components/i-input.vue";
export default {
  components: {
    iInput,
  },
  data() {
    return {
      userName: "父组件初始值",
    };
  },
  methods: {
    onInput(data) {
      this.userName = data;
    },
    onSubmit() {
      console.log(this.userName);
    },
  },
};
</script>
js 复制代码
<!-- 子组件 -->
<template>
  <!-- iInput组件 -->
  <div>
    <input type="input" :value="currentValue" @input="handleInput" />
    <div>子组件中,显示input输入的值: {{ currentValue }}</div>
  </div>
</template>

<script>
export default {
  props: {
    value: {
      type: String,
      default: "",
    },
  },
  data() {
    return {
      // 用父组件的值作为初始值
      currentValue: this.value,
    };
  },
  methods: {
    /* 对于input框,需要绑定input事件,拿到更改后的值 */
    handleInput(event) {
      console.log(event);
      this.currentValue = event.target.value;

      // 执行$emit,将更改后的数据传递给父组件
      this.$emit("input", this.currentValue);
    },
  },
};
</script>

而如果使用 v-model,子组件写法不用变,因为直接用的名称为 value 的 prop 和名称为 input 的事件(this.$emit("input", this.currentValue);这里$emit参数的 input)

父组件修改如下,示例 3-2 :

js 复制代码
<template>
  <div class="about">
    <h2>v-model测试</h2>
    <!-- 修改为使用 v-model="userName" -->
    <i-input v-model="userName"></i-input>

    <!-- <i-input :value="userName" @input="onInput"></i-input> -->
    <button @click="onSubmit">提交</button>
  </div>
</template>
<script>
import iInput from "@/components/i-input.vue";
export default {
  components: {
    iInput,
  },
  data() {
    return {
      userName: "父组件初始值",
    };
  },
  methods: {
    /* 注释掉没用到的方法 */
    /* onInput(data) {
      this.userName = data;
    }, */
    onSubmit() {
      console.log(this.userName);
    },
  },
};
</script>

示例 3-1 和示例 3-2 最终的效果是一样的。

写法上父组件中的<i-input :value="userName" @input="onInput"></i-input>写法被替换成更简单的:<i-input v-model="userName"></i-input>,子组件中按照 Vue 要求的用名称为 value 的 prop 和名称为 input 的事件即可。这里能清晰的看到为什么说 v-model 本质上是语法糖。

文档中还提到了其他的表单控件,比如 checkbox,它的选中与否状态是用 checked 属性来表示,也就是我们需要拿到的类似 input 输入框的 value 值,而它的 value 属性有其他作用,对应的需要处理的事件也不同。所以 Vue 文档中说:

v-model 在内部为不同的输入元素使用不同的 property 并抛出不同的事件:

  • text 和 textarea 元素使用 value property 和 input 事件;
  • checkbox 和 radio 使用 checked property 和 change 事件;
  • select 字段将 value 作为 prop 并将 change 作为事件。

在将这些输入控件封装成组件时,文档上也提到:

像单选框、复选框等类型的输入控件可能会将 value attribute 用于不同的目的。model 选项可以用来避免这样的冲突。

这种情况下可能就是前文 Vue 文档中提到的对极端场景进行处理。

以 checkbox 为例

子组件中增加了 model,注意子组件中注释的修改。

js 复制代码
<!-- 子组件 -->
<template>
  <div>
    <input
      type="checkbox"
      value="agreement"
      :checked="currentValue"
      @change="handleChange"
    />
    <label for="agreement">同意</label>
    <div>子组件中,显示radio checked值: {{ currentValue }}</div>
  </div>
</template>

<script>
export default {
  // 不是默认的value和input,这里需要配置model
  model: {
    prop: "checked",
    event: "change",
  },
  props: {
    // 改为checked
    checked: Boolean,
  },
  data() {
    return {
      currentValue: this.checked,
    };
  },
  methods: {
    handleChange(event) {
      console.log(event);
      // 这里是checked
      this.currentValue = event.target.checked;
      // 改为change
      this.$emit("change", this.currentValue);
    },
  },
};
</script>
js 复制代码
<!-- 父组件 -->
<template>
  <div class="about">
    <h2>v-model测试</h2>
    <i-checkbox v-model="checkedItem"></i-checkbox>
    <button @click="onSubmit">提交</button>
  </div>
</template>
<script>
import iCheckbox from "@/components/i-checkbox.vue";
export default {
  components: {
    iCheckbox,
  },
  data() {
    return {
      checkedItem: true,
    };
  },
  methods: {
    onSubmit() {
      console.log(this.checkedItem);
    },
  },
};
</script>

Vue 文档中还有一段话:

AngularJS 使用双向绑定,Vue 在不同组件间强制使用单向数据流。这使应用中的数据流更加清晰易懂。

也就是说一般情况下,Vue 在不同组件间强制使用单向数据流。我们常用的就是通过 prop 和 $emit 来实现父子通信,prop是父向子,$emit 是子向父,也就是示例 3-1。在示例 3-2 中,对于子组件还是 prop 和 $emit,父组件中使用 v-model 的写法更简单了,看父组件就像看 示例 2 一样是双向绑定了。

还能看到一个"双向绑定"的地方就在.sync修饰符,这里的双向绑定被添加了双引号。下面先看下.sync 用法。

.sync修饰符

在有些情况下,我们可能需要对一个 prop 进行"双向绑定"。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以变更父组件,且在父组件和子组件两侧都没有明显的变更来源。

举例:父组件中有个按钮,点击打开弹窗组件。点击弹窗上的关闭按钮,关闭弹窗。

效果图:

普通写法

示例 4-1:

js 复制代码
<!-- 父组件 -->
<template>
  <div>
    <i-modal
      :modal-visible="modalVisible"
      @on-modal-change="modalVisible = $event"
    ></i-modal>
    <button @click="modalVisible = true">打开modal</button>
  </div>
</template>
<script>
import iModal from "@/components/i-modal.vue";
export default {
  data() {
    return {
      modalVisible: false,
    };
  },
};
</script>
js 复制代码
<!-- 子组件 -->
<template>
  <div v-show="modalVisible" class="box">
    <div class="content">
      <button @click="closeModal">点击关闭弹窗</button>
      <p>弹窗组件</p>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    modalVisible: Boolean,
  },
  methods: {
    closeModal() {
      this.$emit("on-modal-change", false);
    },
  },
};
</script>
<style scoped>
.box {
  position: fixed;
  z-index: 1000;
  width: 100%;
  height: 100%;
  left: 0;
  top: 0;
  background: rgba(0, 0, 0, 0.1);
}
.content {
  width: 240px;
  height: 120px;
  padding: 10px;
  margin: 0 auto;
  background: white;
}
</style>

使用.sync 写法

示例 4-2:

js 复制代码
<!-- 父组件 -->
<template>
  <div>
    <!-- 加上.sync 写法更简单 -->
    <i-modal :modal-visible.sync="modalVisible"></i-modal>
    <button @click="modalVisible = true">打开modal</button>
  </div>
</template>
<script>
import iModal from "@/components/i-modal.vue";
export default {
  data() {
    return {
      modalVisible: false,
    };
  },
};
</script>
js 复制代码
<!-- 子组件 -->
<template>
  <div v-show="modalVisible" class="box">
    <div class="content">
      <button @click="closeModal">点击关闭弹窗</button>
      <p>弹窗组件</p>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    modalVisible: Boolean,
  },
  methods: {
    // 子组件通过'update:modalVisible'触发事件去更新父组件传入的prop
    closeModal() {
      this.$emit("update:modalVisible", false);
    },
  },
};
</script>
<style scoped>
.box {
  position: fixed;
  z-index: 1000;
  width: 100%;
  height: 100%;
  left: 0;
  top: 0;
  background: rgba(0, 0, 0, 0.1);
}
.content {
  width: 240px;
  height: 120px;
  padding: 10px;
  margin: 0 auto;
  background: white;
}
</style>

.sync总结

父组件中,普通写法:<i-modal :modal-visible="modalVisible" @on-modal-change="modalVisible = $event"></i-modal>

和前文中的 v-model 一样,使用.sync 语法糖写法更简单。父组件中在 propName 后面加上.sync。如:<i-modal :modal-visible.sync="modalVisible"></i-modal>。子组件中,$emit 事件名称相应的使用update:propName。如:this.$emit("update:modalVisible", false);

这种情况下,不是前面说的表单输入情况下视图和数据之间的双向绑定,而是 Vue 提供了.sync 语法糖,实现看起来像是数据在父子组件之间双向绑定了。

总结

前面展示的单向绑定和双向绑定的案例,指的是视图与数据之间的绑定关系。而能够实现这些效果,都是因为使用了 Vue 框架,其基于响应式的原理使我们不用直接操作 DOM,而是只需要变更数据(Model)即可驱动视图(View)变化,也就是 MVVM 框架做的事情。

关于 MVVM,Vue 官网中说:

虽然没有完全遵循 MVVM 模型,但是 Vue 的设计也受到了它的启发。因此在文档中经常会使用 vm (ViewModel 的缩写) 这个变量名表示 Vue 实例。

以上就是个人对 Vue 官方文档中关于双向绑定的一些解读,如有谬误,欢迎指正!

参考资料

  1. 廖雪峰的官方网站
相关推荐
范文杰13 分钟前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪21 分钟前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪29 分钟前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy1 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom2 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom2 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom2 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom2 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom2 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试
LaoZhangAI3 小时前
2025最全GPT-4o图像生成API指南:官方接口配置+15个实用提示词【保姆级教程】
前端