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 赋值给 activeEffectactiveEffect = 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 赋值给 activeEffectactiveEffect = 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 对应的不同 effectFnlet depsMap = bucket.get(target);// 如果 depsMap 不存在,就新建一个 Map 与 target 关联if (!depsMap) {// 这里的Map,后续用来存储 target 的 keybucket.set(target, (depsMap = new Map()));            }// 2、再根据 key 从 depsMap 中取得 deps, deps是一个 Set 类型,里面存储着所有与当前 key 相关联的副作用函数:effectslet deps = depsMap.get(key);// 如果 deps 不存在,就新建一个 Set 与 key 关联if (!deps) {// 这里的 Set,后续用于存储与 key 关联的副作用函数 effectsdepsMap.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 的 keymapData.set(key1, 1);// WeakMap 数据执行表达式,在此处引用 key2 对象作为 weakmapData 的 keyweakmapData.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 :>>  undefinedconsole.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 赋值给 activeEffectactiveEffect = 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.mzph.cn/news/832633.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Golang | Leetcode Golang题解之第71题简化路径

题目: 题解: func simplifyPath(path string) string {stack : []string{}for _, name : range strings.Split(path, "/") {if name ".." {if len(stack) > 0 {stack stack[:len(stack)-1]}} else if name ! "" &am…

在uniapp里面使用 mp-html 并且开启 latex 功能

在uniapp里面使用 mp-html 并且开启 latex 功能 默认情况下 mp-html 是不会开启 latex 功能的, 如果需要开启 latex 功能是需要到代码操作拉取代码自行打包的。 这里说一下 mp-html 里面的 latex 功能是由 https://github.com/rojer95/katex-mini 提供的技术实现,…

计算机系列之数据库技术

13、数据库技术(重点、考点) 1、三级模式-两级映像(考点) 内模式:管理如何存储物理的数据,对应具体物理存储文件。 **模式:**又称为概念模式,就是我们通常使用的基本表&#xff0c…

每日算法-java

题目来自蓝桥云 // 这是一个Java程序,用于解决最长不下降子序列问题。 // 问题描述:给定一个整数序列,找到最长的子序列,使得这个子序列是不下降的(即相邻的元素不严格递减)。 // 程序使用了动态规划的方法…

【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. 浮…

读取打包到JAR中的文件:常见问题与解决方案(文件在但是报错not find)

读取打包到JAR中的文件:常见问题与解决方案 喝淡酒的时候,宜读李清照;喝甜酒时,宜读柳永;喝烈酒则大歌东坡词。其他如辛弃疾,应饮高梁小口;读放翁,应大口喝大曲;读李后主…

学习c#第26天 面向对象基础之类与对象

1.类 1.什么是类? 俗话说,“物以类聚,人以群分”。意思是同类的东西经常聚在一起,志同道合 的人相聚成群。前者说物,后者说人。这里以物来进行举例说明[见图]: 水果超市,所有同类的水果摆放在一起&#xf…

PHP 框架安全:ThinkPHP 序列 漏洞测试.

什么是 ThinkPHP 框架. ThinkPHP 是一个流行的国内 PHP 框架,它提供了一套完整的安全措施来帮助开发者构建安全可靠的 web 应用程序。ThinkPHP 本身不断更新和改进,以应对新的安全威胁和漏洞。 ThinkPHP 框架的安全特性: (1) 输入过滤和验证…

【go项目01_学习记录05】

学习记录 1 依赖管理 Go Modules1.1 弃用 $GOPATH1.2 Go Modules 日常使用1.2.1 初始化生成go.mod文件1.2.2 Go Proxy代理1.2.3 go.mod文件查看1.2.4 go.sum文件查看1.2.5 indirect 含义1.2.6 go mod tidy 命令1.2.7 清空 Go Modules 缓存1.2.8 下载依赖1.2.9 所有 Go Modules …

[qnx] 通过zcu104 SD卡更新qnx镜像的步骤

0. 概述 本文演示如果给Xlinx zcu104开发板刷入自定义的qnx镜像 1.将拨码开关设置为SD卡启动 如下图所示,将1拨到On,2,3,4拨到Off,即为通过SD启动。 2.准备SD卡中的内容 首先需要将SD格式化为FAT32的(如果已经是FAT32格式,则…

网络安全之OSI七层模型详解

OSI七层模型分为控制层(前三层)和数据层(后四层)。从第七层到一层为; 应用层(7)接收数据 表示层(6)将数据翻译为机器语言 会话层(5)建立虚连接…

C++证道之路第十八章探讨C++新标准

一、复习前面介绍过的C11新功能 1、新类型 C11新增了类型long long 和unsigned long long 新增了类型char16_t 和char32_t 2、统一的初始化 C11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可以用于所有内置类型和用户定义的类…

可编程 IP 新星 Story Protocol 何以引领链上文艺复兴浪潮?

当前,随着 Web3 行业发展进入全新阶段,与生成式人工智能(AIGC)技术融合正在创造潜力新星项目。也是目前的互联网生态下,任何普通民众都有权利创作高质量的音乐、艺术、散文和视频内容,带来了用户生成内容&a…

算法(C++

题目:螺旋矩阵(59. 螺旋矩阵 II - 力扣(LeetCode)) 给你一个正整数 n ,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。 示例 1: 输入&am…

Vue进阶之Vue项目实战(二)

Vue项目实战 构建基础框架路由 项目( v1.0,base-app-layer)导航 router物料编排选型 插件化插件化平时写代码场景 配置器开发 构建基础框架 路由 路由分类: memoryHistory:内存路由,路由信息记录在内存中&#xff0…

解决github的remote rejected|git存储库的推送保护

前言 git存储库的推送保护。当你试图推送代码到GitHub仓库时,由于存在与主分支(master)相关的仓库规则违规行为,推送会被拒绝了。这种保护机制帮助确保只有经过授权和符合规定的代码才能被合并到主分支,从而保护了主分…

Unreal Engine插件打包技巧

打开UE工程,点击编辑,选择插件,点击"打包"按钮,选择输出目录UE4.26版本打包提示需要VS2017问题解决 1)用记事本打开文件【UE4对应版本安装目录\Epic Games\UE_4.26\Engine\Build\BatchFiles\RunUAT.bat】 2&…

Linux网络部分——DNS域名解析服务

目录 1. 域名结构 2. 系统根据域名查找IP地址的过程 3.DNS域名解析方式 4.DNS域名解析的工作原理【☆】 5.域名解析查询方式 6.搭建主从DNS域名服务器 ①初始化操作主服务器和从服务器,安装BIND软件 ②修改主服务器的主配置文件、区域配置文件、区域数…

pyside6的调色板QPalette的简单应用

使用调色板需要先导入:from PySide6.QtGui import QPalette 调色板QPalette的源代码: class QPalette(Shiboken.Object):class ColorGroup(enum.Enum):Active : QPalette.ColorGroup ... # 0x0Normal : QPalette.ColorGrou…

鸿蒙内核源码分析(消息队列篇) | 进程间如何异步传递大数据

基本概念 队列又称消息队列,是一种常用于任务间通信的数据结构。队列接收来自任务或中断的不固定长度消息,并根据不同的接口确定传递的消息是否存放在队列空间中。 任务能够从队列里面读取消息,当队列中的消息为空时,挂起读取任务…