Vue3响应式
- 在Vue2的时候使用defineProperty来进行数据的劫持,需要对属性进行重写setter和getter性能差
- 当新增属性和删除属性时无法监听变化,需要通过$set和$delete实现
- 数组不采用defineProperty来劫持(对所有索引进行劫持将浪费性能),所以对数组需要进行单独处理
- Vue3中采用Proxy来实现响应式,从而解决了上述问题
使用Vue3中的reactivity
pnpm install vue -w
创建reactivity/dist/index.html文件,引入官方reactivity
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="../../../node_modules/@vue/reactivity/dist/reactivity.global.js"></script>
<script>
// effect 副作用函数,如果此函数依赖的数据变化将会重新执行
// reactive 将数据变成响应式,其实就是一个Proxy
// shallowReactive 浅响应式
// readonly 只读的
// shallowReadonly readonly shallowReactive 都是基于reactive
const { effect, reactive, shallowReactive, readonly, shallowReadonly } =
VueReactivity;
// 创建一个响应式数据
const state = reactive({ name: 'bowen', age: 18, desc: { money: 0 } });
// 对象中的对象也是一个Proxy
// console.log(state.desc);
// 浅响应式
// const littleState = shallowReactive({ name: 'bowen', age: 18, desc: { money: 0 } });
// 浅响应式中对象里的对象不是Proxy
// console.log(littleState.desc);
// 只读将不能更改属性值
// const state = readonly({ name: 'bowen', age: 18, desc: { money: 0 } });
// 深层属性可以更改但是不会触发响应式
// const state = shallowReadonly({ name: 'bowen', age: 18, desc: { money: 0 } });
// 副作用逻辑
// effect 默认会执行一次,对响应式数据进行取值
// 取值的过程中数据会依赖于当前的effect
// name和age变化将会重新执行effect函数
effect(() => {
app.innerHTML = `我叫${state.name},今年${state.age}岁。`;
});
setTimeout(() => {
state.age = 24;
}, 2000);
</script>
</body>
</html>
创建共享方法模块
创建shared/src/index.ts
// 判断是否是不为null的对象
export const isObject = function (obj) {
return typeof obj === 'object' && obj !== null;
};
实现reactive
创建reactivity/src/baseHandler.ts文件
import { track, trigger } from './effect';
// 响应式标识枚举
export const enum ReactiveFlags {
IS_REACTIVE = '__vue_isReactive__',
}
export const mutableHandlers = {
get(target, key, receiver) {
if (key === ReactiveFlags.IS_REACTIVE) {
return true;
}
track(target, 'get', key);
const result = Reflect.get(target, key, receiver);
if(isObject(result)) {
// 深度代理
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
// 值变化了,要进行更新
trigger(target, 'set', key, value, oldValue);
}
return result;
},
};
创建reactivity/src/reactive.ts文件
import { isObject } from '@vue/shared';
import { mutableHandlers, ReactiveFlags } from './baseHandler';
// 代理缓存,防止同一个对象生成多个代理
// WeakMap不会导致内存泄漏,key只能是对象
const reactiveMap = new WeakMap();
/**
* 将数据转换成响应式数据
* 实现同一对象多次代理只返回同一个代理
* 代理对象二次代理将直接返回传入的代理
*/
export function reactive(target) {
// 只能做对象的代理
if (!isObject(target)) {
return;
}
// 如果传入的是已经代理过的Proxy无需再次代理
if (target[ReactiveFlags.IS_REACTIVE]) {
return target;
}
// 传入的对象是否已经进行了代理
const existProxy = reactiveMap.get(target);
if (existProxy) {
return existProxy;
}
// 不会对属性进行重新定义
// 只是对取值和赋值进行代理
const proxy = new Proxy(target, mutableHandlers);
reactiveMap.set(target, proxy);
return proxy;
}
实现effect
创建reactivity/src/effect.ts文件
export let activeEffect = undefined;
// 创建响应式effect
class ReactiveEffect {
// 默认是激活状态
public active = true;
// 保证effect嵌套情况下的正确关联
public parent = null;
// 收集依赖了哪些值 effect记录属性
public deps = [];
constructor(public fn) {}
// 执行effect
public run() {
// 如果是非激活的状态,只需要执行函数,不需要进行依赖收集
if (!this.active) {
return this.fn();
}
try {
this.parent = activeEffect;
// 依赖收集,将当前的effect和渲染的属性关联在一起
activeEffect = this;
// 当调用取值操作的时候就可以获取到全局的activeEffect
return this.fn();
} finally {
activeEffect = this.parent;
this.parent = null;
}
}
public stop() {
this.active = false;
}
}
export function effect(fn) {
// fn可以根据状态变化重新执行
// effect可以嵌套
const _effect = new ReactiveEffect(fn);
// 默认先执行一次
_effect.run();
}
/**
* 属性收集
* 一个effect对应多个属性
* 一个属性对应多个effect
* 对象中的某个属性可能对应多个effect
* weekMap = { 对象: Map{ name: Set } }
*/
const targetMap = new WeakMap();
export function track(target, type, key) {
if (!activeEffect) {
return;
}
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
let shouldTrack = !dep.has(activeEffect);
if (shouldTrack) {
dep.add(activeEffect);
// effect记录属性,清理时用
activeEffect.deps.push(dep);
}
}
export function trigger(target, type, key, value, oldValue) {
const depsMap = targetMap.get(target);
if (!depsMap) {
// 触发的值不在模板中使用
return;
}
// 获取属性对应的key
const effects = depsMap.get(key);
if (effects) {
effects.forEach((effect) => {
if (effect !== activeEffect) {
// 屏蔽掉相同的调用
effect.run();
}
});
}
}
导出自己的方法
reactivity/src/index.ts
export { effect } from './effect';
export { reactive } from './reactive';
使用reactivity
创建reactivity/dist/index.html文件,引入自己的reactivity
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./reactivity.global.js"></script>
<script>
const { effect, reactive } = VueReactivity;
// 创建一个响应式数据
const obj = { name: 'bowen', age: 18, desc: { money: 0 } };
const state = reactive(obj);
effect(() => {
document.getElementById(
'app'
).innerHTML = `我是${state.name},今年${state.age}岁.`;
});
setTimeout(() => {
state.age++
}, 1000);
</script>
</body>
</html>
依赖收集问题
例子:reactivity/dist/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./reactivity.global.js"></script>
<script>
const { effect, reactive } = VueReactivity;
const obj = { name: 'bowen', age: 18, flag: true };
const state = reactive(obj);
effect(() => {
console.log('渲染');
document.getElementById('app').innerHTML = state.flag ? state.name : state.age;
});
setTimeout(() => {
state.flag = false;
setTimeout(() => {
console.log('修改了name,但是这时显示的是age,所以不应该执行渲染函数');
state.name = 'xc';
}, 1000);
}, 1000);
</script>
</body>
</html>
清空依赖,重新收集
当执行函数时,我们应该清空收集的依赖,然后进行重新收集
reactivity/src/effect.ts中新增函数cleanupEffect
function cleanupEffect(effect) {
const { deps } = effect;
deps.forEach((item) => {
item.delete(effect);
});
effect.deps.length = 0;
}
reactivity/src/effect.ts中ReactiveEffect执行函数时执行cleanupEffect
try {
this.parent = activeEffect;
// 依赖收集,将当前的effect和渲染的属性关联在一起
activeEffect = this;
// 清空依赖属性,重新收集
cleanupEffect(this);
// 当调用取值操作的时候就可以获取到全局的activeEffect
return this.fn();
} finally {
activeEffect = this.parent;
this.parent = null;
}
reactivity/src/effect.ts中trigger执行effects时先拷贝一份再执行解决Set死循环问题
let effects = depsMap.get(key);
if (effects) {
effects = [...effects];
effects.forEach((effect) => {
if (effect !== activeEffect) {
// 屏蔽掉相同的调用
effect.run();
}
});
}