Vue实现数据双向绑定的原理解析

原理

简单来说Vue 通过数据劫持 结合发布者-订阅者模式实现了双向绑定

如何理解

先来理解数据劫持

数据劫持

数据劫持是指在访问或修改对象属性时,通过拦截的方式进行处理。在前端框架中,特别是在实现数据绑定的过程中,数据劫持是一种常见的技术,它可以用于监听对象属性的变化并触发相应的操作,比如更新视图。

Vue 中通过数据劫持实现了双向绑定,主要是通过 Object.defineProperty 来实现的。

Object.defineProperty方法

概念 Object.defineProperty 是 JavaScript 中用于定义对象属性的方法。它允许你精确控制属性的行为,包括读取(get)、写入(set)和删除(delete)等。具体内容可以查看 参考文档

代码

javascript 复制代码
Object.defineProperty(obj, prop, descriptor)

// 详细写法
Object.defineProperty(obj, 'propertyName', {
  get: function() {
    // 在读取属性时调用
    return this._internalValue; // 返回属性的实际值
  },
  set: function(value) {
    // 在设置属性时调用
    this._internalValue = value; // 更新属性的实际值
  },
  //value 和 get/set 只能存在一个
  //value: '666' // 属性值
  writable: true, // 是否可写
  enumerable: true,  // 是否可枚举
  configurable: true  // 是否可配置
});

参数

  1. obj:要定义属性的对象。
  2. prop:要定义或修改的属性的名称。
  3. descriptor:要定义或修改的属性描述符。

其中属性描述符 descriptor 是一个对象,它可以包含以下属性:

  • value: 属性的值,默认为 undefined。
  • writable: 布尔值,表示属性值是否可写 ,默认为 false。如果为 false,属性值不能被重新赋值
  • enumerable: 布尔值,表示属性是否可枚举,默认为 false。如果为 false,属性将不会出现在 for...in 或 Object.keys() 的遍历中。
  • configurable: 布尔值,表示属性是否可删除或是否可以修改属性的特性,默认为 false。如果为 false,任何尝试删除属性或修改属性特性的操作都会被忽略。且不能再把该属性变回数据属性。

另外,descriptor 还可以包含 getset 方法,用于获取设置属性值。这两个方法分别在访问属性和修改属性时被调用。

示例

javascript 复制代码
let obj = {};

Object.defineProperty(obj, 'name', {
  value: '张三',
  writable: false, // 不能重新赋值
  enumerable: true,
  configurable: true
});

console.log(obj.name); // 输出: 张三
obj.name = '李四';      // 由于 writable 为 false,此赋值无效

for (let key in obj) {
  console.log(key);   // 输出: name
}

delete obj.name;       // 由于 configurable 为 true,属性可删除
console.log(obj.name); // 输出: undefined

Object.defineProperty 在 Vue 中的应用是通过它来设置响应式数据的 getter 和 setter,从而实现数据的双向绑定。Vue 利用它来劫持对象的属性,实现对数据的监听和更新。

再来理解发布者-订阅者模式

发布者-订阅者模式

概念

发布者-订阅者模式(Publisher-Subscriber Pattern)是一种设计模式,也被称为观察者模式(Observer Pattern)。它是一种行为型模式,用于定义对象之间的一对多的依赖关系,当一个对象的状态发生变化时,所有依赖它的对象都会得到通知并自动更新。

在这个模式中,有两个主要角色:

  • 发布者(Publisher): 也称为主题(Subject),负责维护一组订阅者并通知它们状态的变化。发布者是被观察的对象,当其状态发生变化时,会通知所有订阅者。
  • 订阅者(Subscriber): 也称为观察者(Observer),监听发布者的变化并作出相应的响应。订阅者通过订阅发布者来接收通知,以便在发布者状态变化时执行相应的操作。

基本流程如下:

  1. 发布者维护一个订阅者列表。
  2. 订阅者通过订阅发布者来注册自己。
  3. 当发布者的状态发生变化时,会遍历订阅者列表,并调用它们的更新方法(通知)。
  4. 订阅者接收到通知后执行相应的操作。

这种模式的优点在于降低了对象之间的耦合度,使得发布者和订阅者之间可以独立地进行扩展和修改。这也是为什么这个模式常用于实现事件处理系统、UI组件之间的通信等场景。

在JavaScript中,发布者-订阅者模式通常通过自定义事件来实现。例如,DOM事件就是一种发布者-订阅者模式,通过addEventListener注册事件监听器,通过dispatchEvent触发事件,实现了对象之间的解耦。在现代JavaScript框架和库中,也经常使用这种模式来实现组件之间的通信

代码示例

发布者(Publisher)

发布者是一个对象,负责维护一组订阅者,并在自身状态变化时通知订阅

javascript 复制代码
// 观察者模式中的发布者(Publisher)类
class Publisher {
  constructor() {
    // 存储订阅者列表
    this.subscribers = [];
  }

  // 添加订阅者
  subscribe(subscriber) {
    this.subscribers.push(subscriber);
  }

  // 移除订阅者
  unsubscribe(subscriber) {
    this.subscribers = this.subscribers.filter(sub => sub !== subscriber);
  }

  // 通知所有订阅者
  notify(message) {
    this.subscribers.forEach(subscriber => {
      subscriber.update(message);
    });
  }
}

订阅者(Subscriber)

订阅者是一个对象,它订阅发布者的事件或状态变化,并在发布者通知时执行相应的操作

javascript 复制代码
// 观察者模式中的订阅者(Observer)类
class Subscriber {
  // 订阅者的更新操作
  update(message) {
    console.log(`收到消息: ${message}`);
  }
}

使用

javascript 复制代码
// 创建发布者实例
const publisher = new Publisher();

// 创建订阅者实例
const subscriberA = new Subscriber();
const subscriberB = new Subscriber();

// 将订阅者订阅到发布者
publisher.subscribe(subscriberA);
publisher.subscribe(subscriberB);

// 模拟发布者状态变化时通知订阅者
publisher.notify('你好');

输出结果

javascript 复制代码
// 输出结果
// 收到消息:你好
// 收到消息:你好

Publisher 发布消息时,所有订阅者(Subscriber)都收到了相同的消息,并执行了各自的 update 方法,输出了接收到的消息。

现在我们了解了数据劫持发布者-订阅者模式的概念,再来看vue是如何实现的。

Vue实现步骤

下面是一个简化实现案例

1.对数据进行监控

对需要观察者(observe)的数据进行递归遍历,包括子属性都加上setter和getter

javascript 复制代码
// 观察者
class Observer {
  constructor(value) {
    this.value = value;
    this.walk(value);
  }

  // 递归遍历对象,将每个属性都转换为响应式
  walk(obj) {
    Object.keys(obj).forEach(key => {
      this.defineReactive(obj, key, obj[key]);
    });
  }

  // 将对象的属性转换为响应式
  defineReactive(obj, key, val) {
    const dep = new Dep();

    // 递归处理嵌套属性
    observe(val);
	// 添加setter和getter
    Object.defineProperty(obj, key, {
      get() {
        // 在读取属性时,将当前 Watcher 添加到依赖收集器中
        if (Dep.target) {
          dep.addSubscriber(Dep.target);
        }
        return val;
      },
      set(newVal) {
        if (val !== newVal) {
          val = newVal;
          // 当数据发生变化时,通知依赖收集器中的所有 Watcher 更新
          dep.notify();
        }
      }
    });
  }

2.解析模板指令

Compile(编译)解析模板指令,然后调用 updateText 方法更新文本节点的内容,将模板变量替换成数据

javascript 复制代码
// 简化版编译器部分内容
... 
// 编译文本节点,处理 {{...}} 表达式
compileText(node) {
  // 提取表达式内容
  const exp = RegExp.$1;
  // 更新文本节点的内容
  this.updateText(node, exp);
  ...
}
...
// 更新文本节点的内容
updateText(node, exp) {
  // 获取表达式的值,并设置文本节点的内容
  const value = this.vm[exp];
  node.textContent = value;
}
...

3.Watcher创建

在模板编译阶段,对于每个需要建立双向绑定的表单元素,Vue 会创建一个 Watcher 实例,将其与数据进行关连

javascript 复制代码
// 简化版编译器部分内容
... 
// 编译文本节点,处理 {{...}} 表达式
compileText(node) {
  // 提取表达式内容
  const exp = RegExp.$1;
  // 更新文本节点的内容
  this.updateText(node, exp);
  ...
  
   // 创建 Watcher 实例,负责更新视图
  new Watcher(this.vm, exp, (newVal) => {
    // 表达式的值变化时,更新文本节点的内容
    node.textContent = newVal;
  });
  
  ...
}
...

Watcher 是 Vue 数据响应系统的核心组件,它在数据发生变化时负责通知相关的视图进行更新。

javascript 复制代码
// 简化版Watcher订阅者     
class Watcher {
  constructor(vm, key, updateFn) {
    this.vm = vm;
    this.key = key;
    this.updateFn = updateFn;

    // 将当前 Watcher 实例指定为全局的当前 Watcher
    Dep.target = this;

    // 触发一次属性的 getter,从而在依赖收集阶段将当前 Watcher 添加到依赖列表中
    this.vm[this.key];

    // 重置全局的当前 Watcher
    Dep.target = null;
  }

  // 更新视图
  update() {
    this.updateFn.call(this.vm, this.vm[this.key]);
  }
}

Watcher(订阅者)是Observer(观察者)和Compile(编译器)之间通讯的桥梁,Vue 中的响应式系统通过 Watcher、Dep(属性订阅器)等机制来建立数据与视图之间的关联,实现了数据驱动视图的目标。主要步骤是:

  1. 在自身实例化时往属性订阅器(dep)里添加自己
javascript 复制代码
class Dep {
  constructor() {
    this.subscribers = [];
  }

  // 添加订阅者(Watcher)
  addSubscriber(subscriber) {
    this.subscribers.push(subscriber);
  }

  // 通知所有订阅者更新
  notify() {
    this.subscribers.forEach(subscriber => subscriber.update());
  }
}

Dep 的主要作用是用于管理一组依赖(Watcher 实例),在数据变化时通知这些依赖执行相应的更新操作。

  1. 自身中要有一个update()方法
javascript 复制代码
// 简化版Watcher订阅者     
class Watcher {
  ...
  // 更新视图
  update() {
    this.updateFn.call(this.vm, this.vm[this.key]);
  }
  ...
}
  1. 等到属性变动dep.notice通知的时候,能调用自身的update()方法,并触发observer中绑定的回调
javascript 复制代码
// 数据劫持,进行通知
function defineReactive(obj, key, val) {
  const dep = new Dep();
  Object.defineProperty(obj, key, {
    get() {
    ...
    },
    set(newVal) {
      if (val !== newVal) {
        val = newVal;
        // 当数据发生变化时,通知依赖收集器中的所有 Watcher 更新
        dep.notify();
      }
    }
  });
}
javascript 复制代码
// 调用update()方法
class Dep {
 ...
  // 通知所有订阅者更新
  notify() {
    this.subscribers.forEach(subscriber => subscriber.update());
  }
  ...
}
javascript 复制代码
class Watcher {
  ...
  // 更新视图
  update() {
    this.updateFn.call(this.vm, this.vm[this.key]);
  }
}
javascript 复制代码
// 创建 Watcher 实例,负责更新视图
const watcher = new Watcher(vm, 'message', (newVal) => {
  console.log('视图更新了:', newVal);
});

总的来说,Vue 数据双向绑定的核心是通过 Watcher 实现数据与视图的同步。当数据变化时,通过 Watcher 检测到并通知相关视图更新;当用户与视图交互时,通过 Watcher 更新相关的数据。这样就实现了数据和视图的双向绑定。

4.以MVVM为数据入口

作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到了双向绑定的效果

了解MVVM、MVC

MVVM(Model-View-ViewModel)是一种软件架构模式,其中 Model 表示应用程序的数据模型,View 表示用户界面,而 ViewModel 则充当 Model 和 View 之间的中介,处理数据的交互和业务逻辑。在前端框架中,MVVM 常常用于实现数据绑定,其中 Observer(观察者)用于监测数据变化,Compile(编译器)用于解析模板并创建 Watcher(观察者),Watcher 负责更新视图。

相关推荐
用户15630681035139 分钟前
Day01 | 什么是Agent?
面试
Momo__41 分钟前
VueUse createReusableTemplate —— 单文件组件内的模板复用神器
前端·vue.js
程序员小富1 小时前
我开源了一个开发者专属的智能 JSON 工具,得到了媳妇高度认可
前端·vue.js·后端
小小小小宇1 小时前
程序员如何给 LLM 装工具以及看懂推理过程
前端
写代码的皮筏艇1 小时前
React中的forwardRef
前端·react.js·面试
槑有老呆1 小时前
花三个月工资请了个 AI 程序员,结果它连青岛啤酒股价都查不了
前端
风骏时光牛马1 小时前
Verilog开发常见问题汇总解析
前端
子兮曰1 小时前
AI Coding Method Map:一张图看懂 AI 编程的完整链路
前端·人工智能·后端
weedsfly1 小时前
语法糖褪去之后——Babel 转译产物中的 JavaScript 本貌
前端·javascript