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

剖析Vue原理&实现双向绑定MVVM


<div id="app">
<h2>What's your name:</h2>
<input v-model="name" />
<div>Hello {{ name }}</div>
</div>

new Vue({
el: "#app",
data: {
name: ""
}
});

JsFiddle 演示

https://jsfiddle.net/0okxhc6f/

在这个示例的输入框中输入的内容,会随后呈现出来。这是 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() 在脚本中定义,主要是把

  • 内容中的那部分拎出来:


    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 }
    ]
    }
    });

    JsFiddle 演示

    https://jsfiddle.net/0okxhc6f/1/

    不过到此为止,数据仍然是单向的。从效果上来看,点击复选框可以反馈出删除线线效果,但这些动态变化都是在 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` 同属表达式。

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

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

    Js Fiddle 演示

    https://jsfiddle.net/0okxhc6f/2/

    改造成 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: 前缀。

    比如 将子组件的 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>

    Js Fiddle 演示

    https://jsfiddle.net/0okxhc6f/3/

    揭密 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 拭目以待。

    原文地址:https://segmentfault.com/a/1190000016593014

    揭密 Vue 的双向绑定的更多相关文章

    1. Vue.js双向绑定的实现原理

      Vue.js最核心的功能有两个,一是响应式的数据绑定系统,二是组件系统.本文仅探究几乎所有Vue的开篇介绍都会提到的hello world双向绑定是怎样实现的.先讲涉及的知识点,再参考源码,用尽可能少 ...

    2. Vue.js双向绑定的实现原理和模板引擎实现原理(##########################################)

      Vue.js双向绑定的实现原理 解析 神奇的 Object.defineProperty 这个方法了不起啊..vue.js和avalon.js 都是通过它实现双向绑定的..而且Object.obser ...

    3. vue的双向绑定原理及实现

      前言 使用vue也好有一段时间了,虽然对其双向绑定原理也有了解个大概,但也没好好探究下其原理实现,所以这次特意花了几晚时间查阅资料和阅读相关源码,自己也实现一个简单版vue的双向绑定版本,先上个成果图 ...

    4. 西安电话面试:谈谈Vue数据双向绑定原理,看看你的回答能打几分

      最近我参加了一次来自西安的电话面试(第二轮,技术面),是大厂还是小作坊我在这里按下不表,先来说说这次电面给我留下印象较深的几道面试题,这次先来谈谈Vue的数据双向绑定原理. 情景再现: 当我手机铃声响 ...

    5. Vue数据双向绑定原理及简单实现

      嘿,Goodgirl and GoodBoy,点进来了就看完点个赞再go. Vue这个框架就不简单介绍了,它最大的特性就是数据的双向绑定以及虚拟dom.核心就是用数据来驱动视图层的改变.先看一段代码. ...

    6. vue数据双向绑定

      Vue的双向绑定是通过数据劫持结合发布-订阅者模式实现的,即通过Object.defineProperty监听各个属性的setter,然后通知订阅者属性发生变化,触发相应的回调. 整个过程分为以下几步 ...

    7. 【学习笔记】剖析MVVM框架,简单实现Vue数据双向绑定

      前言: 学习前端也有半年多了,个人的学习欲望还比较强烈,很喜欢那种新知识在自己的演练下一点点实现的过程.最近一直在学vue框架,像网上大佬说的,入门容易深究难.不管是跟着开发文档学还是视频教程,按步骤 ...

    8. vue数据双向绑定原理

      vue的数据双向绑定的小例子: .html <!DOCTYPE html> <html> <head> <meta charset=utf-> < ...

    9. Vue.js双向绑定原理

      Vue.js最核心的功能有两个,一个是响应式的数据绑定系统,另一个是组件系统.本文仅仅探究双向绑定是怎样实现的.先讲涉及的知识点,再用简化的代码实现一个简单的hello world示例. 一.访问器属 ...

    随机推荐

    1. rancher中级(二)(rancher中添加证书及操作虚拟主机)

      制作一个ssl证书 首先了解关于ssl证书的背景知识:http://www.cnblogs.com/zxj015/p/4458066.html SSL证书包括: 1,CA证书,也叫根证书或者中间级证书 ...

    2. 记录一个调试REST风格的web服务的client

      coogle浏览器的advanced rest client很好用,记录一下,脑子不好,容易忘,,可以在chrome 的网上应用店添加 Rest client是用来调试REST风格的Web服务,接收P ...

    3. 使用tortoise git将一个现有项目推送到远程仓库

      一.安装文件: 1.git https://git-scm.com/downloads 2.tortoise git https://tortoisegit.org/download/ 二.将一个现有 ...

    4. Hive 环境的安装部署

      Hive在客户端上的安装部署 一.客户端准备: 到这我相信大家都已经打过三节点集群了,如果是的话则可以跳过一,直接进入二.如果不是则按流程来一遍! 1.克隆虚拟机,见我的博客:虚拟机克隆及网络配置 2 ...

    5. log(A^B) = BlogA

      令 x = logA, y = logB, z=log(AB) .2x = A, 2y = B, 2z = AB, 则有 2z = AB = (2x)^(2y) = 2x(2^y) ,有z = x*2 ...

    6. discuz迁移到虚拟空间后无法上传图片的问题

      discuz X3迁移到虚拟空间后无法上传图片,提示"附件无法保存": 解决方法: 1.看看虚拟空间的容量是不是满了. 2.登录管理员后台,工具->更新缓存.

    7. Primefaces dataTable设置某个cell的样式问题

      设置primefaces dataTable的源网段列的Cell可以编辑,当回车键保存时,判断是否输入的网段合法,如果不合法就显示警告信息,并将这个不合法的数据用红色表示.问题是,怎么给这一个cell ...

    8. 工作经验(JNI篇)

      我的工作是C++开发,主要是做底层的,由于要做跨平台的原因,常会做成JNI给Java调用,下面是工作时总结的经验希望有用 JNI只能使用C语言的方式编译,所以,要使用C++的话,要用 extern & ...

    9. linux cached过高导致性能变低

      场景: 拿到了客户50个文件,平均每个文件大概40M左右的txt,文件在S3上,需要导入到数据库,40M解析出来大概是80W条左右的数据. 描述: 在刚开始执行导入时,因为数据验证复杂程度不同,每个文 ...

    10. Backbone源码风格

           代码风格: 一.自执行匿名函数创建执行环境 var root = this; root保存全局执行环境的指针.浏览器端为window对象 二.依赖库 (1).underscore 如果bac ...