手动实现 Vue 2 的简易双向数据绑定(模仿源码)

实现 Vue 2 的简易双向数据绑定

Vue.js 是一个流行的前端框架,它以其简单易用的双向数据绑定而闻名。在下面的文章中,我们将探索 Vue 2 如何通过其响应式系统实现双向数据绑定,并尝试手动实现一个简化版本。

核心概念

Vue 2 的双向数据绑定基于几个关键概念:响应式系统依赖收集派发更新。它利用 JavaScript 的 Object.defineProperty 方法来跟踪数据的变化。

响应式系统的初始化

当一个 Vue 实例被创建时,它通过一个名为 observe 的函数递归地遍历所有的数据对象,并将这些对象的每个属性转换成 getter/setter。这是通过 defineReactive 函数实现的。

依赖收集和派发更新

每个组件实例都有一个相应的 Watcher 实例,它会在组件渲染过程中记录所有依赖(即数据属性)。当一个数据属性被修改时,它的 setter 会被触发,进而通知相关的 Watcher 更新视图。

实现步骤

以下是实现 Vue 2 双向绑定的简化步骤:

1. 实现 observe 函数

这个函数负责遍历并包装对象的每个属性。

function observe(obj) {
    // 检查obj是否是对象,如果不是对象或者为null,则不需要做响应式处理
    if (!obj || typeof obj !== 'object') return;

    // 遍历对象的每个属性,对每个属性进行响应式处理
    Object.keys(obj).forEach(key => {
        defineReactive(obj, key, obj[key]);
    });
}

2. 定义 defineReactive 函数

这个函数使用 Object.defineProperty 将普通属性转换为响应式属性。

function defineReactive(obj, key, val) {
    // 如果val本身还是对象,则需要递归处理,确保对象内的属性也是响应式的
    observe(val);

    // 创建一个依赖管理器实例,用于收集和派发当前属性的依赖
    let dp = new Dep();

    // 通过Object.defineProperty将属性转换为getter/setter
    Object.defineProperty(obj, key, {
        enumerable: true,   // 属性可枚举
        configurable: true, // 属性可配置
        get: function reactiveGetter() {
            // 收集依赖,当有Watcher读取该属性时,将Watcher添加到依赖列表中
            if (Dep.target) dp.addSub(Dep.target);
            return val;
        },
        set: function reactiveSetter(newVal) {
            // 当属性值发生变化时,更新属性的值
            val = newVal;
            // 并通知所有依赖进行更新
            dp.notify();
        }
    });
}

3. 创建 Dep

Dep 类是一个依赖管理器,它收集和派发依赖。

class Dep {
    constructor() {
        // 初始化依赖数组,用于存储所有依赖该属性的Watcher
        this.subs = [];
    }

    // 添加一个新的依赖(Watcher)
    addSub(sub) {
        this.subs.push(sub);
    }

    // 当属性变化时,通知所有依赖执行更新操作
    notify() {
        this.subs.forEach(sub => sub.update());
    }
}
// 全局属性,用于暂存当前正在计算的Watcher
Dep.target = null;

4. 定义 Watcher

Watcher 类为每个组件或指令创建一个观察者实例。

class Watcher {
    constructor(obj, key, cb) {
        // 将Dep.target指向自己,用于依赖收集
        Dep.target = this;
        this.cb = cb; // 回调函数,用于更新视图
        this.obj = obj; // 监听的目标对象
        this.key = key; // 监听的对象属性
        this.value = obj[key]; // 触发属性的getter,进行依赖收集
        Dep.target = null; // 收集完依赖后,将Dep.target重置
    }

    // 当属性变化时,调用回调函数更新视图
    update() {
        this.value = this.obj[this.key];
        this.cb(this.value);
    }
}

5. 测试案例

创建一个数据对象,并观察它的变化。

var data = { name: 'yck' };
// 对数据对象data进行响应式处理
observe(data);

// 更新DOM的函数
function update(value) {
    document.querySelector('div').innerText = value;
}

// 创建一个Watcher实例,模拟对data.name的依赖收集和视图更新
new Watcher(data, 'name', update);

// 修改data.name的值,触发响应式更新
data.name = 'yyy';

完整代码如下

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue 2 Data Binding Example</title>
</head>
<body>
    <div>{{name}}</div>

    <script>
        // observe 函数:使一个对象变成响应式
        function observe(obj) {
            if (!obj || typeof obj !== 'object') return;
            Object.keys(obj).forEach(key => {
                defineReactive(obj, key, obj[key]);
            });
        }

        // defineReactive 函数:定义一个响应式的属性
        function defineReactive(obj, key, val) {
            observe(val); // 递归处理子属性
            const dep = new Dep(); // 为每个属性创建依赖管理器实例

            Object.defineProperty(obj, key, {
                enumerable: true,
                configurable: true,
                get: function reactiveGetter() {
                    if (Dep.target) {
                        dep.addSub(Dep.target); // 收集依赖
                    }
                    return val;
                },
                set: function reactiveSetter(newVal) {
                    if (newVal === val) return;
                    val = newVal;
                    dep.notify(); // 数据变化,通知所有依赖更新
                }
            });
        }

        // Dep 类:依赖管理器,管理某个属性的所有Watcher
        class Dep {
            constructor() {
                this.subs = [];
            }

            addSub(sub) {
                this.subs.push(sub);
            }

            notify() {
                this.subs.forEach(sub => sub.update());
            }
        }
        Dep.target = null;

        // 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); // 执行回调更新视图
            }
        }

        // 测试代码
        var data = { name: 'Vue' };
        observe(data);

        // 模拟解析到 `{{name}}`,创建一个Watcher来更新视图
        new Watcher(data, 'name', function (value) {
            document.querySelector('div').innerText = value;
        });

        // 修改data.name的值,触发响应式更新
        setTimeout(() => {
            data.name = 'Vue 2';
        }, 2000);
    </script>
</body>
</html>

结论

以上代码提供了一个简单的 Vue 2 双向绑定机制的实现。尽管实际的 Vue 源码要复杂得多,但这个简化版本揭示了 Vue 数据响应系统的核心原理:通过 Object.defineProperty 实现的数据监听、依赖收集、以及基于这些依赖的视图更新。

通过理解这些基础概念,我们可以更深入地理解 Vue 的工作原理,并在需要时对其进行定制和优化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值