vue3.0 响应式

478 阅读8分钟

vue2.x 是利用 Object.defineProperty 方法侦测对象的属性变化,但该方法有一些固有的缺陷:

  1. 性能较差;
  2. 在对象上新增属性是无法被侦测的;
  3. 改变数组的 length 属性是无法被侦测的。

vue3.0 是非常大的重构,源码使用 TypeScript 重写,目前的代码 98% 以上使用 TypeScript 编写。其中数据响应化 使用 ES6 的 Proxy 取代 Object.defineProperty 方法,性能更优异,而且数组和对象一样,可以直接触发 get 和 set 方法。Proxy 称为代理,是一种可以拦截并改变底层 javascript 引擎操作的包装器。

调用 new Proxy(target, handler)可以伟一个目标对象创建一个代理,代理可以拦截 javascript 引擎内部目标的底层对象操作,这些底层操作被拦截后会触发响应特定操作的陷阱函数,在调用 Proxy 构造函数时,需要传入两个参数,target 为目标对象,handler 是一个包含陷阱函数的处理器对象。

下面使用 Proxy 模拟实现 vue3.0 的响应式系统。

对象代理

// 创建对象响应式核心方法
function reactive(target){
    // 如果 target 不是对象,则直接返回
    if(target === null || typeof target !== 'object') return target;

    return new Proxy(target, {
        get(target, property, receiver) {
            console.log('获取值')
            const result = Reflect.get(target, property, receiver);
            return result;
        },
        set(target, property, value, receiver) {
            console.log('设置值')
            const result = Reflect.set(target, property, value, receiver);
            return result;
        },
        deleteProperty(target, property) {
            return Reflect.deleteProperty(target, property);
        }
    });
}

Reflect 是一个内置对象,它提供了可拦截 javascript 操作的方法。每个代理陷阱对应一个命名和参数都相同的 Reflect 方法。

const obj = {name: 'icon'};
const proxy = reactive(obj);
proxy.name = 'lee';
console.log(proxy.name);

上面执行结果为

企业微信截图_16304016202455.png

这样就算了初步实现了数据对象的代理,但是这样的话是无法侦测多层对象的。所以我们需要在 get 陷阱函数中对返回值做一个判断,如果返回值是一个对象,则返回值也创建代理对象,也就是递归调用。

function reactive(target){
    ...
    get(target, property, receiver) {
        console.log('获取值')
        const result = Reflect.get(target, property, receiver);
        // 这里新增了多层对象的判断
        if(target !== null || typeof target === 'object') return reactive(result);
        return result;
    },
    ...
}
const obj = {name: 'icon', address: {province: '广东省'}};
const proxy = reactive(obj);
proxy.address.province = '北京';

上面执行结果为

企业微信截图_16304029002640.png

这样就解决了多层对象无法侦测的问题,但是接下来又有新的问题,比如

  • 对统一目标对象进行了多次代理
  • 目标对象代理后又对代理对象进行代理 这两种情况是没有意义的,接下来我们一起使用 WeakMap 数据结构 来解决,当然有别的解决方法,这就看你自己的咯。
// key: 目标对象 指向 value: 代理对象
const toProxy = new WeakMap();
// key: 代理对象 指向 value: 目标对象
const toRaw = new WeakMap();

function reactive(target){
    ...
    
    // 如果目标对象已经有代理对象,则直接返回该代理对象
    const proxy = toProxy.get(target);
    if(proxy) return proxy;
    
    // 如果目标对象是代理对象,并且有对应的真实对象,则直接返回对象
    if(toRaw.get(target)) return target;
    
    // 这里就不直接返回 proxy 对象了, 因为需要存储
    const observerd = new Proxy(target, {
        ...
    });
    
    toProxy.set(target, observerd);
    toRaw.set(observerd, target);
    
    // 最后再返回 proxy 对象
    return observerd;
}

大家是否以为这样就完美了呢?

当然不是了,如果你去执行数组的 push 的话,会发现 get, set 陷阱函数 执行了两次。

企业微信截图_16304044087406.png

因为 push 方法向数组中添加元素的同时还会修改数组的长度,因此有两次陷阱函数的触发,一次是将数组索引为 3 的位置新增值为 4,一次是修改数组的 length 属性为 4。这样的话,假如在 set 陷阱函数中更新视图,那么就会出现更新两次的情况。

接下来我们再加上对属性是新增还是修改的相关判断

    ...
    
    const observerd = new Proxy(target, {
        ...
        
        set(target, property, value, receiver) {
            console.log('设置值')
            const oldValue = Reflect.get(target, property);
            const result = Reflect.set(target, property, value, receiver);
            // 判断当前对象是否有指定属性
            if(!target.hasOwnProperty(property)) {
                console.log('新增属性')
            }else if(oldValue !== value) {
                console.log('修改属性')
            }
            return result;
        },
        
        ...
    });
    
    ...

好了,这样的话数据对象的代理就告一段落咯。

依赖收集

接下来就是 vue3.0 中比较难理解的依赖收集了,vue3.0 使用了 effect 函数来包装依赖,称为副作用。effect 函数的模拟实现如下。

...

// 保存 effect 的数组,以栈的形式存储
const effectStack = [];

function effect(fn) {
    // 创建响应式 effect
    const effect = createReactiveEffect(fn);
    // 默认先执行一次 effect,本质上调用的是传入的 fn 函数
    effect();
}

// 创建响应式 effect 函数
function createReactiveEffect(fn) {
    // 响应式 effect
    const effect = function() {
        try {
            // 将 effect 保存到全局 effectStack 栈中
            effectStack.push(effect);
            return fn();
        } finally {
            // 调用玩依赖后,弹出 effect
            effectStack.pop();
        }
    }
    return effect;
}
const proxy = reactive({name: 'icon'});
effect(() => {
    console.log(proxy.name);
});
proxy.name = 'lee';

运行结果

企业微信截图_16304067535793.png

从输出结果中可以看到,除了默认执行一次的 effect 外,当name 属性发生变化时,effect 并没有被执行。为了在对象属性发生变化时,让 effect 再次执行,需要将对象的属性与 effect 进行关联,这可以采用 Map 来存储,考虑到一个属性可能会关联多个依赖,那么存储在映射关系应该是 key 为对象属性,value 为 保存所有 effect 的 Set 对象,之所以选择 Set 而不是数组,是因为 Set 有去重的效果。另外,属性毕竟是对象的属性,不能脱离对象而单独存在,要跟踪不同对象属性的依赖,还需要一个 WeakMap, key 为对象本身,value 为保存所有属性与依赖关系的 Map。

定义好数据结构后,我们接下来就可以编写一个依赖收集函数 track。

...

// 保存对象与其属性依赖关系的 Map,key 是对象,value 是 Map
const targetMap = new WeakMap();

// 跟踪依赖
function track(target, property) {
    // 获取全局数组栈 effectStack 中的依赖
    const effect = effectStack[effectStack.length - 1];
    
    // 如果存在依赖
    if(effect) {
        // 取出该对象对应的 Map
        let depsMap = targetMap.get(target);
        // 如果不存在,则以目标对象为 key,新建的 Map 为 value,保存到 targetMap 中
        if(!depsMap) {
            targetMap.set(target, depsMap = new Map())
        }
        // 从 Map 中取出该属性对应的所有 effect
        let deps = depsMap.get(property);
        // 如果不存在,则以属性为 key,新建的 set 为 value,存储到 depsMap 中
        if(!deps) {
            depsMap.set(property, deps = new Set());
        }
        // 判断 Set 中是否已经存在 effect,如果没有,则添加到 deps 中
        if(!deps.has(effect)) {
            deps.add(effect);
        }
    }
}

接下来是当属性发生变化时,触发属性关联的所有 effect 执行,因此,我们再编写一个 trigger 函数

...

// 执行属性关联的所有 effect
function trigger(target, type, property) {
    const depsMap = targetMap.get(target);
    if(depsMap) {
        let deps = depsMap.get(property);
        // 当前属性关联的所有 effect 依次执行
        if(deps) {
            deps.forEach(effect => effect());
        }
    }
}

依赖收集的函数和触发依赖的函数都 OK 了,那么自然需要在某个地方去执行收集依赖和触发依赖,依赖收集放到 get 陷阱函数中,而触发依赖是在属性发生变化时执行依赖,那当然是 set 陷阱函数中。

在Proxy 中 添加 依赖的收集和触发

完善代码

// key: 目标对象 指向 value: 代理对象
const toProxy = new WeakMap();

// key: 代理对象 指向 value: 目标对象
const toRaw = new WeakMap();

function reactive(target){
    // 如果 target 不是对象,则直接返回
    if(target === null || typeof target !== 'object') return target;
    
    // 如果目标对象已经有代理对象,则直接返回该代理对象
    const proxy = toProxy.get(target);
    if(proxy) return proxy;
    
    // 如果目标对象是代理对象,并且有对应的真实对象,则直接返回对象
    if(toRaw.get(target)) return target;
    
    // proxy 代理
    const observerd = new Proxy(target, {
        get(target, property, receiver) {
            console.log('获取值')
            const result = Reflect.get(target, property, receiver);
            // 依赖收集
            track(target, property);
            if(target !== null || typeof target === 'object') return reactive(result);
            return result;
        },
        set(target, property, value, receiver) {
            console.log('设置值')
            const oldValue = Reflect.get(target, property);
            const result = Reflect.set(target, property, value, receiver);
            // 判断当前对象是否有指定属性
            if(!target.hasOwnProperty(property)) {
                console.log('新增属性')
                trigger(target, 'add', property);
            }else if(oldValue !== value) {
                console.log('修改属性')
                trigger(target, 'set', property);
            }
            return result;
        },
        deleteProperty(target, property) {
            return Reflect.deleteProperty(target, property);
        }
    });
    toProxy.set(target, observerd);
    toRaw.set(observerd, target);
    
    // 最后再返回 proxy 对象
    return observerd;
}

// 保存 effect 的数组,以栈的形式存储
const effectStack = [];

function effect(fn) {
    // 创建响应式 effect
    const effect = createReactiveEffect(fn);
    // 默认先执行一次 effect,本质上调用的是传入的 fn 函数
    effect();
}

// 创建响应式 effect 函数
function createReactiveEffect(fn) {
    // 响应式 effect
    const effect = function() {
        try {
            // 将 effect 保存到全局 effectStack 栈中
            effectStack.push(effect);
            return fn();
        } finally {
            // 调用玩依赖后,弹出 effect
            effectStack.pop();
        }
    }
    return effect;
}

// 保存对象与其属性依赖关系的 Map,key 是对象,value 是 Map
const targetMap = new WeakMap();

// 跟踪依赖
function track(target, property) {
    // 获取全局数组栈 effectStack 中的依赖
    const effect = effectStack[effectStack.length - 1];
    
    // 如果存在依赖
    if(effect) {
        // 取出该对象对应的 Map
        let depsMap = targetMap.get(target);
        // 如果不存在,则以目标对象为 key,新建的 Map 为 value,保存到 targetMap 中
        if(!depsMap) {
            targetMap.set(target, depsMap = new Map())
        }
        // 从 Map 中取出该属性对应的所有 effect
        let deps = depsMap.get(property);
        // 如果不存在,则以属性为 key,新建的 set 为 value,存储到 depsMap 中
        if(!deps) {
            depsMap.set(property, deps = new Set());
        }
        // 判断 Set 中是否已经存在 effect,如果没有,则添加到 deps 中
        if(!deps.has(effect)) {
            deps.add(effect);
        }
    }
}

// 执行属性关联的所有 effect
function trigger(target, type, property) {
    const depsMap = targetMap.get(target);
    if(depsMap) {
        let deps = depsMap.get(property);
        // 当前属性关联的所有 effect 依次执行
        if(deps) {
            deps.forEach(effect => effect());
        }
    }
}

到现在我们 模拟实现 vue3.0 响应式的代码就全部编写完毕啦,当然 vue3.0 的响应式 API 不止 reactive 还包括 ref、computed、watch,其内部原理都是类似的。