Vue3 手写 双向绑定 reactive track trigger effect

150 阅读4分钟

项目地址:gitee.com/luobf22/vue…

概述

  • 本文旨在研究Vue3双向绑定的源码,虽然只是简单的案例,但是优化了Vue2对于数组和对象操作的缺陷
  • 如果有不同观点可以评论或者自行查看源码

vue3和vue2的异同

  • vue2是通过Object.defineProperty数据劫持,vue3是通过Proxy代理
  • vue2、vue3都是通过get获取数据、set修改数据
  • vue2通过下标修改数组,添加obj属性时,Object.defineProperty不经过set,只能经过get,而proxy经过set
  • vue2会对每个属性赋予watch对象,然后通过observe观察属性的变化,然后进行update,而vue3通过track收集依赖,数据修改的时候,trigger触发依赖,依赖表里的属性修改的时候,相应修改,并且effect触发相应的副作用函数,将属性相关的函数,属性,dom相应修改

完整代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
</head>
<body>
    <div id="app">
        <input type="text" id="inputElement">
        <p id="textElement">{{state.message}}</p>

        <button id="addBtn">给obj添加属性</button>
        <button id="changeBtn">修改obj属性</button>

        <p id="objElm">{{obj}}</p>

        <button id="changeArr">给arr修改所有参数</button>
        <button id="changeIndexArr">修改obj某个下标参数</button>
        
        <p id="arrElm">{{arr}}</p>
    </div>
</body>
<script src="./手写vue3.js"></script>
</html>

// 用于存储所有的副作用函数(类似Vue 3中的effect),模拟响应式更新机制
const effects = [];

// 用于存储响应式对象和其属性对应的依赖关系(Set集合),结构为 Map<object, Map<string, Set<function>>>
const targetMap = new Map();

// 模拟Vue 3中的effect,用于创建副作用函数,在函数执行时收集依赖
let activeEffect;

function effect(fn) {
    // console.log('fn',fn);
    const effectFn = () => {
        activeEffect = effectFn;
        fn();
        // 如果新增属性的时候没办法让activeEffect不为null则将activeEffect=null注释
        // 才能正常更新新属性的依赖收集
        // activeEffect = null;
    };
    effectFn();
    return effectFn;
}


// 依赖收集函数,模拟Vue 3中收集依赖的逻辑,将当前的副作用函数添加到对应的数据依赖列表中
function track(target, key) {
    // console.log('track',key,activeEffect);
    if (activeEffect) {
        let depsMap = targetMap.get(target);
        if (!depsMap) {
            depsMap = new Map();
            targetMap.set(target, depsMap);
        }
        let dep = depsMap.get(key);
        if (!dep) {
            dep = new Set();
            depsMap.set(key, dep);
        }
        dep.add(activeEffect);
    }
}

// 触发依赖更新函数,模拟Vue 3中当数据变化时通知依赖更新的逻辑,执行所有收集到的副作用函数
function trigger(target, key) {
    const depsMap = targetMap.get(target);
    // console.log('trigger',depsMap);

    if (depsMap) {
        // console.log('depsMap', depsMap);
        const dep = depsMap.get(key);
        if (dep) {
            dep.forEach(effect => effect());
        }
    }
}
// 创建响应式对象,使用Proxy进行数据劫持
function reactive(obj) {
    return new Proxy(obj, {
        get(target, key, receiver) {
            // 收集依赖(副作用函数)
            // console.log('get');
            track(target, key);
            const value = Reflect.get(target, key, receiver);
            // console.log('value', value);
            if (typeof value === 'object' && value !== null) {
                // 递归处理嵌套对象,确保嵌套结构也是响应式的
                return reactive(value);
            }
            return value;
        },
        set(target, key, value, receiver) {
            // console.log('set');
            // console.log('target', target);
            // console.log('key', key);

            const hadKey = key in target;
            const result = Reflect.set(target, key, value, receiver);
            if (!hadKey) {
                // console.log('如果是新添加的属性,进行依赖收集');
                track(target, key);

                trigger(target, key);
            }

            if (hadKey || (value !== target[key])) {
                // console.log('触发依赖更新执行副作用函数仅当值发生变化或者是新添加属性时触发');
                trigger(target, key);
            }
            return result;
        }
    });
}



// 模拟Vue 3中的渲染函数,将数据渲染到DOM元素上
function render() {
    const inputElement = document.getElementById('inputElement');
    const textElement = document.getElementById('textElement');

    const addBtn = document.getElementById('addBtn');
    const changeBtn = document.getElementById('changeBtn');
    const objElm = document.getElementById('objElm');

    const changeArr = document.getElementById('changeArr');
    const changeIndexArr = document.getElementById('changeIndexArr');
    const arrElm = document.getElementById('arrElm');

    const state = reactive({
        message: 'Initial Message',
        arr: [1, 2, 3, 4, 5],
        obj: {
            name: 'luo',
            year: 27
        }
    });

    // 双向绑定逻辑,给input元素添加input事件监听,更新数据
    inputElement.addEventListener('input', (e) => {
        state.message = e.target.value;
    });

    // 给obj操作的按钮
    addBtn.addEventListener('click', (e) => {
        state.obj.sex = '男'
        state.obj.age = 27
        // obj.name = 'lbf'
        // console.log(state.obj);
    });

    changeBtn.addEventListener('click', (e) => {
        state.obj.name = 'lbf'

        // console.log(state.obj);
    });

    // 给arr操作的按钮

    changeArr.addEventListener('click', (e) => {
        state.arr = [1, 2, 3]
    });

    changeIndexArr.addEventListener('click', (e) => {
        state.arr[0] = 9
        // console.log(state.arr);
    });

    // 读取数据并更新文本元素内容,同时收集依赖
    effect(() => {
        textElement.textContent = state.message;
        objElm.textContent = JSON.stringify(state.obj);
        arrElm.textContent = state.arr;
    });
}

render();

代码解析

  • reactive

    • 返回一个proxy对象,对创建的对象进行代理
    • get,将属性名key传递给track函数进行依赖收集,const value = Reflect.get(target, key, receiver)value是key对应的值,如果value是对象,则回调reactive(value),将value对象下的属性也进行依赖收集,如果value是对象,但不回调reactive(value),则会造成修改数组下标的属性时无法及时更新视图,因为对象下的属性没有一一进行依赖收集
    • set,判断修改的属性值是不是在原有的依赖表里,如果不在,则通过track进行依赖收集更新,如果在则trigger触发依赖,实现数据的变更,以及通过effect进行视图的变更
  • track

    • 依赖收集
  • trigger

    • 触发依赖,并执行effect,副作用函数触发,更新视图
  • effect

    • 副作用函数,将与属性变更相关的fn执行,更新视图,如果出现依赖无法及时更新,可以尝试// activeEffect = null;将此代码注释,因为如果activeEffect为空,无法将新的属性进行依赖更新