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. 廖雪峰的官方网站
相关推荐
苹果电脑的鑫鑫3 分钟前
element中表格文字剧中可以使用的属性
javascript·vue.js·elementui
Hejjon8 分钟前
Vue2 elementUI 二次封装命令式表单弹框组件
前端·vue.js
小堃学编程1 小时前
前端学习(3)—— CSS实现热搜榜
前端·学习
Wannaer1 小时前
从 Vue3 回望 Vue2:响应式的内核革命
前端·javascript·vue.js
不灭锦鲤1 小时前
xss-labs靶场基础8-10关(记录学习)
前端·学习·xss
Bl_a_ck1 小时前
--openssl-legacy-provider is not allowed in NODE_OPTIONS 报错的处理方式
开发语言·前端·web安全·网络安全·前端框架·ssl
懒羊羊我小弟1 小时前
手写符合Promise/A+规范的Promise类
前端·javascript
互联网搬砖老肖1 小时前
Web 架构之负载均衡会话保持
前端·架构·负载均衡
赵大仁2 小时前
React vs Vue:点击外部事件处理的对比与实现
javascript·vue.js·react.js
肥肥呀呀呀3 小时前
在Flutter上如何实现按钮的拖拽效果
前端·javascript·flutter