阿卡不拉阿卡不拉
Vue3
阿卡的博客
Vue3
阿卡的博客
  • Vue3

    • 快速入门

      • 搭建工程 👌
      • 模板语法
      • 响应式基础
      • 响应式常用 API
      • 计算属性
      • 类与样式绑定
      • 条件和列表渲染
      • 事件处理
      • 表单处理
      • 生命周期
      • 侦听器
      • 组件介绍
      • Props
      • 自定义事件
      • 组件v-model
      • 插槽
      • 前端路由介绍
      • KeepAlive内置组件
      • 状态管理库
      • 组件库介绍
    • 深入本质

      • 虚拟DOM本质
      • 模板的本质
      • 组件树和虚拟DOM树
      • 数据拦截的本质
      • 响应式数据的本质
      • 响应式的本质
      • 响应式和组件渲染
      • 实现响应式系统 1
      • 实现响应式系统 2
      • 图解EFFECT
      • 实现响应式系统 3
      • 手写computed
      • 手写watch
      • 指令的本质
      • 插槽的本质
      • v-model的本质
      • setup 语法标签
      • 组件生命周期
      • keepalive 生命周期
      • keepalive的本质
      • key的本质
    • 细节补充

      • 【Vue】属性透传
      • 【Vue】依赖注入
      • 【Vue】组合式函数 👌
      • 【Vue】自定义指令
      • 【Vue】插件
      • 【Vue】Transition
      • 【Vue】TransitionGroup
      • 【Vue】Teleport
      • 【Vue】异步组件
      • 【Vue】Suspense
      • 【Router】路由模式
      • 【Router】路由零碎知识
      • 【Router】路由匹配语法
      • 【Router】路由组件传参
      • 【Router】内置组件和函数
      • 【Router】导航守卫
      • 【Router】过渡特效
      • 【Router】滚动行为
      • 【Router】动态路由
      • 【State】通信方式总结
      • 【State】Pinia 自定义插件
      • 【场景】封装树形组件
      • 【场景】自定义 ref 实现防抖
      • 【场景】懒加载
      • 【场景】虚拟列表
      • 【场景】虚拟列表优化
      • 【第三方库】VueUse
      • 【第三方库】vuedragable
      • 【第三方库】vue-drag-resize
      • 【第三方库】vue-chartjs
      • 【第三方库】vuelidate
      • 【第三方库】vue3-lazyload
      • 【场景】Websocket 聊天室
      • 【Vite】✨ 认识 Vite👌
      • 【Vite】配置文件 👌
      • 【Vite】✨ 依赖预构建 👌
      • 【Vite】构建生产版本 👌
      • 【Vite】环境变量与模式
      • 【Vite】CLI
      • 【Vite】Vite 插件
  • 笔面试

    • HTML

      • HTML 面试题汇总
      • 文档声明
      • 语义化
      • W3C 标准组织
      • SEO
      • iframe
      • 微格式
      • 替换元素
      • 页面可见性
    • CSS

      • CSS 面试题汇总
      • CSS 单位总结
      • 居中方式总结
      • 隐藏元素方式总结
      • 浮动
      • 定位总结
      • BFC
      • CSS 属性计算过程
      • CSS 层叠继承规则总结
      • @import 指令
      • CSS3 calc 函数
      • CSS3 媒体查询
      • 过渡和动画事件
      • 渐进增强和优雅降级
      • CSS3 变形
      • 渐进式渲染
      • CSS 渲染性能优化
      • 层叠上下文
      • CSS3 遮罩
    • JavaScript

      • JavaScript 面试题汇总
      • ✨ let、var、const 的区别
      • JS中的数据类型
      • 包装类型
      • 数据类型的转换
      • 运算符
      • ✨ 原型链
      • ✨ this 指向
      • ✨ 垃圾回收与内存泄漏
      • ✨ 执行栈和执行上下文
      • ✨ 作用域和作用域链
      • ✨ 闭包
      • DOM 事件的注册和移除
      • DOM 事件的传播机制
      • 阻止事件默认行为
      • 递归
      • ✨ 属性描述符
      • class 和构造函数区别
      • 浮点数精度问题
      • 严格模式
      • ✨ 函数防抖和节流
      • ✨ WeakSet 和 WeakMap
      • ✨ 深浅拷贝
      • 函数柯里化
      • Node 事件循环
      • 尺寸和位置
    • 浏览器

      • 浏览器面试题汇总
      • ✨ 浏览器的渲染流程
      • ✨ 资源提示关键词
      • 浏览器的组成部分
      • IndexedDB
      • ✨ File API
      • ✨ 浏览器缓存
      • ✨ 浏览器跨标签页通信
      • Web Worker
    • 网络

      • 网络面试题汇总
      • 五层网络模型 👌
      • 常见请求方法 👌
      • ✨cookie👌
      • 面试题
      • 加密
      • ✨JWT👌
      • ✨ 同源策略及跨域问题 👌
      • 文件上传
      • ✨ 输入 url 地址之后
      • 文件下载
      • ✨ session
      • ✨ TCP
      • ✨ CSRF 攻击
      • ✨XSS 攻击 👌
      • ✨ 网络性能优化
      • 断点续传
      • 域名和 DNS
      • SSL、TLS、HTTPS 的关系
      • ✨ HTTP 各版本差异 👌
      • HTTP 缓存协议
      • ✨ WebSocket
    • 工程化

      • CMJ 和 ESM
      • npx
      • ESLint
    • Vue2

      • Vue 面试题汇总相关
      • 组件通信方式总结
      • 虚拟 DOM
      • v-model
      • 数据响应式原理
      • diff
      • 生命周期详解
      • computed
      • 过滤器
      • 作用域插槽
      • 过度和动画
      • 优化
      • keep-alive
      • 长列表优化
      • 其他 API
      • 模式和环境变量
      • 更多配置
      • 更多命令
      • 嵌套路由
      • 路由切换动画
    • Vue3

      • ✨ Vue3 整体变化 👌
      • ✨ Vue3 响应式变化 👌
      • ✨ nextTick 实现原理 👌
      • 两道代码题 👌
      • Vue 运行机制
      • 渲染器核心功能
      • 事件绑定与更新

实现响应式系统 3

依赖收集

实现 Effect

这里直接给出 Effect 实现:

/**
 * 用于记录当前活动的 effect
 */
export let activeEffect = undefined;
export const targetMap = new WeakMap(); // 用来存储对象和其属性的依赖关系
const effectStack = [];

/**
 * 该函数的作用,是执行传入的函数,并且在执行的过程中,收集依赖
 * @param {*} fn 要执行的函数
 */
export function effect(fn) {
    const environment = () => {
        try {
            activeEffect = environment;
            effectStack.push(environment);
            cleanup(environment);
            return fn();
        } finally {
            effectStack.pop();
            activeEffect = effectStack[effectStack.length - 1];
        }
    };
    environment.deps = [];
    environment();
}

export function cleanup(environment) {
    let deps = environment.deps; // 拿到当前环境函数的依赖(是个数组)
    if (deps.length) {
        deps.forEach((dep) => {
            dep.delete(environment);
        });
        deps.length = 0;
    }
}

改造 track

之前 track 仅仅只是简单的打印,那么现在就不能是简单打印了,而是进行具体的依赖收集。

注意依赖收集的时候,需要按照上面的设计一层一层进行查找。

// 收集器:用于在获取属性、是否存在某个属性、便利属性操作是进行依赖收集

import { targetMap, activeEffect } from "./effect.js";
import { TrackOpTypes, TERATE_KEY } from "../utils.js";
// 控制是否需要进行依赖收集的开关
let shouldTrack = true;

/**
 * 恢复依赖收集
 */
export function enableTracking() {
    shouldTrack = true;
}
/**
 * 暂停依赖收集
 */
export function disableTracking() {
    shouldTrack = false;
}

/**
 * 收集器用于收集依赖
 * @param {Object} target 原始对象
 * @param {String} type 操作类型:get/set...
 * @param {String} key 属性
 */
export default function (target, type, key) {
    // 先进行开关状态的判断
    if (!shouldTrack) {
        return;
    }
    // 依赖收集
    // 这里要做的事情很简单就是一层一层的去找,找到了就存储
    let propMap = targetMap.get(target);
    if (!propMap) {
        propMap = new Map();
        target.set(target, propMap);
    }

    // 如果是遍历操作,key值是undefined,所以需要进行参数归一化
    if (type === TrackOpTypes.ITERATE) {
        key = TERATE_KEY;
    }

    let typeMap = propMap.get(key);
    if (!typeMap) {
        typeMap = new Map();
        propMap.set(key, typeMap);
    }
    // 根据type值找对应的Set
    let depSet = typeMap.get(type);
    if (!depSet) {
        depSet = new Set();
        typeMap.set(type, depSet);
    }
    // 找到集合了,可以存储依赖了
    if (!depSet.has(activeEffect)) {
        depSet.add(activeEffect);
        activeEffect.deps.push(depSet); // 将集合存储到deps数组里面
    }
}

改造 trigger

trigger 要做的事情也很简单,就是从我们所设计的数据结构里面,一层一层去找,找到对应的依赖函数集合,然后全部执行一次。

首先我们需要建立一个设置行为和读取行为之间的映射关系:

// 定义修改数据和触发数据的映射关系
const triggerTypeMap = {
    [TriggerOpTypes.SET]: [TrackOpTypes.GET],
    [TriggerOpTypes.ADD]: [
        TrackOpTypes.GET,
        TrackOpTypes.ITERATE,
        TrackOpTypes.HAS,
    ],
    [TriggerOpTypes.DELETE]: [
        TrackOpTypes.GET,
        TrackOpTypes.ITERATE,
        TrackOpTypes.HAS,
    ],
};

我们前面在建立映射关系的时候,是根据具体的获取信息的行为来建立的映射关系,那么我们获取信息的行为有:

  • GET
  • HAS
  • ITERATE

这些都是在获取成员信息,而依赖函数就是和这些获取信息的行为进行映射的。

因此在进行设置操作的时候,需要思考一下当前的设置,会涉及到哪些获取成员的行为,然后才能找出该行为所对应的依赖函数。

// trigger.js
// 触发器:用于在设置属性、新增属性、删除属性操作时进行派发更新
import { TriggerOpTypes, TrackOpTypes, ITERATE_KEY } from "../utils.js";
import { targetMap, activeEffect } from "./effect.js";
// 定义修改数据和触发数据的映射关系
const triggerTypeMap = {
    [TriggerOpTypes.SET]: [TrackOpTypes.GET],
    [TriggerOpTypes.ADD]: [
        TrackOpTypes.GET,
        TrackOpTypes.ITERATE,
        TrackOpTypes.HAS,
    ],
    [TriggerOpTypes.DELETE]: [
        TrackOpTypes.GET,
        TrackOpTypes.ITERATE,
        TrackOpTypes.HAS,
    ],
};
/**
 * 触发器
 * @param {Object} target 原始对象
 * @param {String} type 操作的类型
 * @param {String} key 属性
 */
export default function (target, type, key) {
    // 要做的事情很简单,就是找到依赖然后执行依赖
    const effectFns = getEffectFns(target, type, key);
    if (!effectFns) return;
    for (let effectFn of effectFns) {
        if (effectFn !== activeEffect) {
            effectFn();
        }
    }
}

// 根据传入的信息,找到对应的依赖集合
function getEffectFns(target, type, key) {
    const propMap = targetMap.get(target);
    if (!propMap) return;

    // 如果是新增或者删除操作会额外触发迭代
    const keys = [key];
    if (type === TriggerOpTypes.ADD || type === TriggerOpTypes.DELETE) {
        keys.push(ITERATE_KEY);
    }

    const effectFns = new Set(); // 用于存储依赖的集合

    for (const key of keys) {
        const typeMap = propMap.get(key);
        if (!typeMap) continue;

        const trackTypes = triggerTypeMap[type];
        for (const trackType of trackTypes) {
            const dep = typeMap.get(trackType);
            if (!dep) continue;
            for (const effectFn of dep) {
                effectFns.add(effectFn);
            }
        }
    }
    return effectFns;
}
const obj = {
    a: 1,
    b: 2,
};

const state = reactive(obj);
// 测试1
function fn() {
    console.log("fn");
    state.a = state.a + 1;
}

effect(fn);
state.a = 100;

// 测试2
effect(() => {
    if (state.a === 1) {
        state.b;
    } else {
        state.c;
    }
    console.log("执行了函数1");
});
effect(() => {
    console.log(state.c);
    console.log("执行了函数2");
});
state.a = 2;
state.c = 2;

懒执行

有些时候我们想要实现懒执行,也就是不想要传入 effect 的回调函数自动就执行一次,通过配置项来实现

export function effect(fn, options = {}) {
    const { lazy = false } = options;
    const environment = () => {
        try {
            activeEffect = environment;
            effectStack.push(environment);
            cleanup(environment);
            return fn();
        } finally {
            effectStack.pop();
            activeEffect = effectStack[effectStack.length - 1];
        }
    };
    environment.deps = [];
    if (!lazy) {
        environment();
    }
    return environment;
}

添加回调

有些时候需要由用户来指定是否派发更新,支持用户传入一个回调函数,然后将要依赖的函数作为参数传递回给用户给的回调函数,由用户来决定如何处理。

// effect.js
export function effect(fn, options = {}) {
    const { lazy = false } = options;
    const environment = () => {
        try {
            activeEffect = environment;
            effectStack.push(environment);
            cleanup(environment);
            return fn();
        } finally {
            effectStack.pop();
            activeEffect = effectStack[effectStack.length - 1];
        }
    };
    environment.deps = [];
    environment.options = options; // 传递回调函数
    if (!lazy) {
        environment();
    }
    return environment;
}
// trigger.js
export default function (target, type, key) {
    // 要做的事情很简单,就是找到依赖然后执行依赖
    const effectFns = getEffectFns(target, type, key);
    if (!effectFns) return;
    for (const effectFn of effectFns) {
        if (effectFn === activeEffect) continue;
        if (effectFn.options && effectFn.options.scheduler) {
            // 说明用户传递了回调函数,用户期望自己来处理依赖的函数
            effectFn.options.scheduler(effectFn);
        } else {
            // 否则就直接执行依赖函数
            effectFn();
        }
    }
}
最近更新:: 2025/7/9 07:29
Contributors: AK
Prev
图解EFFECT
Next
手写computed