Vue 的双向绑定原理与用法揭秘


Posted in Javascript onMay 06, 2020

本文实例讲述了Vue 的双向绑定原理与用法。分享给大家供大家参考,具体如下:

Vue 中需要输入什么内容的时候,自然会想到使用 <input v-model="xxx" /> 的方式来实现双向绑定。下面是一个最简单的示例

<div id="app">
  <h2>What's your name:</h2>
  <input v-model="name" />
  <div>Hello {{ name }}</div>
</div>
new Vue({
  el: "#app",
  data: {
      name: ""
  }
});

在这个示例的输入框中输入的内容,会随后呈现出来。这是 Vue 原生对 <input> 的良好支持,也是一个父组件和子组件之间进行双向数据传递的典型示例。不过 v-model 是 Vue 2.2.0 才加入的一个新功能,在此之前,Vue 只支持单向数据流。

Vue 的单向数据流

Vue 的单向数据流和 React 相似,父组件可以通过设置子组件的属性(Props)来向子组件传递数据,而父组件想获得子组件的数据,得向子组件注册事件,在子组件高兴的时候触发这个事件把数据传递出来。一句话总结起来就是,Props 向下传递数据,事件向上传递数据。

上面那个例子,如果不使用 v-model,它应该是这样的

<input :value="name" @input="name = $event.target.value" />

由于事件处理写成了内联模式,所以脚本部分不需要修改。但是多数情况下,事件一般都会定义成一个方法,代码就会复杂得多

<input :value="name" @input="updateName" />
new Vue({
  // ....
  methods: {
    updateName(e) {
      this.name = e.target.value;
    }
  }
})

从上面的示例来看 v-model 节约了不少代码,最重要的是可以少定义一个事件处理函数。所以 v-model 实际干的事件包括

  • 使用 v-bind(即 :)单向绑定一个属性(示例::value="name"
  • 绑定 input 事件(即 @input)到一个默认实现的事件处理函数(示例:@input=updateName
  • 这个默认的事件处理函数会根据事件对象带入的值来修改被绑定的数据(示例:this.name = e.target.value

自定义组件的 v-model

Vue 对原生组件进行了封装,所以 <input> 在输入的时候会触发 input 事件。但是自定义组件应该怎么呢?这里不妨借助 JsFiddle Vue 样板的 Todo List 示例。

JsFiddle 的 Vue 样板

点击 JsFilddle 的 Logo,在上面弹出面板中选择 Vue 样板即可

样板代码包含 HTML 和 Vue(js) 两个部分,代码如下:

<div id="app">
 <h2>Todos:</h2>
 <ol>
  <li v-for="todo in todos">
   <label>
    <input type="checkbox"
     v-on:change="toggle(todo)"
     v-bind:checked="todo.done">

    <del v-if="todo.done">
     {{ todo.text }}
    </del>
    <span v-else>
     {{ todo.text }}
    </span>
   </label>
  </li>
 </ol>
</div>
new Vue({
 el: "#app",
 data: {
  todos: [
   { text: "Learn JavaScript", done: false },
   { text: "Learn Vue", done: false },
   { text: "Play around in JSFiddle", done: true },
   { text: "Build something awesome", done: true }
  ]
 },
 methods: {
   toggle: function(todo){
    todo.done = !todo.done
  }
 }
})

定义 Todo 组件

JsFiddle 的 Vue 模板默认实现一个 Todo 列表的展示,数据是固定的,所有内容在一个模板中完成。我们首先要做事情是把单个 Todo 改成一个子组件。因为在 JsFiddle 中不能写成多文件的形式,所以组件使用 Vue.component() 在脚本中定义,主要是把 <li> 内容中的那部分拎出来:

Vue.component("todo", {
  template: `
<label>
  <input type="checkbox" @change="toggle" :checked="isDone">
  <del v-if="isDone">
    {{ text }}
  </del>
  <span v-else>
    {{ text }}
  </span>
</label>
`,
  props: ["text", "done"],
  data() {
    return {
      isDone: this.done
    };
  },
  methods: {
    toggle() {
      this.isDone = !this.isDone;
    }
  }
});

原来定义在 App 中的 toggle() 方法也稍作改动,定义在组件内了。toggle() 调用的时候会修改表示是否完成的 done 的值。但由于 done 是定义在 props 中的属性,不能直接赋值,所以采用了官方推荐的第一种方法,定义一个数据 isDone,初始化为 this.done,并在组件内使用 isDone 来控制是否完成这一状态。

相应的 App 部分的模板和代码精减了不少:

<div id="app">
  <h2>Todos:</h2>
  <ol>
    <li v-for="todo in todos">
      <todo :text="todo.text" :done="todo.done"></todo>
    </li>
  </ol>
</div>
new Vue({
  el: "#app",
  data: {
    todos: [
      { text: "Learn JavaScript", done: false },
      { text: "Learn Vue", done: false },
      { text: "Play around in JSFiddle", done: true },
      { text: "Build something awesome", done: true }
    ]
  }
});

不过到此为止,数据仍然是单向的。从效果上来看,点击复选框可以反馈出删除线线效果,但这些动态变化都是在 todo 组件内部完成的,不存在数据绑定的问题。

为 Todo List 添加计数

为了让 todo 组件内部的状态变化能在 Todo List 中呈现出来,我们在 Todo List 中添加计数,展示已经完成的 Todo 数量。因为这个数量受 todo 组件内部状态(数据)的影响,这就需要将 todo 内部数据变化反应到其父组件中,这才有 v-model 的用武之地。

这个数量我们在标题中以 n/m 的形式呈现,比如 2/4 表示一共 4 条 Todo,已经完成 2 条。这需要对 Todo List 的模板和代码部分进行修改,添加 countDonecount 两个计算属性:

<div id="app">
  <h2>Todos ({{ countDone }}/{{ count }}):</h2>
  <!-- ... -->
</div>
new Vue({
  // ...
  computed: {
    count() {
      return this.todos.length;
    },
    countDone() {
      return this.todos.filter(todo => todo.done).length;
    }
  }
});

现在计数呈现出来了,但是现在改变任务状态并不会对这个计数产生影响。我们要让子组件的变动对父组件的数据产生影响。v-model 待会儿再说,先用最常见的方法,事件:

  • 子组件 todotoggle() 中触发 toggle 事件并将 isDone 作为事件参数
  • 父组件为子组件的 toggle 事件定义事件处理函数
Vue.component("todo", {
  //...
  methods: {
    toggle(e) {
      this.isDone = !this.isDone;
      this.$emit("toggle", this.isDone);
    }
  }
});
<!-- #app 中其它代码略 -->
<todo :text="todo.text" :done="todo.done" @toggle="todo.done = $event"></todo>

这里为 @toggle 绑定的是一个表达式。因为这里的 todo 是一个临时变量,如果在 methods 中定义专门的事件处理函数很难将这个临时变量绑定过去(当然定义普通方法通过调用的形式是可以实现的)。

事件处理函数,一般直接对应于要处理的事情,比如定义 onToggle(e),绑定为 @toggle="onToggle"。这种情况下不能传入 todo 作为参数。

普通方法,可以定义成 toggle(todo, e),在事件定义中以函数调用表达式的形式调用:@toggle="toggle(todo, $event)"。它和 todo.done = $event` 同属表达式。

注意二者的区别,前者是绑定的处理函数(引用),后者是绑定的表达式(调用)

现在通过事件方式已经达到了预期效果

改造成 v-model

之前我们说了要用 v-model 实现的,现在来改造一下。注意实现 v-model 的几个要素

  • 子组件通过 value 属性(Prop)接受输入
  • 子组件通过触发 input 事件输出,带数组参数
  • 父组件中用 v-model 绑定
Vue.component("todo", {
  // ...
  props: ["text", "value"],  // <-- 注意 done 改成了 value
  data() {
    return {
      isDone: this.value  // <-- 注意 this.done 改成了 this.value
    };
  },
  methods: {
    toggle(e) {
      this.isDone = !this.isDone;
      this.$emit("input", this.isDone); // <-- 注意事件名称变了
    }
  }
});
<!-- #app 中其它代码略 -->
<todo :text="todo.text" v-model="todo.done"></todo>

.sync 实现其它数据绑定

前面讲到了 Vue 2.2.0 引入 v-model 特性。由于某些原因,它的输入属性是 value,但输出事件叫 inputv-modelvalueinput 这三个名称从字面上看不到半点关系。虽然这看起来有点奇葩,但这不是重点,重点是一个控件只能双向绑定一个属性吗?

Vue 2.3.0 引入了 .sync 修饰语用于修饰 v-bind(即 :),使之成为双向绑定。这同样是语法糖,添加了 .sync 修饰的数据绑定会像 v-model 一样自动注册事件处理函数来对被绑定的数据进行赋值。这种方式同样要求子组件触发特定的事件。不过这个事件的名称好歹和绑定属性名有点关系,是在绑定属性名前添加 update: 前缀。

比如 <sub :some.sync="any" /> 将子组件的 some 属性与父组件的 any 数据绑定起来,子组件中需要通过 $emit("update:some", value) 来触发变更。

上面的示例中,使用 v-model 绑定始终感觉有点别扭,因为 v-model 的字面意义是双向绑定一个数值,而表示是否未完成的 done 其实是一个状态,而不是一个数值。所以我们再次对其进行修改,仍然使用 done 这个属性名称(而不是 value),通过 .sync 来实现双向绑定。

Vue.component("todo", {
  // ...
  props: ["text", "done"],  // <-- 恢复成 done
  data() {
    return {
      isDone: this.done  // <-- 恢复成 done
    };
  },
  methods: {
    toggle(e) {
      this.isDone = !this.isDone;
      this.$emit("update:done", this.isDone); // <-- 事件名称:update:done
    }
  }
});
<!-- #app 中其它代码略 -->
<!-- 注意 v-model 变成了 :done.sync,别忘了冒号哟 -->
<todo :text="todo.text" :done.sync="todo.done"></todo>

揭密 Vue 双向绑定

通过上面的讲述,我想大家应该已经明白了 Vue 的双向绑定其实就是普通单向绑定和事件组合来完成的,只不过通过 v-model.sync 注册了默认的处理函数来更新数据。Vue 源码中有这么一段

// @file: src/compiler/parser/index.js

if (modifiers.sync) {
  addHandler(
    el,
    `update:${camelize(name)}`,
    genAssignmentCode(value, `$event`)
  )
}

从这段代码可以看出来,.sync 双向绑定的时候,编译器会添加一个 update:${camelize(name)} 的事件处理函数来对数据进行赋值(genAssignmentCode 的字面意思是生成赋值的代码)。

展望

目前 Vue 的双向绑定还需要通过触发事件来实现数据回传。这和很多所的期望的赋值回传还是有一定的差距。造成这一差距的主要原因有两个

  1. 需要通过事件回传数据
  2. 属性(prop)不可赋值

在现在的 Vue 版本中,可以通过定义计算属性来实现简化,比如

computed: {
  isDone: {
    get() {
      return this.done;
    },
    set(value) {
      this.$emit("update:done", value);
    }
  }
}

说实在的,要多定义一个意义相同名称不同的变量名也是挺费脑筋的。希望 Vue 在将来的版本中可以通过一定的技术手段减化这一过程,比如为属性(Prop)声明添加 sync 选项,只要声明 sync: true 的都可以直接赋值并自动触发 update:xxx 事件。

当然作为一个框架,在解决一个问题的时候,还要考虑对其它特性的影响,以及框架的扩展性等问题,所以最终双向绑定会演进成什么样子,我们对 Vue 3.0 拭目以待。

感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.3water.com/code/HtmlJsRun测试上述代码运行效果。

希望本文所述对大家vue.js程序设计有所帮助。

Javascript 相关文章推荐
用jquery实现的模拟QQ邮箱里的收件人选取及其他效果(一)
Jan 06 Javascript
JS代码放在head和body中的区别分析
Dec 01 Javascript
JS在IE下缺少标识符的错误
Jul 23 Javascript
Ext4.2的Ext.grid.plugin.RowExpander无法触发事件解决办法
Aug 15 Javascript
jQuery Mobile操作HTML5的常用函数总结
May 17 Javascript
vue.js 初体验之Chrome 插件开发实录
May 13 Javascript
vue中动态绑定表单元素的属性方法
Feb 23 Javascript
Vue 将后台传过来的带html字段的字符串转换为 HTML
Mar 29 Javascript
vue进入页面时滚动条始终在底部代码实例
Mar 26 Javascript
vue项目中监听手机物理返回键的实现
Jan 18 Javascript
Vue中import from的来源及省略后缀与加载文件夹问题
Feb 09 Javascript
vue如何使用外部特殊字体的操作
Jul 30 Javascript
微信小程序中使用 async/await的方法实例分析
May 06 #Javascript
JavaScript常用工具函数大全
May 06 #Javascript
详解react组件通讯方式(多种)
May 06 #Javascript
Node.js API详解之 os模块用法实例分析
May 06 #Javascript
js实现无缝轮播图特效
May 09 #Javascript
js实现上传按钮并显示缩略图小轮子
May 04 #Javascript
js代码实现轮播图
May 04 #Javascript
You might like
域名查询代码公布
2006/10/09 PHP
php模拟asp中的XmlHttpRequest实现http请求的代码
2011/03/24 PHP
PHP通过session id 实现session共享和登录验证的代码
2012/06/03 PHP
解析:通过php socket并借助telnet实现简单的聊天程序
2013/06/18 PHP
Laravel程序架构设计思路之使用动作类
2018/06/07 PHP
关于Laravel-admin的基础用法总结和自定义model详解
2019/10/08 PHP
Laravel 6.2 中添加了可调用容器对象的方法
2019/10/22 PHP
functional继承模式 摘自javascript:the good parts
2011/06/20 Javascript
jquery 无限级联菜单案例分享
2013/03/26 Javascript
jquery实现的横向二级导航效果代码
2015/08/26 Javascript
异步JS框架的作用以及实现方法
2015/10/29 Javascript
第七篇Bootstrap表单布局实例代码详解(三种表单布局)
2016/06/21 Javascript
AngularJS 过滤器(自带和自建)详解
2016/09/19 Javascript
详解Angular的双向数据绑定(MV-VM)
2016/12/26 Javascript
除Console.log()外更多的Javascript调试命令
2018/01/24 Javascript
JavaScript设计模式之工厂模式简单实例教程
2018/07/03 Javascript
vueScroll实现移动端下拉刷新、上拉加载
2019/03/22 Javascript
Mpvue中使用Vant Weapp组件库的方法步骤
2019/05/16 Javascript
js生成1到100的随机数最简单的实现方法
2020/02/07 Javascript
使用konva和vue-konva库实现拖拽滑块验证功能
2020/04/27 Javascript
vue+vant 上传图片需要注意的地方
2021/01/03 Vue.js
python使用代理ip访问网站的实例
2018/05/07 Python
基于pip install django失败时的解决方法
2018/06/12 Python
解决pycharm运行时interpreter为空的问题
2018/10/29 Python
python logging 日志的级别调整方式
2020/02/21 Python
python使用turtle库绘制奥运五环
2020/02/24 Python
Selenium常见异常解析及解决方案示范
2020/04/10 Python
英国舒适型鞋履品牌:FitFlop
2017/05/17 全球购物
MaBelle玛贝尔香港官网:香港钻饰连锁店
2019/09/09 全球购物
美国在线肉类和海鲜配送:Crowd Cow
2020/10/02 全球购物
财务会计应届生求职信
2013/11/24 职场文书
2015年美容师个人工作总结
2015/10/14 职场文书
2016猴年开门红标语口号
2015/12/26 职场文书
写一个Python脚本下载哔哩哔哩舞蹈区的所有视频
2021/05/31 Python
MySQL count(*)统计总数问题汇总
2022/09/23 MySQL
什么是clearfix (一文搞清楚css清除浮动clearfix)
2023/05/21 HTML / CSS