什么是数据双向绑定?
了解数据双向绑定前,先要了解什么是MVVM模式。 (完整代码在最后)
MVVM模式
MVVM模式本质上MVC的改进版。
M 模型
模型(Model),指的是后端传递的数据。
V 视图
视图(View),指的是前端的页面。
VM 视图-模型
视图-模型(View-Model),MVVM模式的核心。它起到枢纽作用,连接着视图与模型,它的作用有两个:
- 将模型转化成视图
- 将视图转化成模型
如上图所示,将模型转换成视图使用的是 数据绑定,将视图转换成模型使用的是 DOM事件监听。
数据双向绑定
- 模型变,视图也跟着变--> DOM Listener
- 视图变,模型也跟着变--> Data Binding
需求
假设前端页面有一个输入框,有一个p标签,然后数据data中有一个name,需求是把data中的name渲染到页面中的p标签里,另外,在输入框中输入值的时候,会同步改变data中name的值和p标签里面的内容,反过来也是,改变data中name的值,输入框的值和p标签的内容也会跟着变。
<p id="myP"></p>
<input type="text" value="" id="myInput" placeholder="请输入">
<script>
const data = {
name:'张三'
}
</script>
需求拆解
上面的需求可以拆成两部分:
- 前端页面变,数据跟着变;
- 数据变,页面内容跟着变;
实现
页面变,数据变
这里使用的DOM事件监听,监听input标签,与input标签输入有关的监听事件有下面这几个:
- input事件:用户输入时会触发事件,连续触发
- change事件:在元素失去焦点时或回车时触发一次
- keydown事件:按键按下时触发 具体应该用哪一个呢?
首先是 keydown事件,这个事件是不区分按键的,所以并不适合我们这个需求,pass掉。
其次是change事件,数据双向绑定有一个特点是双向同步,而change事件触发的时候我们的数据可能已经变化好多次了,不能做到同步,所以也pass掉。
最后就只剩下input事件了,连续触发,很适合,所以我们就来监听输入框的input事件。
//监听input事件
let input = document.getElementById('myInput');
input.addEventListener('input',function(e){
console.log(e);
})
监听到input事件,我们就可以把我们输入的值同步给data中的name了。
const data = {
name: '张三'
}
//监听input事件
let input = document.getElementById('myInput');
input.addEventListener('input', function (e) {
data.name = input.value;
})
至此,我们就实现了视图变,模型变了,接下来使用数据绑定实现数据变,页面变。
数据变,页面变
这一步核心是ES5新增的方法 Object.defineProperty()
Object.defineProperty()
该方法的作用就是直接在一个对象上定义一个新属性或者修改一个已经存在的属性。
语法
Object.defineProperty(obj, prop, desc)
- obj 需要定义属性的当前对象
- prop 当前需要定义的属性名
- desc 属性描述符
前两个参数很容易理解,第三个参数则需要进一步研究了。
上面的图片列出了6个属性描述符,其中 value,writable 为数据描述符,get,set 为存取描述符,也是我们今天的主角。
get 取值函数
意思就是,当我们访问我们指定的对象的指定属性的时候,就会触发对应的事件,比如上面的data中的name,假设当我们访问name的时候,希望可以打印出一句话“你正在访问data.name”,就可以使用get来实现。
//数据绑定
Object.defineProperty(data,'name',{
get:function(){
console.log('你正在访问data.name');
}
});
上图可以看出,当我们想获取data中的name属性的值的时候,触发了事件,但是同样出现了一个问题,就是我们没能成功获取到name的值,其实数据绑定还有个叫法“数据劫持”,我们获取数据的操作被劫持了,所以自然获取不到值了,那怎么办呢?
这里使用的解决办法是:在访问前,复制一个临时变量,存储data.name(不要复制data,因为data是个对象,复制出来的是引用复制,一旦访问对应属性还是会触发get函数)。
注意,在get函数中千万不能再访问指定的对象的指定属性,不然会死循环。
Object.defineProperty(data,'name',{
get:function(){
console.log('你正在访问data.name');
console.log(data.name);//这样搞就会死循环,原因很容易理解,就不细说了
}
});
所以我们要先声明一个临时变量name,然后再在get函数中访问临时变量中的数据,进行操作。
let name = data.name;
Object.defineProperty(data,'name',{
get:function(){
console.log('你正在访问data.name');
return name;
}
});
set存值函数
这个修饰符的意思是:当我们修改指定对象的指定属性的时候就会触发对应的事件。 函数的参数是该属性要改变的值。
借助这个特性,我们可以在修改属性值的时候,同步改变input的值和p标签的内容。
let name = data.name;
Object.defineProperty(data, 'name', {
get: function () {
console.log('你正在访问data.name');
return name;
},
set: function (val) {
p.innerHTML = val;
input.value = val;
name = val;
}
});
完整代码
<p id="myP"></p>
<input type="text" value="" id="myInput" placeholder="请输入">
<script>
const data = {
name: '张三'
}
let input = document.getElementById('myInput');//获取input标签
let p = document.getElementById('myP');//获取p标签
//将数据渲染到页面
p.innerHTML = data.name;
input.value =data.name;
//监听input事件
input.addEventListener('input', function (e) {
data.name = input.value;
})
//数据绑定
let name = data.name;
Object.defineProperty(data, 'name', {
get: function () {
console.log('你正在访问data.name');
return name;
},
set: function (val) {
p.innerHTML = val;
input.value = val;
name = val;
}
});
</script>
完结
这是最基本的双向绑定,整篇文章的重点是 Object.defineProperty(),弄懂这个基本就懂整篇文章在说什么了。