Loading

Vue3响应式原理解析

Vue3响应式简介

Vue响应式系统的核心依然是对数据进行劫持,只不过Vue3采样点是Proxy类,而Vue2采用的是Object.defineProperty()。Vue3之所以采用Proxy类主要有以下原因:

  1. 可以提升性能,Vue2是通过层层递归的方式对数据进行劫持,并且数据劫持一开始就要进行层层递归(一次性递归),如果对象的路径非常深将会非常影响性能。而Proxy可以在用到数据的时候再进行对下一层属性的劫持。
  2. Proxy可以实现对整个对象的劫持,而Object.defineProperty()只能实现对对象的属性进行劫持。所以对于对象上的方法或者新增、删除的属性则无能为力。
  3. Vue2响应式数据在对数组进行处理时,是对数组的几个方法进行了拦截。而且并没有对数组的每个索引进行劫持,对数组的长度变化,数组的插入删除元素无法进行响应式处理。 而Vue3采用Proxy在数组长度变化时或者插值时能及时的响应。
// 展示使用Object.defineProperty()存在的缺点
const obj = {name: "vue", arr: [1, 2, 3]};
Object.keys(obj).forEach((key) => {
    let value = obj[key];
    Object.defineProperty(obj, key, {
        get() {
            console.log(`get key is ${key}`);
            return value;
        },
        set(newVal) {
            console.log(`set key is ${key}, newVal is ${newVal}`);
            value = newVal;
        }
    });
});
// 此时给对象新增一个age属性
obj.age = 18; // 因为对象劫持的时候,没有对age进行劫持,所以新增属性无法劫持
delete obj.name; // 删除对象上已经进行劫持的name属性,发现删除属性操作也无法劫持
obj.arr.push(4); // 无法劫持数组的push等方法
obj.arr[3] = 4; // 无法劫持数组的索引操作,因为没有对数组的每个索引进行劫持,并且由于性能原因,Vue2并没有对数组的每个索引进行劫持

// 使用Proxy实现完美劫持
const obj = {name: "vue", arr: [1, 2, 3]};
function proxyData(value) {
    const proxy = new Proxy(value, {
        get(target, key) {
            console.log(`get key is ${key}`);
            const val = target[key];
            if (typeof val === "object") {
                return proxyData(val);
            }
            return val;
        },
        set(target, key, value) {
            console.log(`set key is ${key}, value is ${value}`);
            return target[key] = value;
        },
        deleteProperty(target, key) {
            console.log(`delete key is ${key}`);
        }
    });
    return proxy;
}
const proxy = proxyData(obj);
proxy.age = 18; // 可对新增属性进行劫持
delete proxy.name; // 可对删除属性进行劫持
proxy.arr.push(4); // 可对数组的push等方法进行劫持
proxy.arr[3] = 4; // 可对象数组的索引操作进行劫持

Vue3响应式示例

Vue3的响应式系统被放到了一个单独的@vue/reactivity模块中,其提供了reactive、effect、computed等方法,其中reactive用于定义响应式的数据,effect相当于是Vue2中的watcher,computed用于定义计算属性。
源码目录结构:
image.png!

import {reactive, effect, computed} from "@vue/reactivity";
const state = reactive({
    name: "vincent",
    age: 24,
    arr: ['a','b','c']
});
console.log(state); // 这里返回的是Proxy代理后的对象
effect(() => {
    console.log("effect run");
    app.innerHTML = state.name + '==' + state.age + '_' + state.arr.length
    console.log(state.name); // 每当name数据变化将会导致effect重新执行
});


setTimeout(() => {
  state.name = "vue"; // 数据发生变化后会触发使用了该数据的effect重新执行
  state.arr.length = 2
}, 1000)


const user = computed(() => { // 创建一个计算属性,依赖name和age
    return `name: ${state.name}, age: ${state.age}`;
});
effect(() => { // name和age变化会导致计算属性的value发生变化,从而导致当前effect重新执行
    console.log(`user is ${user.value}`);
});

reactive实现原理

Vue3中采用proxy实现数据代理, 核心就是拦截get方法和set方法,当获取值时收集effect函数,当修改值时触发对应的effect重新执行

reactive() 方法本质是传入一个要定义成响应式的target目标对象,然后通过Proxy类去代理这个target对象,最后返回代理之后的对象,如:

export function reactive(target) {
    return new Proxy(target, {
        get() {

        },
        set() {
            
        }
    });
}

源码:

// 创建响应式数据
function reactive(target) { 
    if (target && target["__v_isReadonly" /* IS_READONLY */]) {
        return target;
    }
    return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);
}

// 创建浅的响应式数据
function shallowReactive(target) {
    return createReactiveObject(target, false, shallowReactiveHandlers, shallowCollectionHandlers, shallowReactiveMap);
}

// 创建仅读
function readonly(target) {
    return createReactiveObject(target, true, readonlyHandlers, readonlyCollectionHandlers, readonlyMap);
}

// 创建浅的仅读数据
function shallowReadonly(target) {
    return createReactiveObject(target, true, shallowReadonlyHandlers, shallowReadonlyCollectionHandlers, shallowReadonlyMap);
}

function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
    // 对目标数据进行判断,如果不是对象 直接返回
    if (!isObject(target)) {
        return target;
    }
    if (target["__v_raw" /* RAW */] &&
        !(isReadonly && target["__v_isReactive" /* IS_REACTIVE */])) {
        return target;
    }
    // 目标对象是否有被代理过
    const existingProxy = proxyMap.get(target);
    // 被代理过了 直接返回缓存中的代理数据
    if (existingProxy) {
        return existingProxy;
    }
    // 是否需要被跳过
    const targetType = getTargetType(target);
    if (targetType === 0 /* INVALID */) {
        return target;
    }
    // 对目标对象进行代理 并判断是普通对象、数组还是集合类型数据
    const proxy = new Proxy(target, targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers);
    // 把代理后的数据进行缓存
    proxyMap.set(target, proxy);
    return proxy;

vue3新增了Map,Set, WeakMap,WeakSet等集合。在对target对象进行代理时要分开进行处理。在对目标对象进行拦截时 最重要的就是collectionHandlersbaseHandlers

baseHandlers为例:baseHandlers即mutableHandlers,Proxy的handler可以代理很多方法,比如get、set、deleteProperty、has、ownKeys,如果将这些方法直接都写在handlers上,那么handlers就会变得非常多代码,所以可以将这些方法分开

import { isObject } from "@vue/shared";
import { reactive, readonly } from "./reactive";

const get = createGetter();
const shallowGet = createGetter(false, true);
const readonlyGet = createGetter(true);
const shallowReadonlyGet = createGetter(true, true)

const set = createSetter();
const shallowSet = createSetter(true);

/**
 * @param isReadonly 是不是仅读
 * @param shallow 是不是浅响应
 */
function createGetter(isReadonly = false, shallow = false) {
    return function get(target, key, receiver) {
        const res = Reflect.get(target, key, receiver);

        if (!isReadonly) { // 如果是仅读的无需收集依赖
            console.log('依赖收集')
        }

        if (shallow) { // 浅无需返回代理
            return res
        }

        if (isObject(res)) { // 取值时递归代理
            return isReadonly ? readonly(res) : reactive(res)
        }
        return res;
    }
}

function createSetter(shallow = false) {
    return function set(target, key, value, receiver) {
        const result = Reflect.set(target, key, value, receiver);
        return result;
    }
}

export const mutableHandlers = {
    get,
    set
};
export const readonlyHandlers = {
    get: readonlyGet,
    set(target, key) {
        console.warn(`Set operation on key "${String(key)}" failed: target is readonly.`)
        return true;
    }
};
export const shallowReactiveHandlers = {
    get: shallowGet,
    set: shallowSet
};
export const shallowReadonlyHandlers = {
    get: shallowReadonlyGet,
    set(target, key) {
        console.warn(`Set operation on key "${String(key)}" failed: target is readonly.`)
        return true;
    }
};

Proxy的handlers对象中的get和set方法都可以拿到被代理的对象target、获取或修改了对象的哪个key,设置了新的值value,以及被代理后的对象receiver,目前拦截到用户的get操作后仅仅是从target中取出对应的值并返回回去,拦截到用户的set操作后仅仅是修改了target中对应key的值并返回回去。

对于数组中的操作 此时会存在一个问题,如果我们执行state.arr.push(4)这样的一个操作,会发现仅仅触发了arr的取值操作,并没有收到arr新增了一个值的通知。因为Proxy代理只是浅层的代理,只代理了一层,所以我们拿到的arr是一个普通数组,此时对普通数组进行操作是不会收到通知的。正是由于Proxy是浅层代理,所以避免了一上来就递归,我们需要修改get,在取到的值是对象的时候再去代理这个对象,如:

function createGetter(isReadonly = false, shallow = false) {
    return function get(target, key, receiver) {
        const res = Reflect.get(target, key, receiver); // 等价于target[key]
        console.log(`拦截到了get取值操作`, target, key);
      + if (isObject(res)) { // 如果取到的值是一个对象,则代理这个值
      +    return reactive(res);
      + }
        return res;
    }
}

此时我们再次执行state.arr.push(4),可以看到输出结果如下:

拦截到了get取值操作 {name: "lihb", age: 18, arr: Array(3)} arr
拦截到了get取值操作 (3) [1, 2, 3] push
拦截到了get取值操作 (3) [1, 2, 3] length
拦截到了set设置值操作 (4) [1, 2, 3, 4] 3 4
拦截到了set设置值操作 (4) [1, 2, 3, 4] length 4

同时也触发了length的修改,其实我们将4 push进入数组后,数组的length会自动修改,也就是说不需要再去设置一遍length的值了,同样的我们执行state.arr[0] = 1也会触发set操作,设置的是同样的值也会触发set操作,所以我们需要判断一下设置的新值和旧值是否相同,不同才需要触发set操作。

import { isObject, hasOwn, hasChanged } from "./shared";
function createSetter(shallow = false) {
    return function set(target, key, value, receiver) {
        const hadKey = hasOwn(target, key);
        const oldValue = target[key]; // 修改前获取到旧的值
        const result = Reflect.set(target, key, value, receiver); // 等价于target[key] = value
        if (!hadKey) {  // 如果当前target对象中没有该key,则表示是新增属性
            console.log(`用户新增了一个属性,key is ${key}, value is ${value}`);
        } else if (hasChanged(value, oldValue)) { // 判断一下新设置的值和之前的值是否相同,不同则属于更新操作
            console.log(`用户修改了一个属性,key is ${key}, value is ${value}`);
        }
        return result;
    }
}

此时再次执行state.arr.push(4)就不会触发length的更新了,执行state.arr[0] = 1也不会触发索引为0的值更新了。

posted @ 2021-10-08 15:57  vincent_cyi  阅读(1801)  评论(0编辑  收藏  举报