Vue3 源码:reactive 依赖收集与触发流程
- 使用
weakMap(weakMap的key只能是对象) 去存储对象,每一次获取reactive中的值,会先去从weakMap中查找,如果没用再采用new Proxy将target给代理起来 createReactiveObject的第三个参数mutableHandlers就是用来处理被代理对象的取值,与设置值的
export const reactiveMap = new WeakMap();
export function reactive(target) {
return createReactiveObject(target, reactiveMap, mutableHandlers);
}
function createReactiveObject(target, proxyMap, baseHandlers) {
// 核心就是 proxy
// 目的是可以侦听到用户 get 或者 set 的动作
// 如果命中的话就直接返回就好了 使用缓存做的优化点
const existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy;
}
const proxy = new Proxy(target, baseHandlers);
// 把创建好的 proxy 给存起来,
proxyMap.set(target, proxy);
return proxy;
}
get 流程
- 在
get中上面会进入一些判断,这是为了给一些判断类型的api进行协助,如isReactive, 当我们在isReactive传入一个对象,如果对象是普通对象,那么它没用任何作用,而如果这个对象是一个proxy, 在获取这个对象上的某个属性,会触发proxy的get方法,这样我们就能在get中处理此逻辑,由于在 我们的reactive api中readonly也是经过proxy代理的,所以要判断是不是isReactive还要排除readonly ReactiveFlags.RAW是为了给toRaw返回原始对象使用shallow只需要对target做浅层次的代理,相当于只对target的第一级对象做了代理,而不去递归代理- 如果对象下的某个属性仍旧是对象,那么需要对此对象进行递归代理,也就是再去调用
createReactiveObject
export function isReactive(value) {
// 如果 value 是 proxy 的话
// 会触发 get 操作,而在 createGetter 里面会判断
// 如果 value 是普通对象的话
// 那么会返回 undefined ,那么就需要转换成布尔值
return !!value[ReactiveFlags.IS_REACTIVE];
}
// 为什么定义这个枚举,其实就是为了给某些方法使用,用来判断是否是 reactive readonly 等
// 原理就是相当于传入的 key 值为对应的枚举值,调用了 handler 的 get 方法
export const enum ReactiveFlags {
IS_REACTIVE = '__v_isReactive',
IS_READONLY = '__v_isReadonly',
RAW = '__v_raw',
}
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
const isExistInReactiveMap = () =>
key === ReactiveFlags.RAW && receiver === reactiveMap.get(target);
const isExistInReadonlyMap = () =>
key === ReactiveFlags.RAW && receiver === readonlyMap.get(target);
const isExistInShallowReadonlyMap = () =>
key === ReactiveFlags.RAW && receiver === shallowReadonlyMap.get(target);
// 判断当前对象是否为 reactive 代理对象,返回 true
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly;
}
// 判断当前对象是否为 readonly 代理对象,返回 true,走的是 const readonlyGet = createGetter(true);
else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly;
}
// 是否需要返回原始对象
else if (
isExistInReactiveMap() ||
isExistInReadonlyMap() ||
isExistInShallowReadonlyMap()
) {
return target;
}
let res = Reflect.get(target, key, receiver);
// 问题:为什么是 readonly 的时候不做依赖收集呢
// readonly 的话,是不可以被 set 的, 那不可以被 set 就意味着不会触发 trigger
// 所有就没有收集依赖的必要了
if (!isReadonly) {
// 在触发 get 的时候进行依赖收集
track(target, 'get', key);
}
if (shallow) {
return res;
}
if (isObject(res)) {
// 把内部所有的是 object 的值都用 reactive 包裹,变成响应式对象
// 如果说这个 res 值是一个对象的话,那么我们需要把获取到的 res 也转换成 reactive
// res 等于 target[key]
return isReadonly ? readonly(res) : reactive(res);
}
return res;
};
}
通常我们要使用的模版数据要进行响应式更新,在 proxy 数据发生变化的时候,模版的数据也同样要更新, 那么如何实现响应式变化呢,如下,effect 函数将 reactive 中的值渲染到了 html 中, 然后我们在定时器中修改了 reactive 中属性的值,这时候就需要再执行一遍 effect 函数,让模版获取到最新的值,这样才能实现响应式变化
// effect 代表的是副作用函数,如果此函数的依赖数据发生了变化,会重新执行
// reactive 将数据变成响应式 proxy
const { effect, reactive } = VueReactivity;
// 首先声明一个对象
const data = {
name: 'wfly',
age: '18',
address: '蚌埠市',
};
// 然后调用 reactive 方法把这个对象进行 proxy 代理
const state = reactive(data);
effect(() => {
// document.getElementById('app').innerHTML = state.name + '今年' + state.age + '岁' + '地址:' + state.address.num
// 执行外部的方法时使用了 state.name,这样会去执行 reactive-proxy 中的 get 方法去拿值
document.getElementById('app').innerHTML =
state.name + '今年' + state.age + '岁';
});
effect(() => {
// document.getElementById('app').innerHTML = state.name + '今年' + state.age + '岁' + '地址:' + state.address.num
// 执行外部的方法时使用了 state.name,这样会去执行 reactive-proxy 中的 get 方法去拿值
document.getElementById('app1').innerHTML =
state.name + '今年' + state.age + '岁';
});
setTimeout(() => {
console.log('执行了吗');
state.age = 14;
state.age = 15;
state.address = '合肥市';
}, 1000);
effect 函数: 可以看出 effect 就是一个函数,然后接收两个参数,第一个参数就是 fn,从上述代码可以看出 fn 主要就是存放 设置 html 内容的一个函数,然后在 effect 函数内部会生成一个 ReactiveEffect 实例
class ReactiveEffect:可以看出这个类中只有两个方法,一个是 run 一个是 stop,然后能够在 effect 函数中看出,只要是创建 effect 函数就会执行 ReactiveEffect 中的 run 方法,run 方法的核心就是执行 this.fn(),执行 this.fn() 后模版的数据自然就更新了。 那么为了实现响应式的数据变化,就需要再 reactive 数据变化后,再次执行 this.fn(),这是如何实现的呢,首先在 ReactiveEffect 中在执行 run 时我们将当前的 ReactiveEffect 赋值给了一个全局变量 activeEffect,这时我们就将当前的 ReactiveEffect 给存储了起来,这个存储的全局变量 activeEffect 会在后续依赖收集中用到
/** 当前执行的 effect 实例对象(ReactiveEffect) */
export let activeEffect = undefined;
let shouldTrack = false;
function cleanupEffect(effect) {
// 找到所有依赖这个 effect 的响应式对象
// 从这些响应式对象里面把 effect 给删除掉
effect.deps.forEach(dep => {
dep.delete(effect);
});
effect.deps.length = 0;
}
export class ReactiveEffect {
// 这个 effect 默认是激活状态,主要是用于用户调用 stop 方法时需要注意
active = true;
deps = [];
public onStop?: () => void;
constructor(
public fn,
public scheduler?,
) {
console.log('创建 ReactiveEffect 对象');
}
// run 就是执行 effect
run() {
// 运行 run 的时候,可以控制 要不要执行后续收集依赖的一步
// 目前来看的话,只要执行了 fn 那么就默认执行了收集依赖
// 这里就需要控制了
// 是不是收集依赖的变量
// 执行 fn 但是不收集依赖
if (!this.active) this.fn(); // 这里表示如果是非激活的情况,只需要执行函数,不需要进行依赖收集
// 执行 fn 收集依赖
// 可以开始收集依赖了
shouldTrack = true;
// 执行的时候给全局的 activeEffect 赋值
// 利用全局属性来获取当前的 effect
activeEffect = this as any;
// 执行用户传入的 fn
console.log('执行用户传入的 fn');
const result = this.fn();
// 重置
shouldTrack = false;
activeEffect = undefined;
return result;
}
stop() {
if (this.active) {
// 如果第一次执行 stop 后 active 就 false 了
// 这是为了防止重复的调用,执行 stop 逻辑
cleanupEffect(this);
if (this.onStop) {
this.onStop();
}
this.active = false;
}
}
}
export function effect(fn, options: any = {}) {
// 这里可以根据状态变化,重新执行,effect 可以嵌套写
/**
* 1. 创建响应式的 effect 并传入外部的 fn 方法
* 2. 当我们执行 effect 函数的时候,会对 ReactiveEffect 实例化一个对象,接着调用实例对象的 run 方法
*/
// 3. 通过 new 命令生成对象实例时,自动调用构造器的方法
const _effect = new ReactiveEffect(fn, options.scheduler);
// 5. 执行 run 方法
_effect.run(); // 默认先执行一次
// 绑定 this 为 _effect 执行
const runner: any = _effect.run.bind(_effect);
// 将 effect 挂载到 runner 函数上
runner.effect = _effect;
return runner;
}
export function stop(runner) {
runner.effect.stop();
}
要想模版实现相应式变化,那么我们就需要执行依赖收集
当 proxy 的 get 触发时,我们需要对触发的对象以及属性进行依赖收集,那么什么时候去触发 proxy 的 get 呢,就如上述的 effect 函数,当我们执行 effect 函数中的 run 方法就会触发,用户的 fn 函数,这里面的 fn 函数中有需要展示 reactive 中的数据, 这时相应的我们就需要触发 proxy 的 get,依赖收集就在此时触发
在依赖收集时,为了不重复收集依赖,我们用一个全局的 weakMap 对象 targetMap 去保存数据,targetMap 第一层数据结构 key 就是 target 对象,value 是一个 Map 对象,其 key 就是当前响应式对象获取的属性,然后这里的 value 就很关键,这里的 value 就是 上面 effect 函数中的 activeEffect 变量,这里面函数当前这个响应式对象对应属性的 fn 函数
/**
* 1. 声明一个响应式对象 new Proxy
* 2. effect 默认数据变化要能更新,我们先将正在执行的 effect 作为全局变量,渲染(取值),
* 我们在get方法中进行依赖收集
* 3. WeakMap = {对象:Map:{name: Set}}
* 4. 稍后用户发生数据变化,会通过对象属性来查找对应的 effect 集合,找到 effect 全局执行
*/
import { createDep } from './dep';
/**
* 一个 effect 对应多个属性,一个属性对应多个 effect
* 1. key: 响应性对象
* 2. value: Map 对象
* 2.1. key: 响应性对象的指定属性
* 2.2. value: Set 集合: 指定对象的指定属性的执行函数(为什么使用 Set 函数,一个 key 可能对应多个 fn 函数)
*/
const targetMap = new WeakMap();
export function isTracking() {
return shouldTrack && activeEffect !== undefined;
}
/**
* 依赖收集-收集在 effect 中使用的一些变量的信息,主要存储为
* WeakMap = {
* 源对象:
* Map:[
* {
* "key": "name",
* "value": Set[ReactiveEffect]
* },
* {
* "key": "age",
* "value" Set[ReactiveEffect]
* }
* ]
*/
export function track(target, type, key) {
// 对象的某个属性 对应 多个 effect
// WeakMap = {对象:Map:{name: Set}}
/**
* 如果没有在 effect 中使用 直接 return
* 如果是在 effect 中 activeEffect 在 class ReactiveEffect 中被赋值为当前的实例对象
*/
if (!isTracking()) {
return;
}
console.log('这里没执行');
/**
* 获取源对象是否在 targetMap 中
* [
* {
* "key": "name",
* "value": Set[ReactiveEffect]
* },
* {
* "key": "age",
* "value" Set[ReactiveEffect]
* }
* ]
*/
// 从 targetMap 寻找源对象是否存在
let depsMap = targetMap.get(target);
// 当在 targetMap 中未获取到 target 对应的值,设置 key 为 target,值为一个 map 对象
if (!depsMap) {
// 初始化 depsMap 的逻辑
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 获取当前的代理对象的属性(key) 在 depsMap 中是否存在,这个 dep 就是
let dep = depsMap.get(key);
if (!dep) {
dep = createDep();
// 当在 depsMap 中未获取到 key 对应的值,设置属性值 为 key,值为一个 Set 集合
depsMap.set(key, dep);
}
trackEffects(dep);
}
export function trackEffects(dep) {
// 用 dep 来存放所有的 effect
// TODO
// 这里是一个优化点
// 先看看这个依赖是不是已经收集了,
// 已经收集的话,那么就不需要在收集一次了
// 可能会影响 code path change 的情况
// 需要每次都 cleanupEffect
// shouldTrack = !dep.has(activeEffect!);
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
(activeEffect as any).deps.push(dep);
}
}
当依赖收集结束后就需要去触发依赖更新了,什么时候触发,也就是响应式数据对应的属性的 value 值进行改变时进而触发 proxy 的 set 方法, 在 set 中对比新旧值,如果不一样就执行 trigger 触发依赖更新
function createSetter() {
return function set(target, key, value, receiver) {
// 获取源对象之前 key 对应的值
let oldValue = target[key];
// 这一步是设置新值,然后再与下面的老值做比较
let result = Reflect.set(target, key, value, receiver);
// 判断 老的值 和 新的值 是否想等,不相等执行 trigger
if (oldValue !== value) {
// 触发依赖
trigger(target, 'set', key);
}
// 可以监控到用户设置值了
return result;
};
}
触发依赖更新的核心就是,拿到依赖收集的 targetMap,一般触发依赖更新的属性都是已经经过依赖收集的, 我们最终要拿到当前需要触发依赖更新的属性对应的 dep 这个 value,这个 value 之前存储了 activeEffect,并且如果有多个 activeEffect 就每一个都拿出来去执行里面的 run 方法, 因为响应式的数据可以对应多个 effect
export function trigger(target, type, key) {
// 1. 先收集所有的 dep 放到 deps 里面,
// 后面会统一处理
let deps: Array<any> = [];
// dep
// 得到的是 {key: 属性值,value: 属性值对应要执行的函数}
const depsMap = targetMap.get(target);
// 当前触发的对象未被 targetMap 进行收集
if (!depsMap) return;
// 如果 depsMap 存在,从 depsMap 拿到 key 对应的值,也就是要执行的函数
let dep = depsMap.get(key);
if (!dep) return;
// 最后收集到 deps 内
deps.push(dep);
const effects: Array<any> = [];
deps.forEach(dep => {
// 这里解构 dep 得到的是 dep 内部存储的 effect
effects.push(...dep);
});
// 这里的目的是只有一个 dep ,这个dep 里面包含所有的 effect
// 这里的目前应该是为了 triggerEffects 这个函数的复用
triggerEffects(createDep(effects));
}
export function triggerEffects(dep) {
console.log(dep, '====');
// 执行收集到的所有的 effect 的 run 方法
for (const effect of dep) {
if (effect.scheduler) {
// scheduler 可以让用户自己选择调用的时机
// 这样就可以灵活的控制调用了
// 在 runtime-core 中,就是使用了 scheduler 实现了在 next ticker 中调用的逻辑
effect.scheduler();
} else {
effect.run();
}
}
}

还没有留言,来发表第一条吧~