手写vue2和vue3响应式对象原理的思路

427 阅读4分钟

最初手动响应的思路

无论vue2还是vue3,做到响应式数据都分为几个步骤,接下一步一步的实现响应式数据。

手动收集依赖和派发通知

实现一个Depend类,可以添加和调用使用对象数据的函数。代码如下:

class Depend {
    constructor() {
        this.reactiveFns = [];
    }
    // 添加依赖
    addDepend(fn) {
        this.reactiveFns.push(fn);
    }
    // 遍历执行收集的依赖函数
    notify() {
        this.reactiveFns.forEach(item => item());
    }
}

const depend = new Depend();
const obj = { name: 'moon', age: 18 };

function foo1() {
    console.log(obj.name, '第一次');
}
function foo2() {
    console.log(obj.name, '第二次');
}
depend.addDepend(foo1);
depend.addDepend(foo2);
depend.notify();
//moon 第一次
// moon 第二次

自动派发通知

上面代码,只能手动调用notify方法才能派发通知,接下来让我们自动调用。

vue2使用Object.defineProperty劫持对象

class Depend {
    constructor() {
        this.reactiveFns = [];
    }
    // 添加依赖
    addDepend(fn) {
        this.reactiveFns.push(fn);
    }
    // 遍历执行收集的依赖函数
    notify() {
        this.reactiveFns.forEach(item => item());
    }
}

const depend = new Depend();
const obj = { name: 'moon', age: 18 };

Object.keys(obj).forEach(key => {
    //保留value值
    let value = obj[key];
    //劫持obj对象
    Object.defineProperty(obj, key, {
        //get可以监听对象的依赖,即谁使用了对象
        get() {
            return value;
        },
        //set可以监听对象属性的改变
        set(newValue) {
            value = newValue;
            depend.notify();
        },
    });
});

function foo1() {
    console.log(obj.name, '第一次');
}
function foo2() {
    console.log(obj.name, '第二次');
}
depend.addDepend(foo1);
depend.addDepend(foo2);
obj.name = 'coder';
// coder 第一次
// coder 第二次

vue3使用Proxy构造函数和Reflect内置对象

class Depend {
    constructor() {
        this.reactiveFns = [];
    }
    // 添加依赖
    addDepend(fn) {
        this.reactiveFns.push(fn);
    }
    // 遍历执行收集的依赖函数
    notify() {
        this.reactiveFns.forEach(item => item());
    }
}

const depend = new Depend();
const obj = { name: 'moon', age: 18 };

const objProxy = new Proxy(obj, {
    get(target, key, receiver) {
        // 获取对象身上某个属性的值,类似于target[key]
        return Reflect.get(target, key, receiver);
    },
    set(target, key, newValue, receiver) {
        // 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。
        Reflect.set(target, key, newValue, receiver);
        depend.notify();
    },
});

function foo1() {
    console.log(objProxy.name, '第一次');
}
function foo2() {
    console.log(objProxy.name, '第二次');
}
depend.addDepend(foo1);
depend.addDepend(foo2);
objProxy.name = 'vue3';
// vue3 第一次
// vue3 第二次

vue2和vue3的区别

  • vue2通过Object.defineProperty直接修改obj对象,实际已经将obj的数据描述符改变为存取描述符,vue3是监控Proxy的对象(是obj对象外层的对象),不会修改obj的描述符。
  • vue2是遍历obj对象的属性,所以对于新增的属性是无法监听的的,而vue3对于新增的数据依然可以监听。
  • Object.defineProperty只有get,set捕获器,而proxy有13种捕获器(比如in,delete操作符都能监听到)能力更强。
  • proxy可以监听数组

自动收集依赖

上面的代码可以自动派发通知,但是依赖仍然是手动收集的。接下来完成自动收集,后续代码使用proxy

思路是声明一个全局函数watchFn和一个全局变量reactiveFn,watchFn的参数是函数,然后调用watchFn将参数保存在reactiveFn中,调用参数,然后情况reactiveFn,代码如下:

class Depend {
    constructor() {
        this.reactiveFns = [];
    }
    // 添加依赖
    addDepend() {
        if (reactiveFn) {
            this.reactiveFns.push(reactiveFn);
        }
    }
    // 遍历执行收集的依赖函数
    notify() {
        this.reactiveFns.forEach(item => item());
    }
}
// 声明变量
let reactiveFn = null;
// 使用全局函数收集依赖并保存在reactiveFn中
function watchFn(fn) {
    reactiveFn = fn;
    fn();
    reactiveFn = null;
}
const depend = new Depend();
const obj = { name: 'moon', age: 18 };

const objProxy = new Proxy(obj, {
    get(target, key, receiver) {
        // 获取对象身上某个属性的值,类似于target[key]
        depend.addDepend();
        return Reflect.get(target, key, receiver);
    },
    set(target, key, newValue, receiver) {
        // 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。
        Reflect.set(target, key, newValue, receiver);
        depend.notify();
    },
});

function foo1() {
    console.log(objProxy.name, '第一次');
}
function foo2() {
    console.log(objProxy.name, '第二次');
}
function foo3() {
    console.log(objProxy.age, '第一次');
}
function foo4() {
    console.log(objProxy.age, '第二次');
}
watchFn(foo1);
watchFn(foo2);
watchFn(foo3);
watchFn(foo4);
objProxy.name = 'vue3';
moon 第一次
moon 第二次
18 第一次
18 第二次
vue3 第一次
vue3 第二次
18 第一次
18 第二次

代码优化

目前代码可以实现自动收集依赖和自动派发,但是仍存在问题

  1. 所有的依赖函数都存在同一数组中,一个属性改变其他属性的依赖函数也会被调用。
  2. 使用数组保存依赖函数会造成添加过依赖的函数重复被添加。

依赖函数存放优化

第一个问题的解决办法是通过数据结构来规划依赖函数的存放,使用WeakMap来存放对象和相应依赖,如图所示

image.png 实现代码如下:

const targetMap = new WeakMap();
function getDepend(target, key) {
    let map = targetMap.get(target);
    if (!map) {
        map = new Map();
        targetMap.set(target, map);
    }
    let depend = map.get(key);
    if (!depend) {
        depend = new Depend();
        map.set(key, depend);
    }
    return depend;
}

重复调用优化

将Depend类中的reactiveFns所使用的的数组改成set即可。 代码如下

class Depend {
    constructor() {
        this.reactiveFns = new Set();
    }
    // 添加依赖
    addDepend() {
        if (reactiveFn) {
            this.reactiveFns.add(reactiveFn);
        }
    }
    // 遍历执行收集的依赖函数
    notify() {
        this.reactiveFns.forEach(item => item());
    }
}

完整代码

class Depend {
    constructor() {
        this.reactiveFns = new Set();
    }
    // 添加依赖
    addDepend() {
        if (reactiveFn) {
            this.reactiveFns.add(reactiveFn);
        }
    }
    // 遍历执行收集的依赖函数
    notify() {
        this.reactiveFns.forEach(item => item());
    }
}
// 声明变量
let reactiveFn = null;
// 使用全局函数收集依赖并保存在reactiveFn中
function watchFn(fn) {
    reactiveFn = fn;
    fn();
    reactiveFn = null;
}

const obj = { name: 'moon', age: 18 };

const targetMap = new WeakMap();
function getDepend(target, key) {
    let map = targetMap.get(target);
    if (!map) {
        map = new Map();
        targetMap.set(target, map);
    }
    let depend = map.get(key);
    if (!depend) {
        depend = new Depend();
        map.set(key, depend);
    }
    return depend;
}

const objProxy = new Proxy(obj, {
    get(target, key, receiver) {
        const depend = getDepend(obj, key);
        depend.addDepend();
        // 获取对象身上某个属性的值,类似于target[key]
        return Reflect.get(target, key, receiver);
    },
    set(target, key, newValue, receiver) {
        // 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。
        Reflect.set(target, key, newValue, receiver);
        const depend = getDepend(obj, key);
        depend.notify();
    },
});

function foo1() {
    console.log(objProxy.name, '第一次');
}
function foo2() {
    console.log(objProxy.name, '第二次');
}
function foo3() {
    console.log(objProxy.age, '第一次');
}
function foo4() {
    console.log(objProxy.age, '第二次');
}
watchFn(foo1);
watchFn(foo2);
watchFn(foo3);
watchFn(foo4);

objProxy.name = 'vue3';
// moon 第一次  
// moon 第二次
// 18 第一次
// 18 第二次
// -------------以上是收集依赖时自动调用一次
// vue3 第一次
// vue3 第二次