最近在面试中被问到了数据的双向绑定是如何实现的。当时只记得学习 Vue.js 时,有一个 v-model 指令能够实现数据双向绑定,实现的原理涉及到了事件监听。所以没有给出一个好的答案。
1. Vue.js 中的数据双向绑定
1.1 v-model 指令
Vue 官网中是这样描述 v-model 的:
你可以用
v-model指令在表单<input>、<textarea>及<select>元素上创建双向数据绑定。它会根据控件类型自动选取正确的方法来更新元素。尽管有些神奇,但v-model本质上不过是语法糖。它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。
下例是使用 v-model 在 <input> 元素上创建双向绑定:
1
2
<input v-model="message" placeholder="edit me" />
<p>Message is: { { message } }</p>
1.2 getter/setter
Vue 官网中介绍响应式原理时,有这样一句话:
当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用
Object.defineProperty把这些属性全部转为getter/setter。Object.defineProperty是 ES5 中一个无法 shim 的特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器。
这涉及到了 ES5 中定义的两种属性:数据属性和访问器属性。
1.2.1 数据属性
数据属性包含一个数据值的位置。它有 4 个描述其行为的特性:
Configurable: 表示能够通过delete删除,以及能否被修改为访问器属性。默认为true。Enumerable: 表示能够通过for-in循环返回。默认为true。Writable: 表示能否修改属性的值。默认为true。Value: 包含这个属性的数据值。默认为undefined。读取属性值时,从这个位置读;写入属性的时候,把新值保存在这个位置。
要修改属性的默认特性,需要使用 Object.defineProperty():
1
2
3
4
5
6
7
8
var obj = {};
Object.defineProperty(obj, "name", {
configurable: true,
enumerable: true,
writable: true,
value: "nie"
});
console.log(obj.name); // nie
注意:通过 Object.defineProperty 创建新属性时,如果不指定,前三个特性默认都是 false。
1.2.2 访问器属性
访问器属性不包含数据值;它们包含一对 getter 和 setter 函数。
- 在读取访问器属性时,调用
getter; - 在写入访问器属性时,调用
setter;
访问器属性也有 4 个特性:
Configurable: 表示能够通过delete删除,以及能否被修改为访问器属性。默认为true。Enumerable: 表示能够通过for-in循环返回。默认为true。Get: 读取属性时调用的函数。默认为undefined。Set: 写入属性时调用的函数。默认为undefined。
访问器属性不能直接定义,需要使用 Object.defineProperty():
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var obj = {
_name: "nie"
};
Object.defineProperty(obj, "name", {
get: function() {
return this._name;
},
set: function(newName) {
this._name = newName;
}
});
console.log(obj.name);// nie
obj.name = "nxj";
console.log(obj.name);// nxj
1.3 简单实现双向绑定
下面的例子中,我们使用 Object.defineProperty() 定义访问器属性简单地实现了在 <input> 元素上进行数据双向绑定。
-
我们给
<input>元素添加一个监听键盘按键松开的事件处理函数:当监听到键盘按键松开时,把<input>元素中的值赋值给data.inputValue。 -
这将导致
inputValue的Set函数被调用,在此函数中,我们把传入的新值赋值给<input>元素的value属性和<span>元素的innerHTML属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<body>
<input id="input1" />
<div>输入框内容为:<span id="span1"></span></div>
<script>
var input = document.getElementById("input1");
var span = document.getElementById("span1");
var data = { _inputValue: "" };
Object.defineProperty(data, "inputValue", {
get: function() {
return this._inputValue;
},
set: function(newValue) {
this._inputValue = newValue;
input.value = newValue;// 修改 input 输入框的值
span.innerHTML = newValue;// 修改 span 中的值
}
});
input.onkeyup = function(event) {
// 监听 keyup 事件,给 data.inputValue 赋值,调用其 set 方法
data.inputValue = event.target.value;
};
</script>
</body>
最终双向绑定的效果是:
- 我们在
<input>元素中输入内容时,data.inputValue的值也会随之变化,我们可以在控制台输入data.inputValue按回车查看其值; - 我们在控制台输入
data.inputValue = 'new Value',改变inputValue的值后,<input>元素和<span>元素的值也会发生变化。