Vue的数据绑定
1.MVVM 模型
MVVM旨在利用WPF中的数据绑定函数,通过从视图层中几乎删除所有GUI代码(代码隐藏),更好地促进视图层开发与模式其余部分的分离。不需要用户体验(UX)开发人员编写GUI代码,他们可以使用框架标记语言(如XAML),并创建到应用程序开发人员编写和维护的视图模型的数据绑定。角色的分离使得交互设计师可以专注于用户体验需求,而不是对业务逻辑进行编程。这样,应用程序的层次可以在多个工作流中进行开发以提高生产力。即使一个开发人员在整个代码库上工作,视图与模型的适当分离也会更加高效,因为基于最终用户反馈,用户界面通常在开发周期中经常发生变化,而且处于开发周期后期。
MVVM模式试图获得MVC提供的功能性开发分离的两个优点,同时利用数据绑定的优势和通过绑定数据的框架尽可能接近纯应用程序模型。它使用绑定器、视图模型和任何业务层的数据检查功能来验证传入的数据。结果是模型和框架驱动尽可能多的操作,消除或最小化直接操纵视图的应用程序逻辑(如代码隐藏)。
Vue 框架就受到了 MVVM 模型的影响,通过 Vue 对象将数据与页面元素绑定,由框架进行视图代码与业务逻辑代码的双向通知和同步,从而做到视图与模型的解耦。从这个角度出发,才能真正理解 Vue 的打法,从而更好的使用它。对于 Vue 来说,Vm 进行页面元素与数据对象的绑定。
2.数据的双向绑定
vue实现数据双向绑定主要是:采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty() 来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应监听回调。当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,用 Object.defineProperty() 将它们转为 getter/setter。用户看不到 getter/setter,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。
2.1 访问器属性
数据劫持通过 getter 与 setter 方法实现,getter 与 setter 将会劫持元素的获取与赋值行为:
var person = {}; var name = ''; Object.defineProperty(person,'name',{ get:function(){ return 'my name is :'+name; }, set:function(n){ name = 'set 方法赋值:'+n; } }); person.name = '斯巴达!'; console.log(person.name);
以此为基础,可在为属性赋值时修改视图元素。
叫法上虽然叫做访问拦截,但实际上 getter 与 setter 函数与其操作的属性并没有直接关系。getter 与 setter 函数与其它属性一样,是对象属性的一种,叫做访问器属性(Accessor Properties)。
访问器属性不包含数据值,它们包含一对儿 getter 和 setter 函数(不过,这两个函数都不是必需的)。在读取访问器属性时,会调用 getter 函数,在写入访问器属性时,又会调用 setter 函数
并传入新值。
-
访问器属性有如下4个特性:
-
[[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。
-
[[Enumerable]]:表示能否通过 for-in 循环返回属性。
-
[[Get]]:在读取属性时调用的函数。默认值为 undefined。
-
[[Set]]:在写入属性时调用的函数。默认值为 undefined。
-
访问器属性有点像一颗语法糖,把函数的调用方式与属性的访问方式进行了统一。利用访问器属性,除了可以在赋值和取值时增加其他动作外,也为编程增加了很多灵活性。比如可以实现属性的私有化。比如利用访问器属性结合块级作用域:
{ let fooVal = 'this is the value of foo'; var obj = { get foo() { return fooVal; }, set foo(val) { fooVal = val } } }
利用函数作用域(闭包)结合访问器属性:
function myobj(){ var fooVal = 'this is the value of foo'; return { get foo() { return fooVal; }, set foo(val) { fooVal = val } } }
便很好的实现了属性的私有化,不能直接通过对象访问属性,而是通过对象的 getter 和 setter 方法来访问属性。
另外,访问器属性还可以帮助我们更加优雅的改变原有代码:
var obj = { date: "2019.6.5" }
如果有一天,需要把 date 改为 yyyy-MM-dd 的格式,那么需要把代码中所有有赋值的地方改一遍。而使用访问器属性,可以只在定义的地方改一遍即可:
var obj = { _date: "2019-6-5", get date() { return this._date; }, set date(val) { this._date = val.split(".").join("-") } }
这样对原有代码的入侵就大大减小了。由此也可以看出访问器属性的好处,可以对数据进行更好的控制,合法性判断,格式之类的,以及关联多个属性,所以最好用访问器属性,可以为未来的扩展留下一个接口。
2.2 双向绑定的简单实现
上面介绍了访问器属性,可以通过闭包和递归来对一个对象的所有属性建立监听:
function defineReactive(data, key, val) { observe(val); // 递归遍历所有子属性 Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { return val; }, set: function(newVal) { val = newVal; } }); } function observe(data) { if (!data || typeof data !== 'object') { return; } Object.keys(data).forEach(function(key) { defineReactive(data, key, data[key]); }); };
看起来比较绕。如果换个写法:
function defineReactive(data, key, val) { observe(val); // 递归遍历所有子属性 Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { return data[key]; }, set: function(newVal) { data[key] = newVal; } }); } function observe(data) { if (!data || typeof data !== 'object') { return; } Object.keys(data).forEach(function(key) { defineReactive(data, key, data[key]); }); };
注意标红部分,赋值和取值操作将引起无限递归:
访问器属性与值属性重名将导致该结果。之前的方法可以正常执行,是因为赋值与取值已经与原本的值属性没有了任何关系。赋值与取值都是操作的 defineReactive 方法的入参 val 。val 作为闭包中的一个变量替代了原来值属性的存在。而由于 setter 与 getter 两个访问器属性会与对象一同存在,该闭包环境也将一直存在。也就是说,闭包环境中的 val 变量与原对象中的值属性的生命周期是相同的。因此拿该闭包环境的变量替换值属性是可行的。