一、MVVM模式
MVVM是一种前端的架构模式,其中M代表数据模型,V代表视图,而VM则代表着ViewModel。MVVM模式基于MVC(Model-View-Controller)和MVP(Model-View-Presenter)模式进一步演变而来,旨在将应用程序的用户界面分离出来,使设计、开发和维护过程更加清晰有序。
在MVVM模式中,ViewModel起到了一个“粘合剂”的作用。它连接视图与数据,维护了视图状态,并对视图进行处理。ViewModel中包含所有的视图逻辑和交互逻辑,实现双向绑定,使得当数据发生改变时,视图也跟随变化,反之亦然。视图和ViewModel之间形成了一个强耦合关系,但是ViewModel只有对数据操作的权限,对视图的操作只能通过属性更改和命令触发来实现。
在MVVM模式中,视图只需要负责展示数据以及响应用户事件,在不涉及具体业务逻辑的情况下通知ViewModel进行数据更新。这种形式简化了视图复杂度,减少了业务逻辑与视图产生直接关联的可能。数据模型则是整个程序的业务数据管理中心,负责数据的增删改查。
总之,MVVM模式在前端领域广泛应用,其优点包括:
- 易于维护和开发:MVVM模式的分层架构清晰明了,使得开发人员可以更加聚焦于业务逻辑的开发,降低代码复杂性,方便维护。
- 双向绑定:MVVM模式将视图和数据模型通过ViewModel实现双向绑定,大大提高了用户体验。
- 适合大规模单页面应用:由于前端场景下常常存在大量交互逻辑,而MVVM的双向绑定使得这类应用变得容易维护、扩展和升级。
用一句话概括就是,数据驱动视图,视图随数据改变而改变。
二、Object.defineProperty()
Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。
Object.defineProperty() 是一个可以用于修改对象属性特性(property descriptor)的方法。属性特性是指一个属性所拥有的一些元信息,例如属性值(value)、可枚举性(enumerable)、可写性(writable)以及可配置性(configurable)等。
这个方法接收三个参数:要进行操作的对象、要定义或修改的属性名称和属性描述符对象。属性描述符对象的常见键值包括:
- value:属性的值。
- writable:表明该属性是否可以被赋值运算符重新赋值,默认为false。
- enumerable:表明该属性是否可以被for..in循环或者Object.keys()函数遍历到,默认为false。
- configurable:表明该属性是否可以被删除或者重新定义(修改特性),默认为false。
- get和set:分别是获取和设置该属性时的自定义处理函数,可以留空。当调用该属性读取器(get)时,会触发对应的get函数;而当调用该属性写入器(set)时,会触发对应的set函数。
以下是一个简单的示例,演示如何使用Object.defineProperty()方法创建或修改对象的属性特性:
const obj = {};
// 使用Object.defineProperty()方法定义一个只读属性
Object.defineProperty(obj, 'name', {
value: 'John',
writable: false,
enumerable: true,
configurable: false
});
console.log(obj.name); // "John"
obj.name = 'Tom'; // 此操作无效,因为该属性是只读的
// 使用Object.defineProperty()方法定义一个计算属性
const firstName = 'John';
const lastName = 'Doe';
Object.defineProperty(obj, 'fullName', {
get: function() {
return `${firstName} ${lastName}`;
},
set: function(value) {
[firstName, lastName] = value.split(' ');
},
enumerable: true,
configurable: false
});
console.log(obj.fullName); // "John Doe"
obj.fullName = 'Tom Lee'; // 此操作会触发set函数,将firstName和lastName分别设置为"Tom"和"Lee"
console.log(obj.fullName); // "Tom Lee"
在上述示例中,我们使用Object.defineProperty()方法为一个空对象添加了两个属性:一个是只读的name,一个是计算属性fullName。可以看到,通过该方法,我们可以更加精细地控制对象属性的可读性、可写性、可枚举性和可配置性等特性,并且可以为一个属性自定义读取器(get)和写入器(set)等逻辑处理。
Vue2 的响应式
Vue2 响应式系统的设计思路主要依赖于 JavaScript 语言特性 Object.defineProperty,通过该特性实现对对象数据的劫持,从而实现数据的响应式。
具体来说,通过 Object.defineProperty 定义一个对象属性,可以在 get 和 set 方法中进行各种操作,例如收集依赖、派发更新。
以下是 Vue2 响应式系统的具体实现流程:
- 创建一个 Observer 对象,它的主要作用是给对象的每个属性添加 getter 和 setter 方法。
- 在 getter 和 setter 方法中分别进行依赖的收集和派发更新。
- 创建 Watcher 对象,用于监听数据的变化,当数据发生任何变化时,Watcher 对象会触发自身的回调函数。
- 在模板解析阶段,对模板中使用到的数据进行依赖的收集,即收集 Watcher 对象。
- 当数据发生变化时,Observer 对象会通知 Dep 对象调用 Watcher 对象的回调函数进行更新操作,即派发更新。
- 更新完毕后,Vue2 会进行视图的重新渲染,从而实现响应式。
下面是一个基于 Object.defineProperty 实现响应式的示例,仅供参考:
function observe(obj) {
if (!obj || typeof obj !== 'object') {
return;
}
Object.keys(obj).forEach(key => {
// 尝试递归处理
observe(obj[key]);
let val = obj[key];
const dep = new Dep(); // 新建一个依赖
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if (Dep.target) {
dep.depend(); // 收集依赖
}
return val;
},
set(newVal) {
if (newVal === val) {
return;
}
val = newVal;
dep.notify(); // 派发更新
}
});
});
}
// 依赖类
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
removeSub(sub) {
const index = this.subs.indexOf(sub);
if (index !== -1) {
this.subs.splice(index, 1);
}
}
depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
Dep.target = null;
// 观察者类
class Watcher {
constructor(vm, expOrFn, callback) {
this.vm = vm;
this.getter = parsePath(expOrFn);
this.callback = callback;
this.value = this.get(); // 初始化,触发依赖
}
get() {
Dep.target = this; // 设置当前依赖
const value = this.getter.call(this.vm, this.vm); // 触发 getter
Dep.target = null; // 清除当前依赖
return value;
}
addDep(dep) {
dep.addSub(this);
}
update() {
const oldValue = this.value;
this.value = this.get(); // 重新获取
this.callback.call(this.vm, this.value, oldValue); // 触发回调
}
}
// 解析路径
function parsePath(expOrFn) {
if (typeof expOrFn === 'function') {
return expOrFn;
}
const segments = expOrFn.split('.');
return function(obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) {
return;
}
obj = obj[segments[i]];
}
return obj;
};
}
// 测试
const obj = { foo: 'foo', bar: { a: 1 } };
observe(obj);
new Watcher(obj, 'foo', (val, oldVal) => {
console.log(`foo changed from ${oldVal} to ${val}`);
});
new Watcher(obj, 'bar.a', (val, oldVal) => {
console.log(`bar.a changed from ${oldVal} to ${val}`);
});
obj.foo = 'FOO'; // 输出 `foo changed from foo to FOO`
obj.bar.a = 2; // 输出 `bar.a changed from 1 to 2`
以上代码中,函数 observe
用于递归遍历对象属性,把其进行劫持,包括收集依赖和派发更新;类 Dep
代表一个依赖,其中 addSub
用于添加观察者实例,removeSub
用于移除观察者实例,depend
用于收集依赖,即把当前依赖加到对应的观察者中,notify
用于派发更新,即遍历所有观察者,并触发其回调函数。类 Watcher
则代表一个观察者,其中 getter
用于获取数据,callback
用于回调函数,addDep
用于添加依赖,即把当前观察者添加到对应的依赖中,update
用于更新值,并触发相应的回调函数,如有必要。函数 parsePath
则用于解析路径字符串,返回对应属性的值。
例子中我们对对象 obj
进行了劫持,同时创建了两个观察者,分别对应 foo
和 bar.a
两个属性。当其中任意一个属性的值发生变化时,其对应的依赖都会被更新,从而触发其绑定的观察者的回调函数。
简单来说,在 Vue2 响应式系统中,当数据发生改变时,会触发 get 和 set 方法,get 方法会收集所有依赖该数据的 Watcher 对象,set 方法会通知 Dep 对象触发所有 Watcher 对象的回调函数进行更新。如此循环,实现了数据的响应式。