Vue2 响应式原理

一、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 响应式系统的具体实现流程:

  1. 创建一个 Observer 对象,它的主要作用是给对象的每个属性添加 getter 和 setter 方法。
  2. 在 getter 和 setter 方法中分别进行依赖的收集和派发更新。
  3. 创建 Watcher 对象,用于监听数据的变化,当数据发生任何变化时,Watcher 对象会触发自身的回调函数。
  4. 在模板解析阶段,对模板中使用到的数据进行依赖的收集,即收集 Watcher 对象。
  5. 当数据发生变化时,Observer 对象会通知 Dep 对象调用 Watcher 对象的回调函数进行更新操作,即派发更新。
  6. 更新完毕后,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 对象的回调函数进行更新。如此循环,实现了数据的响应式。

  • 3
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值