一.reactive与effect功能
reactive
方法会将对象变成proxy
对象, effect
中使用reactive
对象时会进行依赖收集,稍后属性变化时会重新执行effec
t函数。
<div id="app"></div>
<script type="module">
import {
reactive,
effect,
} from "/node_modules/@vue/reactivity/dist/reactivity.esm-browser.js";
// reactive创建一个响应式对象
// effect 副作用函数,默认执行一次,数据变化后再次执行
const state = reactive({ name: "orange", age: 18 });
effect(() => {
document.getElementById("app").innerHTML = state.name;
});
console.log(state);
setTimeout(() => {
state.name = "apple";
}, 2000);
</script>
二.reactive与effect实现
1.实现reactive
1.1 get、set基础实现
// reactive.ts
import { isObject } from "@vue/shared";
const mutableHandlers = {
get(target, key, receiver) {
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
return Reflect.set(target, key, value, receiver);
},
};
export function reactive(target) {
if (!isObject(target)) {
return target;
}
const proxy = new Proxy(target, mutableHandlers);
return proxy;
}
如下图,如果采用target[key]
方法,获取aliasName
时 不会触发name
的get
1.2 完善重复
const state = { name: "orange", age: 18 };
const p1 = reactive(state);
const p2 = reactive(state);
const p3 = reactive(p1);
console.log(p1 === p2);
console.log(p1 === p3);
- 响应式数据缓存,防止重复代理
使用map
,key
为对象,值为响应式对象,实现p1 === p2
// 2.1用于记录缓存代理 解决针对同一对象创建两次不相同问题
const reactiveMap = new WeakMap(); //内存泄漏
export function reactive(target) {
if (!isObject(target)) {
return target;
}
// 2.3如果在map中发现了相同的对象,则直接返回
const exitProxy = reactiveMap.get(target);
if (exitProxy) {
return exitProxy;
}
const proxy = new Proxy(target, mutableHandlers);
// 2.2创建过一次后,就放到map里面,属性名用对象实体
reactiveMap.set(target, proxy);
return proxy;
}
- 响应式数据标记
用枚举做标记,响应式对象都有get和set方法,p1
初次创建时state
没有get
和set
方法,target[ReactiveFlags.IS_REACTIVE]
取值为false
,创建p3
时,p1
响应式,取值为true
,直接返回p1
// 3.1 给对象增加标识,是否为代理对象
export const enum ReactiveFlags {
IS_REACTIVE = "__v_isReactive",
}
get(target, key, receiver) {
// 4.2 如果访问的是ReactiveFlags.IS_REACTIVE 就返回true,代表这个对象是被代理过的
if (ReactiveFlags.IS_REACTIVE == key) {
return true;
}
return Reflect.get(target, key, receiver);
},
export function reactive(target) {
...,
// 3.2 如果传递过来的对象已经是一个被代理过的对象,进行处理 直接返回这个对象
// 4.1首先访问这个属性,如果是被代理过的对象,就会触发劫持的get方法
if (target[ReactiveFlags.IS_REACTIVE]) {
return target;
}
const proxy = new Proxy(target, mutableHandlers);
reactiveMap.set(target, proxy);
return proxy;
}
2.实现effect
2.1 effect函数
vue2
与vue3
早期的依赖收集采用的都是栈方式存储,vue3
后来改为树型数据存储。
effect
执行时,把当前effect
作为全局的,触发属性的get
方法,收集依赖
let activeEffect;
class ReactiveEffect {
_trackId = 0; //用于记录当前effect执行了几次
public deps: Array<any> = []; // 判断依赖属性
public _depsLength = 0;
public active: boolean = true; // 是否是响应式的
public parent = undefined;
constructor(public fn, private scheduler) {}
run() {
if (!this.active) {
return this.fn();
}
try {
this.parent = activeEffect;
activeEffect = this;
preCleanEffect(this); // 清理只是更新长度。没有实质性的删除内容
return this.fn();
} finally {
postCleanEffect(this); // 对比后删除多余的内容
activeEffect = this.parent;
this.parent = undefined;
}
}
}
export function effect(fn) {
const _effect = new ReactiveEffect(fn);
_effect.run();
}
2.2 依赖收集
执行effec
t时,会触发依赖属性的get
方法,在属性的get
中进行依赖收集
get(target, key, receiver) {
if (ReactiveFlags.IS_REACTIVE == key) {
return true;
}
console.log(activeEffect, key);
track(target,key)
return Reflect.get(target, key, receiver);
},
/* 属性变动后 要重新执行run 需要把属性和effect关联起来 收集对象上属性关联的effect
不能光记录属性,容易两个对象有重名的属性,所以需要带着对象记录
target为对象
let mapping = {
target:{
name:[effect1,effect2,effect3] 一个属性可以在多个页面使用
}
}
*/
const targetMap = new WeakMap();
export function createDep(cleanup, key) {
let dep = new Map() as any;
dep.cleanup = cleanup;
dep.name = key;
return dep;
}
export function track(target, key) {
// 如果取值操作没有发生在effect中,则不需要收集依赖
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 = createDep(() => depsMap.delete(key), key)));
}
trackEffect(activeEffect, dep);
}
// 双向记忆
export function trackEffect(effect, dep) {
// 重新收集依赖 将不需要的移除
// _trackId:用于记录执行次数,防止一个属性在当前effect中多次收集
// 进行比较,flag name ==》flag age 把name删掉
if (dep.get(effect) !== effect._trackId) {
dep.set(effect, effect._trackId);
let oldDep = effect.deps[effect._depsLength];
if (oldDep !== dep) {
if (oldDep) {
cleanDepEffect(oldDep, effect);
}
effect.deps[effect._depsLength++] = dep;
} else {
effect._depsLength++;
}
}
2.3 属性更改 触发effect
set(target, key, value, receiver) {
// 数据变化后触发对应的effect方法
const oldValue = target[key];
let r = Reflect.set(target, key, value, receiver);
if (oldValue != value) {
trigger(target, key, value, oldValue);
}
return r;
},
export function trigger(target, key, newValue, oldValue) {
// 通过对象找到对应的属性 让这个属性对应的effect重新执行
const depsMap = targetMap.get(target);
if (!depsMap) {
return;
}
const dep = depsMap.get(key); // name 或者 age对应的所有effect
if (dep) {
triggerEffects(dep);
}
}
export function triggerEffects(dep) {
for (const effect of dep.keys()) {
if (!effect.scheduler) {
effect.run();
} else {
effect.scheduler();
}
}
}
2.4 清除effect依赖
const state = reactive({ flag: true, name: "orange", age: 18 });
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);
在每次收集依赖前先清除,但是此处的清除为初始化_depsLength
长度,但是没有真正清除deps
中的内容,用于在收集过程中进行比较,实现复用,复用收集结束后,_depsLength
为本次真正所依赖的属性长度,如果deps
的长度更长,则删除后续的属性依赖。
function preCleanEffect(effect) {
effect._depsLength = 0;
effect._trackId++; // 同一个effect执行 id相同
}
function postCleanEffect(effect) {
if (effect.deps.length > effect._depsLength) {
for (let i = effect._depsLength; i < effect.deps.length; i++) {
cleanDepEffect(effect.deps[i], effect); // 删除映射表中对应的effect
}
effect.deps.length = effect._depsLength; // 更新列表的长度
}
}
function cleanDepEffect(dep, effect) {
dep.delete(effect);
if (dep.size == 0) {
dep.cleanup(); // 如果map为空则删除这个属性
}
}
2.5 effect失活
通过effect
返回当前effect
实例,然后curEffect.effect.stop()
调用stop
方法
let runner = effect(() => {
// 副作用函数 (effect执行渲染了页面)
console.log("render");
document.body.innerHTML = state.flag ? state.name : state.age;
});
runner.effect.stop()
stop() {
cleanupEffect(this);
this.active = false;
}
export function effect(fn) {
const _effect = new ReactiveEffect(fn);
_effect.run();
const runner = _effect.run.bind(_effect); //调用effect.effect.stop() 停止effect
runner.effect = _effect;
return runner;
}
失活后我们可以手动调用 runner()
触发effect
方法
2.6 调度执行
trigger触发时,我们可以自己决定副作用函数执行的时机、次数、及执行方式
export function effect(fn, options:any = {}) {
const _effect = new ReactiveEffect(fn,options.scheduler); // 创建响应式effect
// if(options){
// Object.assign(_effect,options); // 扩展属性
// }
_effect.run(); // 让响应式effect默认执行
const runner = _effect.run.bind(_effect);
runner.effect = _effect;
return runner; // 返回runner
}
let runner = effect(() => {
app.innerHTML = state.flag ? state.name : state.age;
},{
scheduler:()=>{
console.log('不执行更新'); // 实现了AOP切片编程
runner()
}
});
2.7 深度代理
只有当数据被获取的时候才进行代理,如果获取到的数据还是对象,继续代理
get(target, key, receiver) {
if (ReactiveFlags.IS_REACTIVE == key) {
return true;
}
track(target, key);
let r = Reflect.get(target, key, receiver);
if (isObject(r)) {
return reactive(r);
}
return r;
},
2.8 防止重复代理
判断当前是否正在执行effect
,如果effect
中会再次修改属性,触发effect
,则不做改变
export class ReactiveEffect {
...
public _running = 0; // 是否正在执行
constructor(public fn, private scheduler) {}
run() {
...
try {
...
this._running++;
return this.fn();
} finally {
this._running--;
...
}
}
}
export function triggerEffects(dep) {
for (const effect of dep.keys()) {
if (!effect._running) {
if (!effect.scheduler) {
effect.run();
} else {
effect.scheduler();
}
}
}
}