实现effect函数和收集依赖的功能

125 阅读6分钟

前言

(⚠️长篇代码警告⚠️)

密码的,好难😇,看了好几遍视频,还让豆包和kimi给我解释代码,果然是我太菜了吗...

这节讲的是如何实现vue框架中使用的effect,实现将响应式数据和依赖双向关联,创建响应式的副作用函数,在响应式数据变化时及时重新调用副作用函数。

effect似乎在平时开发中不常用,只有在构建框架时可能用到,所以并没有完全吃透,尚且有些囫囵吞枣,大概了解了一下实现的思路...🤫

感觉整体上比较巧妙的点在于数据结构的组织以及在reactiveEffect类型中添加parent字段。

  • weakMap={对象:Map:{属性:set}},这部分很值得思考:首先weakMap支持对象作为键值,并且有更好的垃圾处理机制;其次,使用Map作为对象的映射,一个对象正好对应多个属性key,也符合它采用string类型作为键值的规定;最后,使用set来保存某个键值key对应的多个副作用函数,而且使用set最讨巧的点在于它可以自动去重。(虽然在代码逻辑里已经手动去重😗)

  • 听闻vue之前版本的代码不是通过为reactiveEffect添加parent字段来实现嵌套effect的,而是通过数组??然后改成了现在更加简单省内存的版本,使用新字段parent来记录父级effect,这部分其实也挺精妙,一个简单的改动却很大的提升了性能。👍

代码

effect.ts

export let activeEffect = undefined; // 存储当前的effect

class ReactiveEffect {

    public deps = [];

    // 反向收集effect关联了哪些属性

    public parent = null;

    public active = true; //实例上新增active属性 默认是激活的

    constructor(public fn) {} // ts的语法,用户传递的参数也会挂到this上

    run() {

        // 此处表示非激活的情况,只执行函数,不进行依赖收集

        if (!this.active) {

            this.fn();

        }
        // 此处进行依赖收集,核心是将effect和当前的渲染关联

        try {

            this.parent = activeEffect;

            activeEffect = this;

            return this.fn();

            // 稍后调用取值操作时,可以获取到全局的activeEffect

        } finally {

            // 即使发生了错误,也重置activeEffect

            this.parent = null;

            activeEffect = this.parent;

        }

    }

}

export function effect(fn) {

    // 这里fn可以根据状态变化,重新执行,effect可以嵌套写

    const _effect = new ReactiveEffect(fn); // 创建响应式的effect

    _effect.run(); // 默认先执行一次

}

// 一个effect对应多个属性,一个属性对应多个effect

// 多对多的关系,需要一个中间结构,来存储这种对应关系

// weakMap = {object:map{name:'effect'}}

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);

    // 如果dep中有没有activeEffect,那么就需要收集了,实现去重

    if (shouldTrack) {

        dep.add(activeEffect);

        activeEffect.deps.push(dep); // 让effect记录dep,稍后清理的时候会用到

    }

    // 对象的某个属性-> 多个effect

    // weakMap={对象:map:{属性:set}},set实现去重

}

export function trigger(target, type, key, value, oldValue) {

    const depsMap = targetMap.get(target);

    if (!depsMap) return;

    // 触发的值不在模板中使用,不需要触发

    const effects = depsMap.get(key);

    // effects是一个set类型数据,包含所有与当前属性key相关联的effect

    // 找到对应的effect

    effects && effects.forEach((effect) => {

        if (effect !== activeEffect) {

            // 在执行effect的时候,又要执行自身,那就需要屏蔽掉,不要无限调用

            effect.run();

        }

    });

}

baseHandler.ts

import { isObject } from '@vue/shared';

import { reactive } from './reactive';

import { activeEffect, track, trigger } from './effect';

export const enum ReactiveFlags { //响应式标识

    // 实现同一个对象代理多次,返回同一个代理对象

    // 代理对象的代理对象还是之前的代理对象

    IS_REACTIVE = '__v_isReactive',

}

export const mutableHandlers = {

    get(target, key, receiver) {

        if (key === ReactiveFlags.IS_REACTIVE) {

            return true;

        }

        track(target, 'get', key);

        // 去代理对象上取值 使用get

        // return target[key]; 这种方式this指向有问题

        console.log(key);

        // 可以监控到用户取值

        const value = Reflect.get(target, key, receiver);

        // 如果获取的值是对象,递归调用 reactive 函数将其转换为响应式对象

        if (isObject(value)) {

            return reactive(value);

        }

        return value;

        // Proxy要配合Reflect使用,保证this指向正确

        // Reflect的recerver参数使this指向代理对象

    },

    set(target, key, value, receiver) {

        // 去代理上设置值 使用set

        let oldValue = target[key];

        let result = Reflect.set(target, key, value, receiver);

        if (oldValue !== value) {

            // 触发effect,更新

            trigger(target, 'set', key, value, oldValue);

        }

        // 可以监控到用户设置值

        return Reflect.set(target, key, value, receiver);

    }

};

reactive.ts

import { isObject } from '@vue/shared';

import { mutableHandlers, ReactiveFlags } from './baseHandler';

// 1.将数据转换成响应式数据,只能做对象类型数据的代理

const reactiveMap = new WeakMap(); //key只能是对象类型

export function reactive(target) {

    if (!isObject(target)) {

        return target;

    }

    if (target[ReactiveFlags.IS_REACTIVE]) {

        //如果target是代理对象,那么它一定会在这一步中进入get

        return target; // 判定为代理对象,直接返回,对应多重代理问题

    }

    // 并没有重新定义属性,只是代理,在取值的时候会调用get,赋值时会调用set

    let existingProxy = reactiveMap.get(target);

    if (existingProxy) {

        return existingProxy;

    }

    // 第一次普通对象代理,会通过new Proxy代理一次

    // 下一次传入proxy对象,为了检测是否代理过,可以查看是否有get方法,有的话说明被proxy代理

    const proxy = new Proxy(target, mutableHandlers);

    reactiveMap.set(target, proxy);

    return proxy;

}

代码解释

Vue响应式系统中, effect 是一个核心概念,它的主要作用是创建一个响应式的副作用函数,当响应式数据发生变化时,这个副作用函数会自动重新执行。

effect 函数用于创建一个响应式的副作用函数。当副作用函数中访问了响应式对象的属性时,Vue 的响应式系统会自动跟踪这些依赖关系。一旦这些响应式属性的值发生变化,副作用函数会被重新执行,从而实现数据的自动更新。

effect函数实现
  • activeEffect :它是一个全局变量,用于存储当前正在执行的 effect 。在依赖收集过程中,通过这个变量可以知道当前是哪个 effect 在访问响应式数据。
  • ReactiveEffect 类 :用于封装副作用函数 fn ,并提供 run 方法来执行副作用函数。在 run 方法中,会将当前的 effect 赋值给 activeEffect ,以便在访问响应式数据时进行依赖收集。
  • effect 函数 :创建一个 ReactiveEffect 实例,并调用其 run 方法,默认先执行一次副作用函数。
依赖收集
  • targetMap :它是一个 WeakMap ,用于存储对象和其对应的依赖关系。每个对象对应一个 Map ,这个 Map 存储了对象的属性和对应的 effect 集合。
  • track 函数 :当访问响应式对象的属性时,会调用 track 函数进行依赖收集。它会检查当前是否有 activeEffect ,如果有,则将该 effect 添加到对应的属性的依赖集合中。
触发更新
  • trigger 函数 :当修改响应式对象的属性时,会调用 trigger 函数触发更新。它会从 targetMap 中找到对应的依赖集合,并遍历这些 effect ,调用它们的 run 方法重新执行副作用函数。
响应式对象的代理
  • reactive 函数 :用于创建响应式对象的代理。它使用 Proxy 对象对目标对象进行代理,并使用 mutableHandlers 作为代理的处理程序。
  • mutableHandlers :定义了 getset 拦截器。在 get 拦截器中,会调用 track 函数进行依赖收集;在 set 拦截器中,会调用 trigger 函数触发更新。