Vue.js设计与实现——通过Proxy实现简单的响应式系统

文章内容来源:《Vue.js设计与实现》 —— 当当网 ,作者:霍春阳(HcySunYang)

一、通过 Proxy 实现基本的响应式数据:

function Section1 () {
    // 存储副作用函数的桶
    const bucket = new Set();
    
    // 原始数据
    const data = {text: "hello world"}
    // 对原始数据的代理
    let obj = new Proxy(data, {
        // 拦截读取操作
        get (target, key) {
            // 将副作用函数 effect 添加到存储副作用的桶中
            bucket.add(effect);
            // 返回属性值
            return target[key];
        },
        set (target, key, newVal) {
            // 设置属性值
            target[key] = newVal;
            // 将副作用函数从桶里取出并执行
            bucket.forEach(fn => fn());
            // 返回 true 代表设置操作成功
            return true;
        }
    });

    // 副作用函数
    function effect () {
        document.body.innerText = obj.text;
    }
    effect ();

    setTimeout(() => {
        obj.text = 'hello vue3'
    }, 1000);
    //  旧  ↑↑↑↑
}
Section1();

二、设计一个较完善的响应式系统

/**
 * 解决 一、中 effect 副作用函数是硬编码命名函数的情况
 * 提供一个注册副作用函数的机制
 */
function Section2 () {
    // 存储副作用函数的桶
    const bucket = new Set();
        
    // 原始数据
    const data = {text: "hello world"}
    
    // 用一个全局变量存储被注册的的副作用函数
    let activeEffect;
    // effect 函数用于注册副作用函数
    function effect(fn) {
        // 当调用effect注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
        activeEffect = fn;
        // 执行副作用函数
        fn();
    }

    /**
     * 重写 obj 的 Proxy
     */
    const obj = new Proxy(data, {
        get (target, key) {
            // 如果 activeEffect 存在(副作用函数赋值给了activeEffect),就将 activeEffect 存储的副作用函数收集到“桶”里
            if (activeEffect) { // 比旧obj的proxy 新增
                bucket.add(activeEffect); // 比旧obj的proxy 新增
            } // 比旧obj的proxy 新增
            // 返回属性值
            return target[key];
        },
        set (target, key, newVal) {
            target[key] = newVal;
            bucket.forEach(fn => fn());
            return true;
        }
    });

    /**
     * 如何使用 effect 函数?
     */
    // 1、匿名函数
    effect (
        // 一个匿名的副作用函数
        () => {
            document.body.innerText = obj.text;
        }
    )
    
    // 2、具名函数
    function setBodyInnerText () {
        document.body.innerText = obj.text;
    }
    effect(setBodyInnerText);
    
    setTimeout(() => {
        obj.text = 'hello Section2'
    }, 2000);
    
    // 以上 ↑↑↑↑ 将 副作用函数存储到activeEffect中,在把activeEffect收集到 “桶”里,响应系统不再依赖副作用函数的名字了

    /**
     * 由于上面没有在副作用函数和被操作的目标字段之间建立明确的联系,
     * 如果调用 obj 不存在的属性例如(obj.notExit),与obj.text相关的副作用也会执行,这是不正确的。
     */
    
    effect(
        // 匿名函数
        () => {
            console.log('effect run'); // 会打印两次
            document.body.innerText = obj.text;
        }
    );
    setTimeout(() => {
        // 副作用函数中并没有读取 notExit 属性的值 ↑↑↑
        obj.notExit = 'hello Vue3'
    }, 1000);
}
// Section2();

function Section3 () {

    // 原始数据
    const data = {text: "hello world"}

    /**
     * 解决 副作用函数 和 被操作的目标字段 之间建立明确联系的问题。
     * target:表示一个代理对象(Proxy)所代理的原始对象
     * key:表示被操作的字段名
     * effectFn:表示被注册的副作用函数
     * 以上3个角色的关系如下:
     * target
     *  |__ key
     *      |__ effectFn
     * 
     * 2、如果有2个副作用函数同时读取同一个对象的属性值:
     * effect(function effectFn () { obj.text; });
     * effect(function effectFn2 () { obj.text; });
     * 那么关系如下:
     * target
     *  |__ text
     *      |__ effectFn
     *      |__ effectFn2
     * 
     * 3、如果一个副作用函数中读取了同一个对象的2个不同属性:
     * effect (function effectFn () {
     *      obj.text;
     *      obj.text2;
     * });
     * 那么关系如下:
     * target
     *  |__ text
     *      |__ effectFn
     *  |__ text2
     *      |__ effectFn
     * 
     * 4、如果在不同副作用函数中,读取了2个不同对象的不同属性:
     * effect (function effectFn () {
     *      obj1.text1;
     * });
     * effect (function effectFn2 () {
     *      obj2.text2;
     * });
     * 那么关系如下:
     * target
     *  |__ text
     *      |__ effectFn
     * 
     * target2
     *  |__ text2
     *      |__ effectFn2
     * 
     * target 对应 n 个 key, 每个 key 又对应了 n 个 effectFn,
     * 为了保证一一对应关系,需要用 WeakMap、Map 和 Set 将 target、key、effectFn 关联起来
     */
    
    // 实现新的“桶”,首先使用 WeakMap 替代 Set 作为桶的数据结构:
    // 存储副作用函数的桶
    const bucket = new WeakMap();

    // 用一个全局变量存储被注册的的副作用函数
    let activeEffect;
    // effect 函数用于注册副作用函数
    function effect(fn) {
        // 当调用effect注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
        activeEffect = fn;
        // 执行副作用函数
        fn();
    }
    
    // 修改 get / set 拦截器代码:
    const obj = new Proxy(data, {
        // 拦截读取操作
        get (target, key) {
            // 没有 activeEffect ,直接 return 属性值
            if (!activeEffect) return target[key];
            
            // 1、根据 target 从“桶”中取得 depsMap, 是 target 对应的 Map,Map 包含了 target 各个 key,以及不同 key 对应的不同 effectFn
            let depsMap = bucket.get(target);
            // 如果 depsMap 不存在,就新建一个 Map 与 target 关联
            if (!depsMap) {
                // 这里的Map,后续用来存储 target 的 key
                bucket.set(target, (depsMap = new Map()));            
            }
    
            // 2、再根据 key 从 depsMap 中取得 deps, deps是一个 Set 类型,里面存储着所有与当前 key 相关联的副作用函数:effects
            let deps = depsMap.get(key);
            // 如果 deps 不存在,就新建一个 Set 与 key 关联
            if (!deps) {
                // 这里的 Set,后续用于存储与 key 关联的副作用函数 effects
                depsMap.set(key, (deps = new Set()));
            }
    
            // 3、最后将当前激活的副作用函数添加到“桶”里
            deps.add(activeEffect);
    
            // 返回属性值
            return target[key];
        },
        // 拦截设置操作
        set (target, key, newVal) {
            // 设置属性值
            target[key] = newVal;
            // 根据 target 从桶中取出 depsMap,depsMap如果存在,就是Map数据,是 key ---->  effects 对应关系
            let depsMap = bucket.get(target);
            // 如果 depsMap 不存在,就不执行后续操作
            if (!depsMap) return
            // 根据 key 从 depsMap 取出 effects , effects 是 Set 数据,存储的是所有与 key 关联的副作用函数
            let effects = depsMap.get(key);
            // 如果存在关联副作用函数,执行每一个副作用函数
            effects && effects.forEach(fn => fn());
        }
    });
    effect(
        // 匿名函数
        () => {
            console.log('effect run'); // 只打印1次,已确立 effect 和 key 的明确联系
            document.body.innerText = obj.text;
        }
    );
    setTimeout(() => {
        // 副作用函数中并没有读取 notExit 属性的值 ↑↑↑
        obj.notExit = 'hello Section3'
    }, 1000);
}
// Section3();

function Section4 () {
    /**
     * 为何 target 和 key 的关系要使用 WeakMap ?
     * 1、WeakMap 是弱引用,Map 是强引用。
     * 2、WeakMap 的 key 是弱引用,在 WeakMap 的表达式执行完之后,垃圾回收器(grabage collector)就会将对象 target 从内存中移除。
     * 3、Map 的 key 是强引用,在 Map 的表达式执行完之后,对于对象 target 来说,它仍然作为 Map 的 key 被引用着,因此垃圾回收器不会把它从内存中移除。
     * 举例:
     */
    // 创建 Map 数据
    const mapData = new Map();
    // 创建 WeakMap 数据
    const weakmapData = new WeakMap();
    
    // IIFE 立即执行函数,执行表达式
    (function() {
        const key1 = { foo: 1 };
        const key2 = { bar: 2 };
    
        // Map 数据执行表达式,在此处引用 key1 对象作为 mapData 的 key
        mapData.set(key1, 1);
        // WeakMap 数据执行表达式,在此处引用 key2 对象作为 weakmapData 的 key
        weakmapData.set(key2, 2);
    
        // 此处已执行完表达式
        console.log('mapData.keys :>> ', mapData.keys); 
        // 打印:mapData.keys :>>  ƒ keys() { [native code] }
        for (const k of mapData.keys()) {
            console.log('k :>> ', k); 
            // 打印:k :>>  {foo: 1}
        };
        console.log('weakmapData.keys :>> ', weakmapData.keys); 
        // 打印:weakmapData.keys :>>  undefined
        console.log('weakmapData :>> ', weakmapData);
        // 打印:weakmapData :>>  WeakMap {{…} => 2}
        console.log('weakmapData.get(key2) :>> ', weakmapData.get(key2));
        // 打印:weakmapData.get(key2) :>>  2
        /**
         * 执行完 mapData.set() 和 weakmapData.set() 后,
         * 1、Map数据:对于 key1 对象,它仍然作为 mapData 的 key 被引用着,不会被垃圾回收器从内存中移除,可通过 mapData.keys 打印出 key1 对象。
         * 2、WeakMap 数据:对于 key2 对象,由于 WeakMap 的 key 是弱引用,它不影响垃圾回收器的工作,在.set()表达式执行完之后,垃圾回收器就会将 key2 对象从内存中移除,并且我们无法通过 weakmapData 的 key 值,也就无法通过 weakmapData 取得 key2 对象。
         * 3、简单说:WeakMap 对 key 是弱引用,不影响垃圾回收器的工作。根据这个特性可知:一旦 key 被垃圾回收器回收,那么对应的键和值就访问不到了。
         * 4、WeakMap 应用场景:用于存储那些只有当 key 所引用的对象存在时(没有被回收)才有价值的信息。
         * 例如上面的场景,如果 key2 对象没有任何引用了,说明用户侧不再需要它,这时垃圾回收器就会完成回收任务。如果用 Map 代替 WeakMap ,那么即使用户侧对 key2 没有任何引用,这个 key2 也不会被回收,最终可能导致内存溢出。
         */
    })()
}

// Section4();

/**
 * 第5节 封装处理
 * 1、将 get 拦截函数里 编写副作用函数收集到 “桶” 里的逻辑,单独封装到一个 track 函数中,表示追踪。
 * 2、将 set 拦截函数里 触发副作用函数重新执行的逻辑,单独封装到一个 trigger 函数中,表示触发。
 */

function Section5 () {
    // 原始数据
    const data = { text: 'hello world' };

    // 存储副作用函数的桶
    const bucket = new WeakMap();

    // 用一个全局变量存储被注册的的副作用函数
    let activeEffect;
    // effect 函数用于注册副作用函数
    function effect(fn) {
        // 当调用effect注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
        activeEffect = fn;
        // 执行副作用函数
        fn();
    }

    // 在 get 拦截函数内调用 track 函数追踪变化
    function track (target, key) {
        // 没有 activeEffect,直接 return 属性值
        if (!activeEffect) return;
        let depsMap = bucket.get(target);
        if (!depsMap) {
            bucket.set(target, (depsMap = new Map()))
        };
        let deps = depsMap.get(key);
        if (!deps) {
            depsMap.set(key, (deps = new Set()))
        };
        deps.add(activeEffect);
    }
    // 在 set 拦截函数内调用 trigger 函数触发副作用函数
    function trigger (target, key) {
        const depsMap = bucket.get(target);
        if (!depsMap) return;
        const effects = depsMap.get(key);
        effects && effects.forEach(fn => fn());
    }
    const obj = new Proxy(data, {
        // 拦截读取操作
        get (target, key) {
            // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
            track(target, key);
            // 返回属性值
            return target[key];
        },
        // 拦截设置操作
        set (target, key, newVal) {
            // 设置属性值
            target[key] = newVal;
            // 把副作用函数从桶里取出并执行
            trigger (target, key);
        }
    })

    effect(
        // 匿名函数
        () => {
            console.log('effect run'); // 只打印1次,已确立 effect 和 key 的明确联系
            document.body.innerText = obj.text;
        }
    );

    effect(
        // 匿名函数
        () => {
            // 打印2次,设置 obj.notExit 时触发打印
            console.log('obj.notExit:>> ', obj.notExit);
        }
    );
    setTimeout(() => {
        // 副作用函数中并没有读取 notExit 属性的值 ↑↑↑
        obj.notExit = 'hello Section5'
    }, 1000);
}
Section5();

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/597181.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

车牌号识别系统:PyQT5+QT Designe+crnn/PaddleOCR+YOLO+OpenCV矫正算法。

PyQT5&QT Designecrnn/PaddleOCRYOLO传统OpenCV矫正算法。可视化的车牌识别系统项目。 车牌号识别系统 项目绪论1.项目展示2.视频展示3.整体思路 一、PyQT5 和 QT Designer1.简介2.安装3.使用 二、YOLO检测算法三、OpenCV矫正算法四、crnn/PaddleOCR字符识别算法五、QT界面…

FreeRTOS任务详解

一、任务的创建与删除 1.任务的基本概念 RTOS系统的核心就是任务管理,FreeRTOS 也不例外,而且大多数学习 RTOS 系统的工程 师或者学生主要就是为了使用 RTOS 的多任务处理功能,初步上手 RTOS 系统首先必须掌握的 也是任务的创建、删除、挂起和恢复等操作,由此可见任务管理…

限量背包问题

问题描述 限量背包问题:从m个物品中挑选出最多v个物品放入容量为n的背包。 问题分析 限量背包问题,可以用来解决许多问题,例如要求从n个物品中挑选出最多v个物品放入容量为m的背包使得背包最后的价值最大,或者总共有多少种放法…

我独自升级:崛起怎么下载 我独自升级游戏下载教程分享

定于5月8日全球揭幕的《我独自升级崛起》——一款扣人心弦的动作RPG巨制,灵感采撷于同名动画及网络漫画的热潮,誓将引领满怀热忱的玩家步入一场交织着深邃探索和宏大规模的奇妙冒险。该游戏立足于一个独树一帜的网络武侠宇宙,细腻刻画了一个凡…

VSCode通过SSH连接虚拟机Ubuntu失败

问题说明 最近使用VSCode通过SSH连接Ubuntu,通过VSCode访问Ubuntu进行项目开发,发现连接失败 在VSCode中进行SSH配置 这些都没有问题,但在进行连接时候出现了问题,如下: 出现了下面这个弹窗 解决方法 发现当…

软件测试职责

软件测试职责主要包括以下几个方面: 1. 需求分析:理解软件需求规格说明书,确保测试活动覆盖所有的功能需求和非功能需求(如性能、安全性、兼容性等)。 2. 测试计划制定:根据项目需求,设计测试…

NodeJS 如何在npm运行时设置Windows控制台的标题?

通过代码设置 const server app.listen(port, () > {console.log(主机名称:, global.hostname)console.log(主机IP地址:, global.host)console.log(后台服务端口号:, port)console.log(恭喜你,启动成功!)process.title node …

图像处理

图像处理 导入图片 导入io模块,读取文件所在位置,将生成的图像数据赋给变量img,显示图像 from skimage import ioimgio.imread(D:\工坊\图像处理\十个勤天2.png)io.imshow(img) 运行结果: 将图片进行灰度处理 from skimage i…

Autodesk AutoCAD 2025 for Mac:强大的二维三维绘图工具

Autodesk AutoCAD 2025 for Mac是一款专为Mac用户打造的计算机辅助设计软件,它在继承了AutoCAD系列软件的优秀传统的基础上,针对Mac系统进行了全面优化,为用户提供了更出色的绘图和设计体验。 这款软件不仅支持用户创建和编辑复杂的二维几何图…

Nvidia发布Llama3-ChatQA-1.5: 提升对话问答和表格推理能力,平均性能超越GPT-4

前言 近日,Nvidia推出了一款名为Llama3-ChatQA-1.5的对话问答模型。该模型在对话式问答和检索增强型生成等能力方面表现出色,在综合评测指标上甚至超越了当前业界顶尖的GPT-4模型。 技术特点 Llama3-ChatQA-1.5是基于Llama-3基础模型训练而成的。相比之…

01-基本概念

1. 到底什么是数据结构? 数据结构是指在计算机中组织和存储数据的方式,它涉及到数据元素之间的关系以及对这些关系进行操作的方法。数据结构可以看作是一种将数据组织起来以便有效使用的方式,它关注数据的组织、存储和操作,以及如…

关于冯诺依曼体系结构 和 操作系统(Operator System)的概念讲解(冯诺依曼体系结构,操作系统的作用等)

目录 一、冯诺依曼体系结构 二、操作系统 1. 概念 2. 设计操作系统的目的 3.系统调用和库函数概念 4.总结 三、完结撒❀ 一、冯诺依曼体系结构 我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。 截…

标贝数据采集标注在自动驾驶场景中落地应用实例

AI数据服务作为人工智能和机器学习的基础,在自动驾驶领域中有着重要地位。与其他人工智能应用场景相比,自动驾驶的落地场景相对复杂,想要让汽车本身的算法做到处理更多、更复杂的场景,就需要运用大量场景化高质量AI数据做支撑。标…

第八节课《大模型微调数据构造》

大模型微调数据构造(补充课程)_哔哩哔哩_bilibili Tutorial/FineTune at main Focusshang/Tutorial GitHub 一、大模型训练数据介绍 预训练: 网络、论文数据,无标签数据transform算法base model典型:GPT监督微调 对…

【C语言】整数,浮点数数据在内存中的存储

Tiny Spark get dazzling some day. 目录 1. 整数在内存中的存储1.1 原码、反码、补码1.1 大小端存储1.2.1 字节序分类1.2.2 判断字节序 2. 浮点数在内存中的存储2.1 浮点数的存储形式2.2 浮点数的 “ 存 ”2.2.1 S2.2.2 E2.2.3 F 2.3 浮点数的 “ 取 ”2.3.1 S2.3.2 E、F 3. 浮…

ISIS的基本概念

1.ISIS概述 IS-IS是一种链路状态路由协议,IS-IS与OSPF在许多方面非常相似, 例如运行IS-IS协议的直连设备之间通过发送Hello报文发现彼此,然后建立邻接关系,并交互链路状态信息。 CLNS由以下三个部分组成: CLNP&#xf…

新的项目springboot

buybuyshenglombok <dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency> 添加依赖 lombok package com.example.demo.pojo;import lombok.AllArgsConstructor; import lombok.Data; import …

LLM应用:prompt提示让大模型总结生成Mermaid流程图;充当角色输出

1、prompt提示让大模型总结生成Mermaid流程图 生成内容、总结文章让大模型Mermaid流程图展示&#xff1a; mermaid 美人鱼, 是一个类似 markdown&#xff0c;用文本语法来描述文档图形(流程图、 时序图、甘特图)的工具&#xff0c;您可以在文档中嵌入一段 mermaid 文本来生成 …

项目实战 | 如何恰当的处理 Vue 路由权限

前言 哈喽&#xff0c;小伙伴你好&#xff0c;我是 嘟老板。最近接了一个成本千万级的前端项目运维工作&#xff0c;本着 知己知彼 的态度&#xff0c;我将整个前端的大致设计思路过了一遍。不看不知道&#xff0c;一看…吓一跳。光是 路由权限 这块儿的设计&#xff0c;都让我…

linux上Redis安装使用

环境centOS8 redis是缓存数据库&#xff0c;主要是用于在内存中存储数据&#xff0c;内存的读写很快&#xff0c;加快系统读写数据库的速度 一、Linux 安装 Redis 1. 下载Redis 官网下载Downloads - Redis 历史版本Index of /releases/ 本文中安装的版本为&#xff1a;h…
最新文章