import { isObject } from "@vue/shared";
/*
* @Author: 毛毛
* @Date: 2022-06-25 13:50:54
* @Last Modified by: 毛毛
* @Last Modified time: 2022-06-25 14:15:28
*/
// 缓存已经代理过后的响应式对象
const reactiveMap = new WeakMap();
const enum ReactiveFlags {
IS_REACTIVE = "__v_isReactive",
}
/**
* 代理对象为响应式
* @param obj
*/
export function reactive(target: unknown) {
if (!isObject(target)) return;
const existingProxy = reactiveMap.get(target);
// 目标对象被代理过 返回同一个代理
if (existingProxy) return existingProxy;
// 第一个普通对象 创建代理
// 如果传入的对象 是已经被代理过的对象 我们可以看看这个对象是否有get方法,有表示已经是代理对象
if (target[ReactiveFlags.IS_REACTIVE]) {
// TODO 取到true 就返回自身 源码这一步很妙
return target;
}
// 创建代理对象
const proxy = new Proxy(target, {
get(target, key, receiver) {
// 用来判断是否是响应式对象
// 对象没有被代理之前,没有该key,如果代理对象被用来二次代理,会在上面取值,然后get走到这里,返回true了
if (key === ReactiveFlags.IS_REACTIVE) {
return true;
}
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const flag = Reflect.set(target, key, value, receiver);
return flag;
},
});
// 已经代理的对象进行缓存 如果再次代理同一个对象 返回同一个代理
reactiveMap.set(target, proxy);
return proxy;
}
当然这样写会有点乱,我们可以把对目标对象的代理操作提取出来:
baseHandler.ts:
/*
* @Author: 毛毛
* @Date: 2022-06-25 14:22:33
* @Last Modified by: 毛毛
* @Last Modified time: 2022-06-25 14:25:24
*/
export const enum ReactiveFlags {
IS_REACTIVE = "__v_isReactive",
}
export const mutableHandlers = {
get(target, key, receiver) {
// 用来判断是否是响应式对象
// 对象没有被代理之前,没有该key,如果代理对象被用来二次代理,会在上面取值,然后get走到这里,返回true了
if (key === ReactiveFlags.IS_REACTIVE) {
return true;
}
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const flag = Reflect.set(target, key, value, receiver);
return flag;
},
} as ProxyHandler这样看起来更清晰。
该API函数是来收集副作用的。参数是一个函数,当我们函数内使用的变量的值发生改变,会让这个副作用函数重新执行。
这里,我们需要定义一个ReactiveEffect类,也就是响应式的副作用函数,用来记录用户传入的副作用函数,并对其进行一些列扩展,毕竟我们不能随意修改用户传入的函数嘛。
/*
* @Author: 毛毛
* @Date: 2022-06-25 14:00:05
* @Last Modified by: 毛毛
* @Last Modified time: 2022-06-25 14:44:53
*/
/**
* 传入的副作用函数类型
*/
type effectFn = () => any;
export function effect(fn: effectFn) {
// 创建响应式的effect
const _effect = new ReactiveEffect(fn);
// 默认先执行一次副作用函数
const res = _effect.run();
return res;
}
/**
* 当前正在执行副作用函数暴露出去
*/
export let activeEffect: ReactiveEffect = null;
/**
* 把副作用函数包装为响应式的effect函数
*/
export class ReactiveEffect {
/**
* 这个effect默认是激活状态
*
* @memberof ReactiveEffect
*/
active = true;
constructor(public fn: effectFn) {}
/**
*
* run 方法 就是执行传入的副作用函数
* @memberof ReactiveEffect
*/
run() {
let res;
// 激活状态 才需要收集这个副作用函数fn内用到的响应式数据 也就是我们说的依赖收集
// 非激活状态 只执行函数 不收集依赖
if (!this.active) {
return this.fn();
}
try {
// 激活状态 依赖收集了 核心就是将当前的effect和稍后渲染的属性关联在一起
activeEffect = this;
// 执行传入的fn的时候,如果出现了响应式数据的获取操作,就可以获取到这个全局的activeEffect
res = this.fn();
} finally {
activeEffect = null;
}
return res;
}
/**
* 取消副作用函数的激活 不再收集依赖
*/
stop() {
this.active = false;
}
}
此时,虽然我们还没开始进行依赖收集,但是如果使用effect函数,是正常执行用户传入的函数逻辑的。 
effect(()=>{
console.log(obj.name)
effect(()=>{
console.log(obj)
})
// .... 来到这里 activeEffect 变成 null 了
})
所以我们可以修改记录当前正在执行的可响应式副作用对象的变量为一个栈结构。
let activeEffect:ReactiveEffect[] = []
每次执行effect的时候,都关联栈中的最后一个元素,执行完当前的副作用函数就弹出,这样内层effect执行完,栈中的最后一个元素还是指向当前正在执行的effect
在vue3.0版本的时候,的确就是采用栈结构来实现的。
但是在最新的3.2版本,又做了一些更改。因为栈结构也是比较消耗性能的。
最新的策略是采用了类似树结构的形式,每个ReactiveEffect对象,都记录自己父ReactiveEffect对象,让activeEffect变量依然指向自身,当自己执行完毕以后,将activeEffect的值指向自己的parent属性,也就是父节点,就实现了在嵌套执行effect的时候,不会弄丢activeEffect指向的问题。
可以这样做,主要还是依赖了js是单线程。
export class ReactiveEffect {
/**
* 记录父ReactiveEffect
*
* @type {ReactiveEffect}
* @memberof ReactiveEffect
*/
parent: ReactiveEffect = null;
// ...
run() {
// ...
try {
// 激活状态 依赖收集了 核心就是将当前的effect和稍后渲染的属性关联在一起
this.parent = activeEffect
activeEffect = this;
// 执行传入的fn的时候,如果出现了响应式数据的获取操作,就可以获取到这个全局的activeEffect
res = this.fn();
} finally {
// 执行完当前的effect 归还上次 activeEffect 变量指向的值
activeEffect = this.parent;
this.parent = null
}
return res;
}
}
实现了reactive和effect函数,接下来就是进行依赖收集。
在副作用函数中,我们如果进行了对响应式数据的取值操作,就会触发get,来到get钩子里,就可以进行对当前对象的当前属性收集正在执行的副作用函数。
也就是说:
当前对象 -> 取值get -> key -> effects
一个对象有多个属性,每个属性又可能在多个effect中使用,所以一个key对应多个effect,而且应该保证key对应的effect是不重复的(重复的副作用函数有必要吗?很明显没必要)
因此:我们得出可以使用map结构来记录对象和key的关系,用set来记录每个key和effects关系。
我们只需要这样做,就可以完成三者之间的映射关系。
但是,有时候我们在触发副作用函数的执行的时候,可以会出现需要清理副作用函数的情况