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

发布时间:2023年12月19日

Vue 3 带来了许多令人兴奋的新特性和改进,其中之一就是其双向数据绑定的实现方式。与 Vue 2 使用 Object.defineProperty 不同,Vue 3 利用了 JavaScript 的 Proxy 特性来创建响应式数据。在这篇博客中,我们将探讨 Vue 3 中双向数据绑定的基础原理,并尝试手动实现一个简化版的这一机制。

核心概念

Vue 3 的双向绑定依赖于两个核心概念:响应式代理(Reactive Proxy)Effect 依赖收集系统

响应式代理

Vue 3 使用 Proxy 对象来创建响应式数据。这允许框架在不改变对象本身结构的情况下,拦截并跟踪属性的访问和修改。

Effect 依赖收集系统

Effect 依赖收集系统用于自动追踪响应式数据的使用情况,并在数据变化时重新执行副作用(如渲染函数)。

实现步骤

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

1. 创建 reactive 函数

这个函数用于将普通对象转换为响应式代理。

function reactive(target) {
    // 使用 Proxy 对象创建响应式数据
    return new Proxy(target, {
        // 拦截对象属性的读取
        get(target, key, receiver) {
            console.log(`访问了属性:${key}`);
            // 使用 Reflect.get 保证 this 指向正确
            return Reflect.get(target, key, receiver);
        },
        // 拦截对象属性的设置
        set(target, key, value, receiver) {
            console.log(`设置了属性:${key},新值为:${value}`);
            // 使用 Reflect.set 设置属性值
            Reflect.set(target, key, value, receiver);
            // 此处省略了依赖通知逻辑,实际 Vue 3 中会触发更新
            return true;
        }
    });
}

2. 定义 effect 函数

effect 函数用于注册副作用函数,并在响应式数据变化时执行这些函数。

let activeEffect = null;

function effect(fn) {
    // 设置当前活动的副作用函数
    activeEffect = fn;
    // 立即执行函数,用于依赖收集
    fn();
    // 重置当前活动的副作用函数
    activeEffect = null;
}

3. 改进 reactive 以收集依赖

现在我们需要修改 reactive 函数,以支持依赖收集和派发更新。

const targetMap = new WeakMap();

function track(target, key) {
    // 如果没有活动的副作用函数,直接返回
    if (!activeEffect) return;
    // 从 targetMap 中获取当前对象的所有依赖
    let depsMap = targetMap.get(target);
    if (!depsMap) {
        // 如果没有,就创建新的 Map 并存入 targetMap
        targetMap.set(target, (depsMap = new Map()));
    }
    // 获取当前属性的所有依赖
    let dep = depsMap.get(key);
    if (!dep) {
        // 如果没有,就创建新的 Set 并存入 depsMap
        depsMap.set(key, (dep = new Set()));
    }
    // 将当前活动的副作用函数添加到依赖集合中
    dep.add(activeEffect);
}

function trigger(target, key) {
    // 从 targetMap 中获取对象的所有依赖
    const depsMap = targetMap.get(target);
    if (!depsMap) return;
    // 获取当前属性的所有依赖
    let dep = depsMap.get(key);
    if (dep) {
        // 对于每一个依赖(即副作用函数),执行它
        dep.forEach(effect => effect());
    }
}

// 更新 reactive 函数
function reactive(target) {
    return new Proxy(target, {
        // 拦截属性读取
        get(target, key, receiver) {
            // 收集依赖
            track(target, key);
            return Reflect.get(target, key, receiver);
        },
        // 拦截属性设置
        set(target, key, value, receiver) {
            // 设置属性值
            Reflect.set(target, key, value, receiver);
            // 触发更新
            trigger(target, key);
            return true;
        }
    });
}

4. 测试实例

创建一个响应式对象,并观察它的变化。

const state = reactive({ count: 0 });

// 注册一个副作用函数来模拟渲染逻辑
effect(() => {
    console.log(`count is now: ${state.count}`);
});

// 更改响应式数据的属性,触发副作用函数
state.count++;

完整代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue 3 简易双向数据绑定测试</title>
</head>
<body>
    <h1>Vue 3 简易双向数据绑定测试</h1>
    <div id="app">
        <p>Count: <span id="count">0</span></p>
        <button id="increment">Increment</button>
    </div>

    <script>
        // reactive 函数
        function reactive(target) {
            const handler = {
                get(target, key, receiver) {
                    track(target, key);
                    return Reflect.get(target, key, receiver);
                },
                set(target, key, value, receiver) {
                    Reflect.set(target, key, value, receiver);
                    trigger(target, key);
                    return true;
                }
            };
            return new Proxy(target, handler);
        }

        let activeEffect = null;

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

        const targetMap = new WeakMap();

        function track(target, key) {
            if (!activeEffect) return;
            let depsMap = targetMap.get(target);
            if (!depsMap) {
                targetMap.set(target, (depsMap = new Map()));
            }
            let dep = depsMap.get(key);
            if (!dep) {
                depsMap.set(key, (dep = new Set()));
            }
            dep.add(activeEffect);
        }

        function trigger(target, key) {
            const depsMap = targetMap.get(target);
            if (!depsMap) return;
            let dep = depsMap.get(key);
            if (dep) {
                dep.forEach(effect => effect());
            }
        }

        // 测试代码
        const state = reactive({ count: 0 });

        effect(() => {
            document.getElementById('count').innerText = state.count;
        });

        document.getElementById('increment').addEventListener('click', () => {
            state.count++;
        });
    </script>
</body>
</html>

在这个文件中,我们创建了一个按钮,每当它被点击时,就会增加 state.count 的值。这个值的变化会触发绑定的副作用函数,从而更新显示在页面上的计数结论

小结

以上代码展示了一个 Vue 3 中双向数据绑定的简化实现。通过 Proxy 和动态依赖收集系统,我们可以创建一个灵活且强大的响应式系统。这种实现方式使得 Vue 3 在处理复杂应用时更为高效和灵活。

文章来源:https://blog.csdn.net/a2274001782/article/details/135095426
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。