双向数据绑定是Vue的重要功能之一,它让数据与视图之间的交互变得便捷,今天,咱们就来深入探讨一下Vue2和Vue3在双向数据绑定原理与实现上的差异。

一、双向数据绑定原理

Vue实现双向数据绑定的核心依托于MVVM模式,该模式由三部分构成:

  • Model(数据层):在Vue里,它主要负责存储数据,是应用程序中数据的载体。比如说,一个待办事项列表应用中,待办事项的内容、完成状态等数据就存储在Model中。
  • View(视图层):这部分由HTML模板和Vue指令组成,也就是用户直接看到并与之交互的界面。用户在页面上看到的待办事项的展示、输入框等都属于View层。
  • ViewModel(业务逻辑层):它是Vue的核心部分,作为一个Vue实例,就像一座桥梁,连接着Model和View。当Model中的数据发生变化时,ViewModel能够监听到这些变化,并通过相应机制更新View;反之,当用户在View上进行操作(如输入内容、点击按钮等),ViewModel也能捕获这些事件,并将变化同步到Model中。

在双向数据绑定的过程中,当数据发生变化时,监听器(Observer)会触发依赖通知,从而实现对视图层的更新;而当用户操作视图时,解析器(Compiler)会捕获相关事件(例如v-model指令下的输入操作),进而更新数据层。

二、双向数据绑定的实现方式

(一)监听器的实现

在Vue中,监听器用于监听数据变化,主要有数据劫持和发布 – 订阅模式这两种实现方式。

  1. 数据劫持
    • Vue2的实现:Vue2利用Object.defineProperty方法来实现数据劫持。这个方法能够拦截对象属性的gettersetter操作。在getter中,会收集依赖,也就是记录哪些地方使用了这个数据;而在数据变化,即setter被调用时,通过触发之前收集依赖过程中记录的更新函数,来实现视图的更新。下面通过一个简单示例来理解:
//示例 const o = {}; let bValue = 38; Object.defineProperty(o, "b", { get() { //收集依赖 return bValue; }, set(newValue) { bValue = newValue; // 触发视图更新 }, }); 

在这个例子里,定义了一个对象o,并通过Object.defineProperty对其属性b进行劫持。当获取b的值时,会执行get函数,在这里可以进行依赖收集;当设置b的值时,set函数被调用,除了更新数据bValue,还可以在这个函数里添加触发视图更新的逻辑。
Vue3的实现:Vue3则使用Proxy来替代Object.definePropertyProxy可以创建一个对象的代理,通过它能够方便地对对象的基本操作(如获取属性、设置属性等)进行拦截和自定义。当调用代理对象的get方法时,会进行依赖收集;调用set方法时,就会进行依赖更新。具体语法为const p = new Proxy(target, handler),其中target是要使用Proxy包装的目标对象,可以是各种类型的对象,包括数组、函数等;handler是一个包含各种函数的对象,这些函数定义了代理在执行不同操作时的行为。示例如下:

//示例 let products = new Proxy( { browsers: ["Internet Explorer", "Netscape"], }, { get: function (obj, prop) { // 收集依赖 return obj[prop]; }, set: function (obj, prop, value) { obj[prop] = value; // 触发视图更新,表示成功 return true; }, }, ); 

在这个示例中,创建了一个products的代理对象。当访问products的属性时,get函数会被调用进行依赖收集;当设置属性值时,set函数被调用,更新对象属性的同时触发视图更新。
2. 发布 – 订阅模式
Vue.js借助发布 – 订阅模式来管理组件和数据之间的依赖关系。简单来说,当数据发生变化时,依赖于这些数据的视图(可以理解为观察者)会收到通知并进行更新。下面通过一段代码来详细了解:

// 1. 发布者类 class Dep { constructor() { this.subscribers = []; } // 依赖收集 depend() { if (Dep.target &&!this.subscribers.includes(Dep.target)) { this.subscribers.push(Dep.target); } } // 通知更新 notify() { this.subscribers.forEach(sub => sub()); } } // 2. // ①数据劫持 Vue2-Start function defineReactive(obj, key, val) { const dep = new Dep(); Object.defineProperty(obj, key, { get() { dep.depend(); // 收集依赖 return val; }, set(newVal) { val = newVal; dep.notify(); // 触发更新 } }); } // Vue2-End // ②响应式代理 Vue3-Start function reactive(obj) { const deps = new Map(); // 存储每个属性的依赖 return new Proxy(obj, { get(target, key) { let dep = deps.get(key); if (!dep) { dep = new Dep(); deps.set(key, dep); } if (Dep.currentEffect) { dep.depend(Dep.currentEffect); // 收集依赖 } return target[key]; }, set(target, key, value) { target[key] = value; deps.get(key)?.notify(); // 触发更新 return true; } }); } // Vue3-End // 3. 观察者函数 function watch(effect) { Dep.target = effect; // 标记当前依赖 effect(); // 首次执行以触发getter Dep.target = null; // 重置 } // 使用示例 const data = {}; defineReactive(data, 'count', 0); // 订阅数据变化 watch(() => { console.log('Count updated:', data.count); }); // 触发更新 data.count = 1; // 输出: "Count updated: 1" 

在这段代码中,Dep类充当发布者,它维护了一个订阅者数组subscribersdepend方法用于收集依赖,将相关的观察者添加到数组中;notify方法则用于通知所有订阅者进行更新。defineReactive函数是Vue2中结合发布 – 订阅模式实现数据劫持的关键,在getset操作中分别进行依赖收集和更新通知。Vue3中的reactive函数通过ProxyDep实现了类似的功能,不过在依赖管理上使用了Map来存储每个属性的依赖。watch函数则是一个简单的观察者函数示例,用于订阅数据变化并执行相应操作。

(二)Compile模板解析

Compile主要负责实现视图到数据的绑定,其过程分为以下两个阶段:

  1. 解析阶段:遍历DOM树,识别各种Vue指令,比如v-model{{}}插值等。针对每个指令,创建对应的更新函数,并将其绑定到相应的数据依赖上。例如,下面这个处理v-model指令的函数:
function compileInput(node, data, key) { node.value = data[key]; // 初始化值 node.addEventListener('input', (e) => { data[key] = e.target.value; // View → Model }); // 订阅数据变化,更新视图 watch(key, (value) => node.value = value); // Model → View } 

在这个函数里,首先将数据绑定到输入框的初始值。然后,通过监听输入框的input事件,当用户在输入框中输入内容时,将输入的值同步到数据层(View → Model)。同时,通过watch函数订阅数据变化,当数据发生改变时,更新输入框的显示内容(Model → View)。
2. 虚拟DOM优化:Vue会将模板转换为轻量级的虚拟DOM树。当数据发生变化时,会生成新的虚拟DOM,然后通过Diff算法比对新旧虚拟DOM的差异,只对真实DOM中发生变化的部分进行更新。这样做可以大大减少对真实DOM的操作次数,提高页面的性能和渲染效率。

通过对Vue2和Vue3双向数据绑定原理与实现的深入分析,我们可以看到这两个版本在技术实现上的演进和优化。理解这些差异,有助于我们在实际项目中更好地选择和运用Vue框架,大家还是要掌握下为好哦。