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 负责更新视图。

相关推荐
练习两年半的工程师11 分钟前
React的基础知识:Context
前端·javascript·react.js
Layue000001 小时前
学习HTML第三十三天
java·前端·笔记·学习·html
VillanelleS1 小时前
Vue进阶之Vue CLI服务—@vue/cli-service & Vuex
前端·javascript·vue.js
SRC_BLUE_171 小时前
UPLOAD LABS | PASS 01 - 绕过前端 JS 限制
开发语言·前端·javascript
NetX行者1 小时前
Vue3+Typescript+Axios+.NetCore实现导出Excel文件功能
前端·typescript·c#·excel·.netcore
美团测试工程师1 小时前
Fiddler导出JMeter脚本插件原理
前端·jmeter·fiddler
大家的林语冰2 小时前
🔥 Deno 状告甲骨文,要求取消 JavaScript 商标
前端·javascript·node.js
余生H2 小时前
Angular v19 (二):响应式当红实现signal的详细介绍:它擅长做什么、不能做什么?以及与vue、svelte、react等框架的响应式实现对比
前端·vue.js·react.js·angular.js
聚宝盆_2 小时前
【记录:前端提高用户体验】
前端·css