Vue3响应式原理解析
Vue3响应式简介
Vue响应式系统的核心依然是对数据进行劫持,只不过Vue3采样点是Proxy类,而Vue2采用的是Object.defineProperty()。Vue3之所以采用Proxy类主要有以下原因:
- 可以提升性能,Vue2是通过层层递归的方式对数据进行劫持,并且数据劫持一开始就要进行层层递归(一次性递归),如果对象的路径非常深将会非常影响性能。而Proxy可以在用到数据的时候再进行对下一层属性的劫持。
- Proxy可以实现对整个对象的劫持,而Object.defineProperty()只能实现对对象的属性进行劫持。所以对于对象上的方法或者新增、删除的属性则无能为力。
- 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用于定义计算属性。
源码目录结构:
!
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对象进行代理时要分开进行处理。在对目标对象进行拦截时 最重要的就是collectionHandlers
和baseHandlers
。
以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的值更新了。