禁止转载,侵权必究!
Reactive & Effect
Vue3 对比 Vue2 的响应式变化
- 在 Vue2 的时候使用 defineProperty 来进行数据的劫持, 需要对属性进行重写添加 getter 及 setter 性能差。
- 当新增属性和删除属性时无法监控变化。需要通过delete 实现
- 数组不采用 defineProperty 来进行劫持 (浪费性能,对所有索引进行劫持会造成性能浪费)需要对数组单独进行处理 重写方法(push pop shift unshift splice sort reverse)
Vue3 中使用 Proxy 来实现响应式数据变化。从而解决了上述问题。
CompositionAPI
- 在 Vue2 中采用的是 OptionsAPI, 用户提供的 data,props,methods,computed,watch 等属性 (用户编写复杂业务逻辑会出现反复横跳问题)
- Vue2 中所有的属性都是通过 this 访问,this 存在指向明确问题
- Vue2 中很多未使用方法或属性依旧会被打包,并且所有全局 API 都在 Vue 对象上公开。Composition API 对 tree-shaking 更加友好,代码也更容易压缩。
- 组件逻辑共享问题, Vue2 采用 mixins 实现组件之间的逻辑共享; 但是会有数据来源不明确,命名冲突等问题。 Vue3 采用 CompositionAPI 提取公共逻辑非常方便
Reactivity 模块基本使用
pnpm install @vue/reactivity -w
<div id="app"></div>
<script type="module">
import {
reactive,
effect,
} from "/node_modules/@vue/reactivity/dist/reactivity.esm-browser.js";
const state = reactive({ name: "jw", age: 30 });
effect(() => {
// 副作用函数 默认执行一次,响应式数据变化后再次执行
app.innerHTML = state.name + "今年" + state.age + "岁了";
});
setTimeout(() => {
state.age++;
}, 1000);
</script>
reactive 方法会将对象变成 proxy 对象, effect 中使用 reactive 对象时会进行依赖收集,稍后属性变化时会重新执行 effect 函数~。
1.reactive 函数
@vue/shared
export function isObject(value: unknown): value is Record<any, any> {
return typeof value === "object" && value !== null;
}
import { isObject } from "@vue/shared";
export const enum ReactiveFlags {
IS_REACTIVE = "__v_isReactive",
}
export const mutableHandlers: ProxyHandler<object> = {
get(target, key, receiver) {
if (key === ReactiveFlags.IS_REACTIVE) {
// 访问时会触发
// 在get中增加标识,当获取IS_REACTIVE时返回true
return true;
}
// 等会谁来取值就做依赖收集 Reflect:处理this问题
const res = Reflect.get(target, key, receiver);
return res;
},
set(target, key, value, receiver) {
// 等会赋值的时候可以重新触发effect执行
const result = Reflect.set(target, key, value, receiver);
return result;
},
};
const reactiveMap = new WeakMap(); // 缓存列表 key:只能是对象
function createReactiveObject(target: object, isReadonly: boolean) {
// 不对非对象的类型来进行处理
if (!isObject(target)) {
return target;
}
if (target[ReactiveFlags.IS_REACTIVE]) {
// 在创建响应式对象时先进行取值,看是否已经是响应式对象
return target;
}
const exisitingProxy = reactiveMap.get(target); // 如果已经代理过则直接返回代理后的对象;
if (exisitingProxy) {
return exisitingProxy;
}
const proxy = new Proxy(target, mutableHandlers); // 对对象进行代理
reactiveMap.set(target, proxy); // 缓存
return proxy;
}
// 常用的就是reactive方法
export function reactive(target: object) {
return createReactiveObject(target, false);
}
/*
export function shallowReactive(target: object) {
return createReactiveObject(target, false)
}
export function readonly(target: object) {
return createReactiveObject(target, true)
}
export function shallowReadonly(target: object) {
return createReactiveObject(target, true)
}
*/
这里我们为了代码方便维护,我们将 mutableHandlers 抽离出去到 baseHandlers.ts 中。
这里必须要使用 Reflect 进行操作,保证 this 指向永远指向代理对象
- 举例
let person = {
name: "jw",
get aliasName() {
return "**" + this.name + "**";
},
};
let p = new Proxy(person, {
get(target, key, receiver) {
console.log(key);
// return Reflect.get(target,key,receiver)
return target[key];
},
set(target, key, value, receiver) {
return Reflect.set(target, key, value, receiver);
},
});
// 取aliasName时,我希望可以收集aliasName属性和name属性
console.log(p.aliasName);
// 这里的问题出自于 target[key] ,target指代的是原对象并不是代理对象
2.编写 effect 函数
依赖收集就是将当前的 effect 变成全局的,稍合取值时拿到这个全局的 effect
effect.ts
export let activeEffect = undefined; // 当前正在执行的effect
export class ReactiveEffect {
active = true; // 标记effect是否处于激活状态
deps = []; // 收集effect中使用到的属性
parent = undefined;
constructor(public fn) {
// 参数public fn => public fn; 参数 fn; this.fn = fn
}
run() {
if (!this.active) {
// 不是激活状态,就不需要考虑依赖收集,也就是不需要将这个effect放到全局变量上
return this.fn();
}
try {
this.parent = activeEffect; // 当前的effect就是他的父亲; 早期是用栈维护effect [e1,e2,e3]
activeEffect = this; // 设置成正在激活的是当前effect
return this.fn();
} finally {
// 无论什么情况都会执行
activeEffect = this.parent; // 执行完毕后还原activeEffect
this.parent = undefined;
}
}
}
export function effect(fn, options: any = {}) {
const _effect = new ReactiveEffect(fn); // 创建响应式effect
_effect.run(); // 响应式effect默认执行
}
3.依赖收集
默认执行 effect 时会对属性,进行依赖收集
baseHandlers.ts
get(target, key, receiver) {
if (key === ReactiveFlags.IS_REACTIVE) {
return true;
}
const res = Reflect.get(target, key, receiver);
track(target, key); // 依赖收集
return res;
}
effect.ts
const targetMap = new WeakMap(); // 记录依赖关系 targetMap = {target:{name:[effect1,effect2], age: [effect1,effect2]}}
export function track(target, key) {
if (!activeEffect) {
// 取值没有发生在 effect中
return false;
}
let depsMap = targetMap.get(target); // {对象:map} 将属性和对应的effect维护成映射关系,后续属性变化可以触发对应的effect函数重新run
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set())); // {对象:{ 属性 :[ dep,dep ]}}
}
let shouldTrack = !dep.has(activeEffect);
if (shouldTrack) {
dep.add(activeEffect);
activeEffect.deps.push(dep); // 让effect记住dep,清理使用
// 属性与effect多对多的关系;一个属于对应多个effect; 一个effect对应多个属性
}
}
将属性和对应的 effect 维护成映射关系,后续属性变化可以触发对应的 effect 函数重新 run
4.触发更新
baseHandlers.ts
set(target, key, value, receiver) {
// 等会赋值的时候可以重新触发effect执行
let oldValue = target[key]
const result = Reflect.set(target, key, value, receiver); // true
if (oldValue !== value) {
trigger(target, key, value, oldValue)
}
return result;
}
effect.ts
export function trigger(target, key?, newValue?, oldValue?) {
const depsMap = targetMap.get(target); // 获取对应的映射表
if (!depsMap) {
return;
}
const dep = depsMap.get(key);
if (dep) {
// 这里将要执行的effect拷贝一份
const effects = [...dep];
effects.forEach((effect) => {
if (effect !== activeEffect) effect.run(); // 防止在effect中修改数据造成死循环;
});
}
}
5.分支切换与 cleanup
在渲染时我们要避免副作用函数产生的遗留依赖问题 1.reactive.html script
const state = reactive({ flag: true, name: "jw", age: 30 });
effect(() => {
// 副作用函数 (effect执行渲染了页面)
console.log("render");
document.body.innerHTML = state.flag ? state.name : state.age;
});
setTimeout(() => {
state.flag = false;
setTimeout(() => {
console.log("修改name,原则上不更新");
state.name = "zf";
}, 1000);
}, 1000);
effect.ts
// 每次执行依赖收集前,先做清理操作
function cleanupEffect(effect) {
const { deps } = effect; // 清理effect
// 每次执行effect之前 我们应该清理掉effect中依赖的所有属性
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect); // 属性记录了effect {key:new set()}
}
effect.deps.length = 0;
}
class ReactiveEffect {
active = true;
deps = []; // 收集effect中使用到的属性
parent = undefined;
constructor(public fn) { }
run() {
try {
this.parent = activeEffect; // 当前的effect就是他的父亲
activeEffect = this; // 设置成正在激活的是当前effect
+ cleanupEffect(this);
return this.fn(); // 先清理在运行
}
}
}
这里要注意的是:触发时会进行清理操作(清理 effect),在重新进行收集(收集 effect)。在循环过程中会导致死循环。 demo.js
let effect = () => {};
let s = new Set([effect]);
s.forEach((item) => {
s.delete(effect);
s.add(effect);
}); // 这样就导致死循环了
- 属性变化时执行对应 dep 中所有的 effect (循环 set)
- 清理时找到对应的 dep 删除对应的 effect (set 中移除了 effect)
- 重新收集时 dep 再次收集了 effect (set 中添加了 effect)
6.停止 effect
effect.ts
export class ReactiveEffect {
stop() {
if (this.active) {
cleanupEffect(this); // 先将effect里面的依赖全部删除
this.active = false;
}
}
}
export function effect(fn, options: any = {}) {
const _effect = new ReactiveEffect(fn);
_effect.run();
const runner = _effect.run.bind(_effect);
runner.effect = _effect;
return runner; // 返回runner
}
7.调度执行
trigger 触发时,我们可以自己决定副作用函数执行的时机、次数、及执行方式 effect.ts
export class ReactiveEffect {
...
+ constructor(public fn, private scheduler) {} // 参数public fn => public fn; 参数 fn; this.fn = fn
...
}
export function effect(fn, options: any = {}) {
+ const _effect = new ReactiveEffect(fn, options.scheduler); // 创建响应式effect
_effect.run(); // 让响应式effect默认执行
const runner = _effect.run.bind(_effect);
runner.effect = _effect;
return runner; // 返回runner
}
export function trigger(target, key?, newValue?, oldValue?) {
const depsMap = targetMap.get(target); // 获取对应的映射表
if (!depsMap) {
return;
}
let dep = depsMap.get(key);
if (dep) {
// 这里将要执行的effect
const effects = [...dep];
effects.forEach((effect) => {
if (effect !== activeEffect) {
+ if (effect.scheduler) {
// 如果有调度函数则执行调度函数
+ effect.scheduler();
+ } else {
+ effect.run();
+ }
}
});
}
}
8.深度代理
baseHandlers.ts
get(target, key, receiver) {
if (key === ReactiveFlags.IS_REACTIVE) {
return true;
}
// 等会谁来取值就做依赖收集
const res = Reflect.get(target, key, receiver);
track(target, 'get', key);
+ if(isObject(res)){ // 当取值时返回的值是对象,则返回这个对象的代理对象,从而实现深度代理
+ return reactive(res);
+ }
return res;
}