vue 双向绑定的原理

为了更深入地理解Vue.js的双向绑定原理,我们可以分别用代码示例来说明Vue 2.x中使用Object.defineProperty和Vue 3.x中使用Proxy实现响应式数据的方式,以及如何通过这些机制实现双向绑定。

Vue 2.x 使用 Object.defineProperty

Vue 2.x通过Object.defineProperty方法拦截对象属性的getter和setter,从而实现数据的响应式。当数据变化时,视图自动更新;当用户输入时,数据也会更新,实现双向绑定。

以下是简化版的Vue 2.x响应式系统的实现:

javascript 复制代码
function defineReactive(obj, key, val) {
    // 递归子属性
    observe(val);

    // 创建一个Dep实例,用于收集依赖
    const dep = new Dep();

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            dep.depend(); // 依赖收集
            return val;
        },
        set: function reactiveSetter(newVal) {
            if (newVal === val) return;
            val = newVal;
            dep.notify(); // 通知依赖更新
        }
    });
}

// 递归遍历对象,使其每个属性都为响应式
function observe(value) {
    if (typeof value !== 'object') return;
    Object.keys(value).forEach(key => defineReactive(value, key, value[key]));
}

// 简化版Dep类
class Dep {
    constructor() {
        this.subscribers = [];
    }

    depend() {
        if (Dep.target) {
            this.subscribers.push(Dep.target);
        }
    }

    notify() {
        this.subscribers.forEach(sub => sub.update());
    }
}

// 模拟Watcher
class Watcher {
    constructor(obj, key, cb) {
        Dep.target = this;
        this.cb = cb;
        this.obj = obj;
        this.key = key;
        this.value = obj[key]; // 触发getter进行依赖收集
        Dep.target = null;
    }

    update() {
        this.value = this.obj[this.key];
        this.cb(this.value);
    }
}

const data = { price: 5, quantity: 2 };
observe(data);

// 创建Watcher实例,当数据变化时更新视图
new Watcher(data, 'price', (val) => {
    console.log(`Price changed to ${val}`);
});

// 改变数据,触发更新
data.price = 20;

Vue 3.x 使用 Proxy

Vue 3.x使用Proxy来实现响应式系统,它可以监听对象的所有操作,比Object.defineProperty有更好的性能和灵活性。

以下是简化版的Vue 3.x响应式系统的实现:

javascript 复制代码
function reactive(target) {
    const handler = {
        get(target, key, receiver) {
            const result = Reflect.get(target, key, receiver);
            track(target, key); // 依赖收集
            return result;
        },
        set(target, key, value, receiver) {
            const oldValue = target[key];
            const result = Reflect.set(target, key, value, receiver);
            if (oldValue !== value) {
                trigger(target, key); // 触发更新
            }
            return result;
        }
    };
    return new Proxy(target, handler);
}

let activeEffect = null;

function track(target, key) {
    // 省略依赖收集实现
}

function trigger(target, key) {
    // 省略触发更新实现
}

function effect(fn) {
    activeEffect = fn;
    fn();
    activeEffect = null;
}

const product = reactive({ price: 5, quantity: 2 });

effect(() => {
    console.log(`Total is ${product.price * product.quantity}`);
});

product.price = 20;

双向绑定的实现

在Vue中,v-model指令用于在表单输入和应用状态之间创建双向绑定。对于普通的文本输入,Vue内部大致做了如下事情:

  1. 绑定一个input事件监听器到输入框上,当用户输入时,更新相应的数据。
  2. 使用Object.definePropertyProxy对数据进行响应式处理,当数据变化时,更新输入框的值。

通过这种方式,Vue实现了数据和视图之间的双向绑定,让开发者能够以声明式的方式处理用户输入和应用状态的同步。

让我们逐步混合并详细解释上面的代码示例,以便更好地理解Vue的响应式系统和双向绑定是如何工作的。这里我们主要聚焦于Vue 2.x的实现方式,因为它依赖于Object.defineProperty,这有助于理解基础的响应式原理。Vue 3.x采用Proxy实现了更高级和高效的响应式系统,但基本原理保持一致。

Vue 2.x 响应式系统基础

1. defineReactive 函数

defineReactive函数是响应式系统的核心。它接受一个对象obj,一个键key和该键的初始值val。该函数的目的是将obj[key]转换成getter/setter,以便Vue能够在访问或修改该属性时执行自定义逻辑。

  • Getter:当属性被访问时,getter会执行。在这个示例中,getter的作用是进行依赖收集。每当Vue组件或计算属性访问这个属性时,它都会被添加到当前属性的依赖列表中。这样,当属性值变化时,Vue就知道哪些组件需要重新渲染。
  • Setter:当属性被修改时,setter会执行。在这个示例中,setter的作用是触发更新。它会检查新值是否与旧值不同,如果不同,则通知所有依赖于该属性的地方,告诉它们值已经改变。

2. observe 函数

observe函数递归地遍历对象的所有属性,对每个属性调用defineReactive函数,使整个对象变为响应式。这意味着Vue可以监听对象内部任何级别的数据变化。

3. Dep

Dep类是一个简单的依赖管理器。它收集一个属性的所有依赖(即观察该属性的组件和计算属性),并在该属性变化时通知它们。每个属性都有自己的Dep实例。

  • depend方法用于添加一个新的依赖到当前的依赖列表中。
  • notify方法用于当属性变化时,通知所有依赖进行更新。

4. Watcher

Watcher类模拟Vue中的观察者。它的构造函数接收一个对象、一个键和一个回调函数。创建Watcher实例时,它会读取指定的属性,这个读取操作会触发属性的getter,从而进行依赖收集。当属性值变化时,setter会触发dep.notify(),然后Watcherupdate方法被调用,执行回调函数,通常是更新视图的操作。

运行过程

  1. 初始化 :通过调用observe(data),数据对象data的每个属性都被转化为响应式,可以被Vue追踪变化。
  2. 依赖收集 :当new Watcher实例创建时,它尝试读取data.price,这个操作触发data.price的getter。在getter中,依赖收集发生,当前的Watcher实例被添加到data.price的依赖列表中。
  3. 响应更新 :当执行data.price = 20;时,data.price的setter被触发,因为新值与旧值不同,它调用dep.notify(),通知所有依赖(即之前收集的Watcher实例)data.price已经改变。Watcherupdate方法被调用,执行回调函数,通常会导致视图更新。

通过这种机制,Vue实现了数据的响应式和双向绑定:数据变化自动更新视图,用户输入(通过如v-model指令绑定的视图变化)自动更新数据。

让我们详细解释上面提供的两个示例的运行过程:

Vue 2.x 示例解释

  1. 初始化响应式系统: defineReactive函数被调用,为对象data的每个属性(在这个例子中是pricequantity)创建getter和setter,实现数据的响应式。

  2. 依赖收集:

    • 当创建Watcher实例时,它的构造函数会读取data.price,这会触发price的getter。
    • 在getter内部,dep.depend()被调用,将当前的Watcher实例添加到Dep实例的subscribers数组中,完成依赖收集。
    • 这意味着price属性现在有了一个订阅者(依赖),即Watcher实例。
  3. 响应数据变化:

    • 当执行data.price = 20;时,price属性的setter被触发。
    • 在setter内部,dep.notify()被调用,遍历Dep实例的subscribers数组,并调用每个订阅者的update方法。
    • Watcherupdate方法会重新读取price属性的值,并执行回调函数,打印新的价格。

Vue 3.x 示例解释

  1. 使用Proxy创建响应式对象: reactive函数通过返回一个Proxy实例使product对象变得响应式。这个Proxy能够拦截对product对象的所有操作。

  2. 依赖收集:

    • effect函数接受一个函数作为参数并立即执行它。在这个函数内部,当访问product.priceproduct.quantity时,触发了Proxyget陷阱。
    • get陷阱内部,track函数被调用以进行依赖收集。虽然这里没有详细展示track函数的实现,但其基本作用是记录哪个effect依赖于哪些属性。
  3. 响应数据变化:

    • 当更改product.price的值时,触发了Proxyset陷阱。
    • set陷阱内部,首先通过Reflect.set完成属性值的更新,然后调用trigger函数来响应这个变化。
    • trigger函数会查找所有依赖于被更改属性的effect函数,并重新执行它们。在这个例子中,重新执行导致打印出新的总价。

总结

  • 在Vue 2.x中,Object.defineProperty用于拦截对象属性的访问和修改,通过DepWatcher类进行依赖收集和更新通知。
  • 在Vue 3.x中,Proxy提供了一种更强大和灵活的方式来监听对象的活动。它允许Vue拦截对象上的任何操作,而effect函数和依赖跟踪/触发机制用于响应数据变化。
  • 在双向绑定中,用户界面的更改(如输入框的输入)会更新响应式数据,而响应式数据的更改又会更新用户界面,形成一个闭环。
相关推荐
IT_陈寒22 分钟前
Python开发者必须掌握的12个高效数据处理技巧,用过都说香!
前端·人工智能·后端
gnip8 小时前
企业级配置式表单组件封装
前端·javascript·vue.js
一只叫煤球的猫9 小时前
写代码很6,面试秒变菜鸟?不卖课,面试官视角走心探讨
前端·后端·面试
excel10 小时前
Three.js 材质(Material)详解 —— 区别、原理、场景与示例
前端
掘金安东尼10 小时前
抛弃自定义模态框:原生Dialog的实力
前端·javascript·github
hj5914_前端新手14 小时前
javascript基础- 函数中 this 指向、call、apply、bind
前端·javascript
薛定谔的算法14 小时前
低代码编辑器项目设计与实现:以JSON为核心的数据驱动架构
前端·react.js·前端框架
Hilaku14 小时前
都2025年了,我们还有必要为了兼容性,去写那么多polyfill吗?
前端·javascript·css
yangcode14 小时前
iOS 苹果内购 Storekit 2
前端
LuckySusu14 小时前
【js篇】JavaScript 原型修改 vs 重写:深入理解 constructor的指向问题
前端·javascript