目录
a.Object.defineProperty(obj,prop,descriptor)示例
d.Array方法数据劫持实现双向绑定示例(以push方法为例)
一、概述
JS 几个流行的框架 Vuejs、AngularJS 都使用 MVVM 模式,该模式叫做视图模型双向数据绑定,以达到数据和视图快速同步的目的。
主要体现就是表单元素值变,JS变量值改变,若页面上有输出,输出值也改变,典型的代码如下
<div id="app">
<p>{{ message }}</p>
<input v-model="message">
</div>
new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
运行效果:
二、什么是MVVM?
MVVM 就是 Model-View-ViewModel的缩写,由Model ,View,ViewModel三部分组成,它是一种前端开发的架构模式。
- Model :代表的是模型、数据,可以在 Model 层中定义数据修改和操作的业务逻辑。
- View :代表的是视图,模版,用来显示数据。
- ViewModel :连接 Model 和 View桥梁。
如下图:
在 MVVM 的架构下,View 层(DOM展示层)和 Model 层(数据对象层)并没有直接联系,而是通过 ViewModel 层进行交互。
ViewModel 层通过双向数据绑定将 View 层和 Model 层连接起来,使得 View 层和 Model 层的同步工作完全是自动的。
因此开发者只需关注业务逻辑,无需手动操作 DOM,复杂的数据状态维护交给 MVVM 统一来管理,称之为数据驱动的开发方法。
三、什么是双向数据绑定?
就是指数据传递有两个方向:
方向1:从View———>Model,View变化时,Model也发生变化。
方向2:从Model———>View,Model变化时,View也发生变化。
若要实现以上的数据传递,我们需要去监听视图(DOM)变化和监听数据的变化,才能做出相应改变。
四、监听视图(DOM)改变(View——>Model)
通过 DOM 事件就可以实现监听视图的改变,例如 input 的 change、input 事件都可以完成监听,通过事件处理器,实现对应的模型的数据改变。
如上图例子中的 input 元素的值的不断改变,就是 DOM 的改变,我们监听到之后,将模型变量 message 做同步更改!就是视图(View)改变传递到了模型(Model)上。
为什么页面的展示也变了呢?就是下一个传递了,模型改变传递到了视图上。数据的改变传递到视图上如何实现的呢?就是监听数据来实现。
五、监听模型数据(Model———>View)
上面message变量的值就是模型数据,那么模型数据的变化我们如何可以监听到呢?这就用到了数据劫持
1.什么是数据劫持?
在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改,然后返回结果。
数据劫持实现的核心API就是ES5中提供的方法Object.defineProperty()以及基于数组的数据修改方法push、pop、unshift、shift、splice、sort、reverse。
注:目前Vue中能做到响应式的数组操作方法如下:
push():在数组最后面添加元素
pop():删除数组中的最后一个元素
shift():删除数组中的第一个元素
unshift():在数组最前面添加元素
splice():删除、插入、替换
//删除元素:第二个参数传入你要删除几个元素,(如果没有传,则删除后面所有元素)
//替换元素:第二个参数表示我们要替换几个元素,后面是用于替换前面的元素
//插入元素:第二个参数传入0,并且后面跟上要插入的元素
sort():排序
reverse():反转
2.为什么要使用数据劫持?
首先在前端页面渲染中,触发渲染方案是基于事件机制实现,这种渲染的方案有个很大的弊端,就是需要通过事件监听机制触发JS事件,然后JS通过document获取需要重新渲染的DOM, 然后在js的DOM模型上修改数据,触发document渲染页面。在浏览器中document只是提供给JS操作文档模型的接口,双方通信通道资源有限,基于事件机制触发页面渲染,会消耗这个通 道的大量资源,降低浏览器性能。
下面来看看基于数据劫持实现数渲染的模型图(JS与document通讯仅仅只需要一次,而且基于虚拟DOM(https://segmentfault.com/a/1190000016647776)的支持,还可以实现最精准的 DOM渲染):
3.数据劫持实现原理
Object.defineProperty(obj,prop,descriptor):定义属性或修改原有的属性值。
参数
obj:目标对象
prop:需要定义的属性或方法的名称
descriptor:目标属性所拥有的特性
可供定义的特性列表
value: 属性的值
writable: 如果为false,属性的值就不能被重写。
get: 一旦目标属性被访问就会调回此方法,并将此方法的运算结果返回用户。
set: 一旦目标属性被赋值,就会调回此方法。
configurable: 如果为false,则任何尝试删除目标属性或修改属性性以下特性(writable, configurable, enumerable)的行为将被无效化。
enumerable: 是否能在for...in循环中遍历出来或在Object.keys中列举出来。
Object.keys(obj):返回一个由给定对象的自身可枚举属性组成的数组。
参数:要返回其枚举自身属性的对象
返回值:一个表示给定对象的所有可枚举属性的字符串数组
4.代码示例
a.Object.defineProperty(obj,prop,descriptor)示例
let oData = {name:"test"};
let val = oData["name"];
Object.defineProperty(oData,"name",{
get(){
console.log("getter...",val);
return val;
},
set(newValue){
console.log("setter...",newValue);
if(newValue == val){
return false;
}else{
val = newValue;
}
}
});
运行结果:
b.Object.keys(obj)示例
let obj = {name:"张三",age:25,address:"深圳",getName:function(){}};
let arr = [1,2,3,4,5,6];
let result1 = Object.keys(obj);
let result2 = Object.keys(arr);
console.log(result1);
console.log(result2);
运行结果:
c.Object数据劫持实现双向绑定示例
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>针对Object数据劫持实现数据双向绑定示例</title>
</head>
<body>
<input type="text" name="" id="demo"/>
<div id="show"></div>
</body>
</html>
<script>
var oDiv = document.getElementById('show');
var oInput = document.getElementById('demo');
var oData =
{
valueObj:{
value:'test'
},
name:'默认初始值'
};
//输入框事件:触发数据修改(写入)(监听view内容变化修改model值)
oInput.oninput = function(){
oData.name = this.value;
}
//修改DOM数据(页面渲染)
function upDate(){
oDiv.innerText = oData.name;
}
//初始化页面数据渲染
upDate();
//监听Model(这里就是oData对象)的方法
function Observer(data){
if(!data || typeof data != 'object'){
return data;
};
Object.keys(data).forEach(function(item){
definedRective(data,item,data[item]);
})
}
//当setter被触发时,修改数据并渲染到页面
function definedRective(data,key,val){
//使用递归深度监听对象数据变化,如对oData.valueObj.value的监听
Observer(val);
Object.defineProperty(data,key,{
get(){
return val;
},
set(newValue){
if(newValue == val) return;
val = newValue;
upDate(); //数据渲染到DOM
}
})
}
//观察oData
Observer(oData);
</script>
d.Array方法数据劫持实现双向绑定示例(以push方法为例)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>针对Array数据劫持实现数据双向绑定示例</title>
</head>
<body>
<input type="text" name="" id="demo"/>
<div id="show"></div>
</body>
</html>
<script>
var oDiv = document.getElementById('show');
var oInput = document.getElementById('demo');
let arr = ["test"];
//{push}相当于把Array对象里名字叫做push的那个属性的值拿出来赋给push
let {push} = Array.prototype;
function upArrData(){
oDiv.innerText = arr[arr.length-1];
}
upArrData();
oInput.oninput = function(){
arr.push(this.value);
}
Object.defineProperty(Array.prototype,'push',{
value:(function(){
return function(...arg){
//此处push就是上面{push}中的push
push.apply(arr,arg);
upArrData();
}
})()
});
</script>
以上就是前端架构模式MVVM及数据双向绑定原理的核心实现数据劫持,作为MVVM架构模式框架,Vue的数据双向绑定核心实现其实除了上面的数据劫持,还有另外一个,就是发布订阅模式,有兴趣的自己研究去吧。