Vue3 reactive原理(一)-代理对象及数组

 Proxy 只能拦截对一个对象的基本操作(例如读取、设置属性值),而无法拦截复合操作(例如,obj.fun(),由两个基本操作组成,1)get到fun这个属性,2)函数调用)。

1 代理对象

操作

跟踪方法

触发方法

访问属性,obj.name

get(traget,p)

set、delete

判断对象或原型上是否存在给定的key, key in obj

has(target,p)

delete,set

使用for...in遍历对象

ownkeys(target)

delete,set(添加新属性时)

表 普通对象所有可能的读取操作

ownkeys方法没有具体的属性,这时我们指定一个唯一符号:ITERATE_KEY。来将它与副作用函数关联。

const ITERATE_KEY = Symbol("ITERATE_KEY")
return new Proxy(obj,{// 省略其他代码set(target, p, newValue, receiver) {const type = Object.prototype.hasOwnProperty.call(target,p) ? 'SET' : 'ADD'const res = Reflect.set(target,p,newValue,receiver)trigger(target,p,type,newValue)return res},deleteProperty(target, p) {const res = Reflect.deleteProperty(target,p)trigger(target,p,'DELETE')return res  },has(target, p) {track(target,p)return Reflect.has(target,p)},ownKeys(target) {track(target,ITERATE_KEY)return Reflect.ownKeys(target)}
})
function trigger(target,p,type,newValue) {const map = effectMap.get(target)if (map) {const addSet = new Set()// 省略其他代码if (type === 'ADD') {const tempSet = map.get(ITERATE_KEY)tempSet && tempSet.forEach(fn => {if (activeEffect !== fn) addSet.add(fn)})}addSet.forEach(fn => fn())}
}

1.1 合理触发

为了提供性能及用户交互,应当满足下列条件时,才触发响应:

  1. 值发生改变时。
  2. 初始值和新值不都是NaN。
  3. 响应对象的原型也说响应对象时,访问原型上的属性时,只触发一次。
return new Proxy(obj,{get(target, p, receiver) {if (p === 'raw') return target// 省略其他代码},set(target, p, newValue, receiver) {// 省略其他代码if (receiver.raw === target) {if (oldVal !== newValue && (newValue === newValue || oldVal === oldVal))    {trigger(target,p,type)}}},deleteProperty(target, p) {const hadKey = Object.prototype.hasOwnProperty.call(target,p)const res = Reflect.deleteProperty(target,p)if (hadKey && res) {trigger(target,p,'DELETE')}},
})

1.2 深响应与浅响应

目前的reactive函数创建的响应对象是浅响应,即对象的首层才具有响应性。如果对象的某个属性值是个对象,那么该对象不具备响应性。而深响应是指无论多少层,对象都具有响应性。

function createReactive(obj,isShallow = false) {if (obj.raw) return objreturn new Proxy(obj,{get(target, p, receiver) {// 省略其他代码const val = Reflect.get(target,p,receiver)if (isShallow) return valreturn typeof val === 'object' && val != null ? createReactive(val) : val},// 省略其他代码})
}

1.2.1 深只读和浅只读

某些数据要是只读的(例如props),当用户尝试修改只读数据时,会收到一条警告信息。浅只读是指,对象的首层不可写,但是其他层可写(对象的一个属性值也是对象时,那么这个属性值对象里的属性是可写的)。

function createReactive(obj,isShallow = false,isReadonly) {if (obj.raw) return objreturn new Proxy(obj,{get(target, p, receiver) {// 省略其他代码const val = Reflect.get(target,p,receiver)if (isShallow) return valreturn typeof val === 'object' && val != null ? (isReadonly ? readonly(val) :  createReactive(val)) : val},set(target, p, newValue, receiver) {if (isReadonly) {console.error('该属性不可写')return}// 省略其他代码},deleteProperty(target, p) {if (isReadonly) {console.error('该属性不可写')return}// 省略其他代码},// 省略其他代码})
}function readonly(obj,isShallow = false) {return createReactive(obj,isShallow,true)
}

2 代理数组

数组也是一种对象(但属于异质对象,与常规对象相比,它们的部分内部方法和常规对象的不同)。因此用于代理普通对象的大部份代码可以继续使用。

2.1 索引与length

  1. 通过索引设置元素值时,可能会影响到length属性,即当设置索引值大等于数组长度时,length属性会发生改变。
  2. 设置length属性,可能会影响到索引值。当length设置为更小值时,索引大等于length的部分元素全部会被删除。
new Proxy(obj,{set(target, p, newValue, receiver) {// 省略其他代码const type = Array.isArray(target) ? (Number(p) < target.length ? 'SET' : 'ADD') :(Object.prototype.hasOwnProperty.call(target,p) ? 'SET' : 'ADD')// 省略其他代码},// 省略其他代码
})function trigger(target,p,type,newValue) {const map = effectMap.get(target)if (map) {// 省略其他代码if (type === 'ADD' && Array.isArray(target)) {const tempSet = map.get("length")tempSet && tempSet.forEach(fn => {if (activeEffect !== fn) addSet.add(fn)})}if (p === 'length' && Array.isArray(target)) {map.forEach((set,key) => {if (key >= newValue) {set.forEach(fn => {if (activeEffect !== fn) addSet.add(fn)})}})}addSet.forEach(fn => fn())}
}

2.2 遍历数组

1)for...in,和普通对象一样,内部会调用ownKeys方法,但不同的是,其触发条件是length的改变。

new Proxy(obj,{ownKeys(target) {track(target,Array.isArray(target) ? 'length' : ITERATE_KEY)return Reflect.ownKeys(target)}// 省略其他代码
})

2)for...of,索引值的设置及length的改变,都会触发该迭代,所以几乎不要添加额外的代码,就能让for...for迭代具有响应性。

从性能上及为了避免发生意外的错误,我们不应该使副作用函数与symbol值之间建立响应联系。

2.3 数组的查找方法

数组的方法内部其实都依赖了对象的基本语义。因此大多数情况下,我们不需要做特殊处理即可让这些方法按预期工作。但某些场景,执行结果会不符合我们预期:

const proxyObj = reactive(['hi','js',{name: 'js'}])console.log(proxyObj.includes('hi'),proxyObj.includes(proxyObj[2])) // true、false

这里,查找基本类型数据时,结果是正确的。但是查找对象时,结果错误。这是因为reactive函数会为属性中的对象也创建响应对象,而且每次都会创建新的响应对象。而且,这里还有两个问题:

  1. 将响应体对象赋值给另一个响应体时,reactive不应该为其再创建响应体了。
  2. 无论查找对象,还是其响应体对象,返回的结果应该一致(从业务的角度看,它们属于同一对象)。
const reactiveMap = new WeakMap()new Proxy(obj,{get(target, p, receiver) {if (p === 'raw') return target// 省略其他代码if (Array.isArray(target) && arrayInstrumentation.hasOwnProperty(p)) {return Reflect.get(arrayInstrumentation,p,receiver)}// 省略其他代码},
})function reactive(obj) {const originalProxy = reactiveMap.get(obj)if (originalProxy) return originalProxy;const proxyObj = createReactive(obj)reactiveMap.set(obj,proxyObj)return proxyObj
}const arrayInstrumentation = {};
['includes','indexOf','lastIndexOf'].forEach(method => {const originalMethod = Array.prototype[method]arrayInstrumentation[method] = function (...args) {let res = originalMethod.apply(this,args)if (res === false || res === -1) {res = originalMethod.apply(this.raw,args)}return res}
});

2.4 隐式修改数组长度的原型方法

push/pop/shift/unshift/splice这些方法会隐式地修改数组长度。例如push方法,在往数组中添加元素时,既会读取length,也会设置数组的length。这会导致两个独立的副作用函数互相影响:

effect(() => {proxyObj.push(1)
})
effect(() => {proxyObj.push(2)
})

运行上面这段代码,会得到栈溢出的错误。因为副作用函数1执行时,会修改及读取length,会触发副作用函数2执行,而副作用2也会修改和读取length。这样就好造成循环调用。

解决方法是:“屏蔽”这些方法对length属性的读取。

['push','pop','shift','unshift','splice'].forEach(method => {const originalMethod = Array.prototype[method]arrayInstrumentation[method] = function (...args) {shouldTrack = falseconst res = originalMethod.apply(this,args)shouldTrack = truereturn res}
})function track(target,p) {if (activeEffect && shouldTrack) {// 跟踪代码}
}

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

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

相关文章

C语言 | Leetcode C语言题解之第257题二叉树的所有路径

题目&#xff1a; 题解&#xff1a; char** binaryTreePaths(struct TreeNode* root, int* returnSize) {char** paths (char**)malloc(sizeof(char*) * 1001);*returnSize 0;if (root NULL) {return paths;}struct TreeNode** node_queue (struct TreeNode**)malloc(size…

vue3使用html2canvas

安装 yarn add html2canvas 代码 <template><div class"container" ref"container"><div class"left"><img :src"logo" alt"" class"logo"><h2>Contractors pass/承包商通行证&l…

Mamba-yolo|结合Mamba注意力机制的视觉检测

一、本文介绍 PDF地址&#xff1a;https://arxiv.org/pdf/2405.16605v1 代码地址&#xff1a;GitHub - LeapLabTHU/MLLA: Official repository of MLLA Demystify Mamba in Vision: A Linear AttentionPerspective一文中引入Baseline Mamba&#xff0c;指明Mamba在处理各种高…

网络通讯实验报告

拓扑图 需求 1、通过DHCP服务&#xff0c;给PC4和PC5分配IP地址、网关、掩码、DNS服务器IP地址 2、Client-1要求手工配置IP地址&#xff0c;为192.168.1.1, c 3、telnet客户端可以远程登录telnet服务器进行设备管理&#xff0c;并成功修改telnet服务器的名字为123 &#xff0c…

操作系统——进程与线程(死锁)

1&#xff09;为什么会产生死锁&#xff1f;产生死锁有什么条件&#xff1f; 2&#xff09;有什么办法解决死锁&#xff1f; 一、死锁 死锁:多个程序因竞争资源而造成的一种僵局&#xff08;互相等待对方手里的资源&#xff09;&#xff0c;使得各个进程都被阻塞&#xff0c;…

一篇文章搞懂MySQL的事务与隔离级别

事务 概述 一个事务其实就是一个完整的业务逻辑&#xff0c;是一个最小的工作单元。要么同时成功&#xff0c;要么同时失败&#xff0c;不可再分 假设转账&#xff0c;从A账户向B账户转账10000 A账户的钱减去10000&#xff08;update语句&#xff09; B账户的钱加上10000&…

【HarmonyOS学习】用户文件访问

概述 文件所有者为登录到该终端设备的用户&#xff0c;包括用户私有的图片、视频、音频、文档等。 应用对用户文件的创建、访问、删除等行为&#xff0c;需要提前获取用户授权&#xff0c;或由用户操作完成。 用户文件访问框架 是一套提供给开发者访问和管理用户文件的基础框…

Web开发:属性

属性 介绍全局属性特定元素属性超链接元素 (< a >)图像元素 (< img >)输入元素 (< input >)表单元素 (< form >)表格元素 (< table >) 其他元素段落元素 (< p >)列表元素 (< ul >, < ol >, < li >) 新增的HTML5属性示例…

无需抠图!AI绘画直接文本生成透明底图层,设计师必看的ComfyUI透明图层生成工作流教程!(附插件模型)

大家好&#xff0c;我是画画的小强 AI 绘画自出现以来一直都在不断发展完善&#xff0c;实现了很多我们在实际应用中迫切需要的功能&#xff0c;比如生成正确的手指、指定的姿势、准确的文本内容等。上周&#xff0c;又一个重磅新功能在开源的 SD 生态内实现了——直接通过文本…

PyCharm中的版本控制大师:Git的高效应用

PyCharm中的版本控制大师&#xff1a;Git的高效应用 在软件开发中&#xff0c;版本控制是一个不可或缺的工具&#xff0c;它帮助开发者管理代码变更、跟踪历史和协同工作。Git是目前最流行的版本控制系统之一&#xff0c;而PyCharm&#xff0c;作为一款功能强大的集成开发环境…

【北京迅为】《i.MX8MM嵌入式Linux开发指南》-第三篇 嵌入式Linux驱动开发篇-第四十四章 注册字符设备号

i.MX8MM处理器采用了先进的14LPCFinFET工艺&#xff0c;提供更快的速度和更高的电源效率;四核Cortex-A53&#xff0c;单核Cortex-M4&#xff0c;多达五个内核 &#xff0c;主频高达1.8GHz&#xff0c;2G DDR4内存、8G EMMC存储。千兆工业级以太网、MIPI-DSI、USB HOST、WIFI/BT…

Springboot项目打包成镜像、使用docker-compose启动

Springboot项目打包成镜像、使用docker-compose启动 1、创建一个boot项目 1、添加依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSch…

什么是保费保额,什么是豁免条款

保额1000万和300万的保费具体数额会因多种因素而异&#xff0c;包括保险公司的不同、保险产品的类型、被保险人的年龄、性别、健康状况、生活习惯&#xff08;如是否吸烟&#xff09;等。因此&#xff0c;没有统一的保费标准。 保费的计算通常涉及以下几个因素&#xff1a; 风…

gitee的怎么上传项目

前提 1.先下载Git Bash (如果没有下载的宝子们下载连接如下: 链接: link ) 项目上传到Gitee步骤 1.在Gitee上建立远程仓库 2.填写相关信息 3.进入本地你想要上传的文件目录下&#xff0c;右键单击空白处&#xff0c;点击Git Bash Here 4.配置你的用户名和邮箱 git con…

【leetcode】排列序列

给出集合 [1,2,3,...,n]&#xff0c;其所有元素共有 n! 种排列。 按大小顺序列出所有排列情况&#xff0c;并一一标记&#xff0c;当 n 3 时, 所有排列如下&#xff1a; "123""132""213""231""312""321" 给定…

最简单的typora+gitee+picgo配置图床

typoragiteepicgo图床 你是否因为管理图片而感到头大&#xff1f;是时候了解一下 Typora、Gitee 和 PicGo 这个超级三剑客了&#xff0c;它们可以帮你轻松打造自己的图床&#xff0c;让你的博客图片管理变得简单又有趣。让我们开始这场神奇的图床之旅吧&#xff01; Typora …

20-c语言main函数参数`argc` 和 `argv[]` 解析

argc 和 argv[] 解析 argc 和 argv[] 是 main 函数的参数&#xff0c;用于处理命令行参数。 一、 示例命令行调用 ./a.out 123 345解释&#xff1a; ./a.out 是程序名&#xff0c;也是第一个参数。123 和 345 是运行时传递的额外参数。 二、main 函数定义 int main(int a…

7.20 模拟赛总结 [邻项交换] + [决策单调性]

只放题解喽 题解 T1T2T3T4 T1 等价于维护差分数组&#xff0c;数据范围较小&#xff0c;map 套 vector 维护即可 更大的数据范围可以 hash 做 T2 神奇贪心 本题关键在于定序&#xff0c;考虑顺序确定后答案怎么求 设 f i f_i fi​ 表示 第 i i i 件衣服烘干完的时间&…

Result of ‘BigDecimal.setScale()‘ is ignored

在Java中&#xff0c;BigDecimal.setScale() 方法用于格式化小数点后的位数&#xff0c;并可以选择舍入模式。如果你看到 "Result of BigDecimal.setScale() is ignored" 这样的警告&#xff0c;可能是因为你调用了 setScale() 方法&#xff0c;但没有将返回的新 Big…

Linux内核处理系统调用过程

Linux内核处理系统调用的过程是操作系统中一个非常关键的部分&#xff0c;它允许用户空间的程序请求内核提供的服务。以下是系统调用处理的一般步骤和概念&#xff1a; 1. **用户空间到内核空间的切换**&#xff1a; - 用户空间的程序通过执行系统调用指令&#xff08;如in…