likes
comments
collection

(Vue)双向数据绑定原理

作者站长头像
站长
· 阅读数 62

想象你有一面镜子,这面镜子代表了你的用户界面(UI)。现在,你站在这面镜子前,你自己是数据模型中的数据,而镜子上的反射则代表 UI 中显示的内容。这个反射过程是单向的,因为只有你能够改变镜子上的影像,而镜子上的变化不会影响到你自己。

然而,Vue 的双向数据绑定就像是在你和这面镜子之间添加了一个巧妙的机制。这个机制允许你不仅可以改变镜子上的影像,反过来,镜子上的变化也会实时地反映到你自己身上。这就是所谓的双向数据绑定。

现在,让我们来看看这个过程的具体步骤:

1. 数据劫持(Observer)阶段:

在 Vue 的初始化阶段,Vue 会对数据进行劫持(Observer),这个过程就是通过递归遍历对象,包括其子属性对象,为每个属性都添加 getter 和 setter。这样一来,当你访问或者修改这些属性的值时,就会触发相应的 getter 和 setter 操作,从而实现对数据的监听。

具体来说,Vue 使用一个叫做 Observer 的类来实现对数据的劫持。当 Vue 初始化时,会对数据对象调用 Observer 的构造函数,这个构造函数会遍历对象的每个属性,为每个属性添加 getter 和 setter。如果属性值是对象,那么会递归调用 Observer 构造函数,实现对子属性的劫持。

下面是一个简化的示例,用于说明这个过程:

function observe(obj) {
  // 判断 obj 是否为对象
  if (typeof obj !== 'object' || obj === null) {
    return;
  }

  // 遍历对象的每个属性
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key]);
  });
}

function defineReactive(obj, key, val) {
  // 递归劫持子属性
  observe(val);

  Object.defineProperty(obj, key, {
    get() {
      console.log(`访问属性 ${key}: ${val}`);
      return val;
    },
    set(newVal) {
      if (newVal !== val) {
        console.log(`设置属性 ${key},新值为: ${newVal}`);
        val = newVal;
        // 数据变化时,执行更新操作
      }
    },
  });
}

// 示例数据
const data = {
  name: 'John',
  age: 30,
  address: {
    city: 'New York',
  },
};

// 对数据进行劫持
observe(data);

// 访问数据,触发 getter
console.log(data.name); // 输出: 访问属性 name: John

// 修改数据,触发 setter
data.age = 31; // 输出: 设置属性 age,新值为: 31

// 修改子属性,同样触发 setter
data.address.city = 'San Francisco'; // 输出: 设置属性 address,新值为: { city: 'San Francisco' }

在这个例子中,observe 函数用于判断对象是否为可观察对象,然后通过 defineReactive 函数为对象的每个属性添加 getter 和 setter。递归劫持子属性确保了对嵌套对象的监听。这样,当访问或者修改数据时,就会触发相应的 getter 和 setter 操作,使得 Vue 能够捕捉到数据的变化。

2. 模板解析和编译(Compile)阶段:

在 Vue 的编译阶段,Vue 会对模板进行解析,找到其中的动态绑定表达式,然后建立起数据与 UI 元素之间的关联关系。具体的步骤如下:

  1. 模板解析: Vue 会遍历模板,识别其中的指令、插值表达式等动态内容。指令如 v-bindv-model,插值表达式如 {{ }} 都会被识别出来。
  2. 指令处理: 对于每个识别出来的指令,Vue 会进行相应的处理。比如,v-bind 用于属性绑定,v-model 用于双向绑定,{{ }} 用于文本插值。
  3. 初始化渲染: Vue 在初始化渲染时会将模板中的静态内容直接渲染到页面上,而对于动态内容,Vue会根据数据的当前值进行渲染。这时,数据和视图已经建立了一种初始的关联。
  4. Watcher(观察者)创建: 对于包含动态内容的节点,Vue 会创建一个 Watcher 对象。这个 Watcher 对象负责维护视图和数据的关系,以及在数据变化时触发更新。
  5. 绑定更新函数: Watcher 对象会将一个更新函数与当前节点绑定。这个更新函数负责在数据变化时更新节点的内容,确保视图与数据保持同步。 在 Vue 中,更新函数是一个用于更新视图的函数。当数据发生变化时,Watcher 会通知相关的更新函数执行,以确保视图中的内容与数据模型保持同步。

具体来说,更新函数是由 Vue 编译模板时生成的。在编译阶段,对于包含动态内容的节点,Vue 会为每个节点创建一个 Watcher 对象,并与一个更新函数关联。这个更新函数负责将最新的数据渲染到页面上。


在更新函数中,通常会包含一些逻辑,用于处理动态内容的变化。例如,对于文本节点,更新函数可能会将节点的 textContent 更新为最新的数据值;对于表单元素,更新函数可能会将元素的值更新为最新的数据值。总之,更新函数的主要责任是确保视图中的内容与数据模型保持一致。

下面是一个简化的例子,用于说明更新函数的概念:

// 假设这是一个包含动态内容的节点
const dynamicNode = document.getElementById('dynamic-node');

// 更新函数示例
function updateFunction(newData) {
  // 更新节点的内容为最新的数据值
  dynamicNode.textContent = newData;
}

// Watcher 对象关联更新函数
const watcher = new Watcher(updateFunction);

// 数据发生变化时,通知 Watcher 执行更新
data.value = 'New Value';
watcher.notify(data.value); // 这里的 data.value 是模拟的最新数据值

在这个例子中,updateFunction 就是更新函数。当数据发生变化时,watcher.notify(data.value) 会通知 Watcher 执行更新函数,并将最新的数据值传递给更新函数。这个更新函数就会负责将最新的数据渲染到页面上。

需要注意的是,实际的更新函数是由 Vue 自动生成的,具体内容取决于模板中动态内容的类型和位置。 Vue 会确保这些更新函数能够正确地处理不同类型的动态内容,使得视图能够及时地响应数据变化。

总体来说,在编译阶段,Vue会通过解析模板识别出动态内容,然后初始化渲染页面视图,并为每个动态内容创建一个 Watcher 对象。这个 Watcher 对象负责维护数据和视图的关系,以及在数据变化时更新视图。通过这种方式,Vue建立了数据与UI元素之间的关联关系,并确保它们能够保持同步。

3.Watcher订阅者:

Watcher充当Observer和Compile之间的桥梁。它在实例化时会添加自己到属性的订阅器(dep)中,同时具有update方法。当数据变动时,dep会通知相关的Watcher,触发update方法,从而实现更新视图的操作。

在Vue的响应式系统中,属性的订阅器通常被称为“Dep”(Dependency,依赖)。Dep是一种用于收集和管理依赖关系的容器,它与每个被观察的数据属性相关联。每个被劫持(响应式化)的属性都会有一个对应的Dep实例。

具体来说,Dep的作用包括:

  1. 收集依赖(添加订阅者): Dep可以包含多个Watcher,每个Watcher代表一个视图组件或其他依赖。当属性被读取(被访问)时,Watcher会被添加到Dep中,建立依赖关系。
  2. 通知变化: 当属性被修改时,Dep会通知所有依赖于该属性的Watcher执行更新操作,确保视图等相关部分能够及时更新。

在实现上,每个被观察的属性都有一个对应的Dep实例。属性的getter和setter函数中,会与当前正在执行的Watcher建立关联。当属性被读取时,Watcher会被添加到Dep中;当属性被修改时,Dep会通知其中的所有Watcher执行更新操作。

下面是一个简化的示例,演示了Dep的基本结构:

// 简化的 Dep 类
class Dep {
  constructor() {
    // 用 Set 存储 Watcher,确保不会重复添加
    this.subs = new Set();
  }

  // 添加 Watcher
  addSub(sub) {
    this.subs.add(sub);
  }

  // 通知所有 Watcher 执行更新
  notify() {
    this.subs.forEach(sub => {
      sub.update();
    });
  }
}

// 示例属性类,包含 getter 和 setter,同时使用 Dep
class Observable {
  constructor() {
    this._value = null;
    this.dep = new Dep();
  }

  get value() {
    // 将当前 Watcher 添加到依赖
    if (Dep.target) {
      this.dep.addSub(Dep.target);
    }
    return this._value;
  }

  set value(newValue) {
    this._value = newValue;
    // 通知 Dep 执行更新
    this.dep.notify();
  }
}

// Dep.target 全局变量用于存储当前正在执行的 Watcher
Dep.target = null;

这只是一个简化的示例,实际上Vue的Dep实现更加复杂,涉及到多个Watcher的管理,异步更新的处理等。但基本的原理就是通过Dep来管理依赖关系,确保在数据变化时能够及时通知相关的Watcher执行更新。

4.MVVM整体架构:

MVVM整体将Observer、Compile和Watcher整合起来,通过Observer监听数据变化,Compile解析模板指令,而Watcher则负责连接Observer和Compile,使数据变化能够实时反映在视图上,同时视图的交互变化也能更新到数据模型中。这种双向绑定的机制使得开发者能够更自然地处理数据和视图之间的交互。

(Vue)双向数据绑定原理

整个过程就好比是你在不断地与镜子交流,通过这个交流过程,你和镜子始终保持着同步。如果你变化,镜子立刻反映出来;如果镜子变化,你也能立即感知到。这就是 Vue 双向数据绑定的原理,通过监测数据的变化,以及在数据和 UI 元素之间建立起一个智能的桥梁,实现了数据与界面的双向同步。