有趣的地方

有趣的地方

Vue2 响应式原理

Vue 的响应式原理

Vue 的响应式原理基于"数据劫持"和"依赖收集"的概念。当我们将一个普通的 JavaScript 对象传递给 Vue 实例的 data 选项时,Vue 将遍历此对象的所有属性,并使用 Object.defineProperty()来对每个属性进行 getter 和 setter 的重写,数据变化时能够触发视图更新。

数据劫持

Vue 通过使用 Object.defineProperty 方法对数据对象进行数据劫持。它会重写对象的属性访问器(getter和setter),使得当属性被读取或修改时,Vue 能够捕捉到这一操作,并触发相应的更新。

  下面是一个简单的例子,展示了如何将一个普通对象转化为响应式数据对象:

function defineReactive(data, key, value) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get() {
      console.log(`读取属性 ${key}: ${value}`);
      return value;
    },
    set(newValue) {
      console.log(`设置属性 ${key}: ${newValue}`);
      value = newValue;
    },
  });
}

const obj = {};
defineReactive(obj, 'message', 'Hello, Vue!');
console.log(obj.message);  // 读取属性 message: Hello, Vue!
obj.message = 'Hello, World!';  // 设置属性 message: Hello, World!

在上述代码中,我们定义了 defineReactive 函数,它使用 Object.defineProperty 对属性进行劫持。当属性被读取时,会打印相应的信息,当属性被修改时,也会打印对应的信息。

依赖收集

在 Vue 的响应式系统中,依赖收集是指收集数据属性的依赖关系,也就是说,当一个属性被使用时,Vue 会追踪到这个属性的依赖,并建立起一个关联关系。这样,当依赖的属性发生变化时,Vue 就能够知道哪些地方需要更新。

Vue 通过使用"观察者"和"依赖"的概念来实现依赖收集。每个被劫持的属性都会关联一个"Dep"对象,它负责追踪所有依赖于该属性的"观察者"。当属性被修改时,"Dep"对象会通知所有相关的"观察者"进行更新操作。

  下面是一个简化的例子,展示了如何实现一个简单的观察者和依赖关系:

// dep 是个可观察对象,可以有多个指令订阅它
class Dep {
  constructor() {
    this.subscribers = [];
  }

  // 将观察对象和 watcher 建立依赖
  depend() {
    if (Dep.target && !this.subscribers.includes(Dep.target)) {
      this.subscribers.push(Dep.target);
    }
  }
  // 发布通知
  notify() {
    // 调用每个订阅者的 update 方法实现更新
    this.subscribers.forEach(subscriber => subscriber.update());
  }
}
// Dep.target 用来存放目前正在使用的 watcher
// 全局唯一,并且一次也只能有一个 watcher 被使用
Dep.target = null;

class Watcher {
  constructor(updateCallback) {
    this.updateCallback = updateCallback;
  }

  update() {
    this.updateCallback();
  }
}

function defineReactive(data, key, value) {
  const dep = new Dep();
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get() {
      // 建立依赖
      dep.depend();
      return value;
    },
    set(newValue) {
      value = newValue;
      // 通知订阅者
      dep.notify();
    },
  });
}

const obj = {};
defineReactive(obj, 'message', 'Hello, Vue!');

function render() {
  console.log(obj.message);
}

Dep.target = new Watcher(render);
obj.message = 'Hello, World!';  
// 输出: Hello, World!

在上述代码中,我们定义了 Dep 类作为依赖追踪的容器,它维护了一个订阅者列表。每个被劫持的属性都会对应一个 Dep 实例,用于收集依赖和通知更新。当属性被读取时,会调用dep.depend()方法来收集依赖,将当前的观察者(Dep.target)添加到订阅者列表中。当属性被修改时,会调用dep.notify()方法来通知所有相关的观察者进行更新。

另外,我们定义了 Watcher 类作为观察者,它接收一个更新回调函数。在 render 函数中,我们创建了一个 Watcher 实例,并将render函数作为更新回调函数传递给它。然后,将 Dep.target 设置为当前的观察者,再修改 obj.message 属性的值。这样,当 obj.message 属性发生变化时,Dep 会通知到相关的观察者,触发相应的更新操作。

Vue的响应式原理处理数组和对象的变化

数组的变化

  当对数组进行变异操作(如push、pop、shift、unshift、splice、sort、reverse等)时,Vue能够捕获到这些变化,并触发视图的更新。

  Vue通过重写数组的变异方法,即对这些方法进行了劫持,来实现对数组的监听和触发更新。这些变异方法有以下特点:

  • 它们会修改原始数组本身,而不是返回一个新的数组
  • 它们被重写成能够触发更新的形式

  举个例子,当我们使用 push 方法向数组中添加元素时,Vue会捕获到这个变化并触发相应的更新:

data: {
  items: []
}

// 将一个新元素添加到数组中
this.items.push('new item');

当调用push方法时,Vue会检测到这个变化,并触发视图的更新,以显示新的数组内容。

需要注意的是,对数组进行以下操作时,Vue无法自动触发更新:

  • 通过索引直接修改数组元素的值:this.items[index] = newValue
  • 修改数组的长度:this.items.length = newLength

  对于上述情况,我们可以使用以下方法来触发更新:

// Vue.set 方法
Vue.set(this.items, index, newValue);

// this.$set 方法
this.$set(this.items, index, newValue);

// 或者使用 splice 方法
this.items.splice(index, 1, newValue);

通过上述方法,我们可以通知 Vue 进行更新。

对象的变化

  对于对象的变化,Vue 的响应式系统使用了类似的方法进行劫持。

  当我们修改对象的属性值时,Vue 能够捕获到这个变化并触发更新。这是因为 Vue 在对象上使用了 Object.defineProperty 来重写属性的访问器(getter和setter),从而能够追踪对象属性的变化。

data: {
  user: {
    name: 'John',
    age: 25
  }
}

// 修改对象的属性值
this.user.name = 'Jane';

当我们修改 user 对象的 name 属性时,Vue 能够捕获到这个变化并触发相应的更新。

需要注意的是,如果要在响应式对象上添加新的属性,需要使用以下方法:

// 使用Vue.set方法
Vue.set(this.user, 'address', '123 Main St');

// 或者使用扩展运算符
this.user = { ...this.user, address: '123 Main St' };

通过上述方法,我们可以让新添加的属性也具有响应性,Vue能够追踪到这个变化并触发更新。

最后
  通过数据劫持和依赖收集,Vue 的响应式系统能够追踪数据的变化并自动更新相应的UI。这使得开发者能够以声明式的方式来处理数据,而不需要手动操作DOM。在实际的Vue应用中,这个响应式原理被广泛应用于数据绑定、计算属性、侦听器等方面,极大地简化了开发流程。

参考:面试常问之Vue响应式原理

发表评论:

Powered By Z-BlogPHP 1.7.3

© 2018-2020 有趣的地方 粤ICP备18140861号-1 网站地图