js通过Object.defineProperty实现数据响应式
目录
- 数据响应式
- 属性描述符
- propertyResponsive
- 依赖收集
- 依赖队列
- 寻找依赖
- 观察器
- 派发更新
- Observer
- 完整代码
- 关于数据响应式
- 关于Object.defineProperty的限制
数据响应式
假设我们现在有这么一个页面
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><style>p {font-family: '幼圆';font-size: 20px;}</style>
</head><body><p class="firstName">姓:<span></span></p><p class="lastName">名:<span></span></p><p class="sex">性别:<span></span></p><script>const info = {name: "贝蒂小熊",sex: "男"}function renderFirstName() {const firstName = document.querySelector(".firstName>span")firstName.innerHTML = info.name.length > 3 ? info.name.slice(0, 2) : info.name[0]}function renderLastName() {const lastName = document.querySelector(".lastName>span")lastName.innerHTML = info.name.length > 3 ? info.name.slice(2) : info.name.slice(1)}function renderSex() {const sex = document.querySelector(".sex>span")sex.innerHTML = info.sex}renderFirstName()renderLastName()renderSex()</script>
</body></html>
它的页面显示如下

我们可以发现,页面显示的内容实际上是由我们预先定义的数据决定的,页面本身也不会具有任何数据,此时的页面与数据是高度一致的
如果我们将数据更改了会怎么样
info.name = "牢大"
界面却并没有及时的同步显示

我们可以说解决这个问题十分简单,直接调用renderFirstName和renderLastName函数就行了
info.name = "牢大"
renderFirstName()
renderLastName()

可是为什么更改了name我们就需要调用renderFirstName和renderLastName这两个函数?
我们可以从逻辑上说name的改变会让一个人的姓和名也跟着变更,而一个人的性别却并不和姓名相关,所以不用调用renderSex函数,那如果我们将renderSex的函数修改成以下这样呢
function renderSex() {const sex = document.querySelector(".sex>span")text = info.name === "贝蒂小熊" ? "赛马娘" : "肘击王"sex.innerHTML = info.sex + " - " + text
}
此时的sex依旧是男,没有改变,sex和name在逻辑上也没有强相关的联系,那么此时应该要调用renderSex函数吗

似乎有哪里不对,可见除了从逻辑层面解释在哪些属性被修改时应该调用哪些函数之外还可以通过其他方面解释
我们再来看下面这个例子
const obj = {a: "value",b: 1,c: new Symbol(),d: {key: "key"}
}
function e() {//相关操作......
}
function f() {//相关操作......
}
function g() {//相关操作......
}
function h() {//相关操作......
}
此时无论是obj还是相关的四个函数全是无意义的脏数据,在逻辑上没有任何关联,但每个函数都调用了obj里的某一个属性,我们并不知道哪些函数调用了哪些属性,那么我们该怎么确定在obj里的属性被改变时该调用哪些函数呢
答案其实很简单,当某一个函数访问了某一个属性,那么这个属性被改变时这个函数就需要同步重新运行,无论这个属性与函数在逻辑上是否相关联,一个函数可以访问多个属性,一个属性可以被多个函数访问,函数在运行期间可能会修改多个属性,多个属性被修改会带动更多的函数运行…
这种解决方案我们通常称之为响应式编程,也被称之为数据响应式
那么新的问题又出来了,我们如何记录哪些属性被哪些函数访问了呢
属性描述符
我们在学习属性描述符的时候我们学过两个存取属性描述符,分别是set和get,set会在属性被设置时调用,get会在属性被读取时调用,我们能不能在这两个描述符上完成函数收集与函数运行的操作呢?
propertyResponsive
我们定义一个函数用来重写属性的set和get描述符
function propertyReponsive(obj, key) {}
这个函数需要传递两个参数,obj为需要监控的对象,key为具体监控的属性
我们首先需要获得原属性的值
function propertyReponsive(obj, key) {let _value = obj[key]
}
然后我们需要拦截原本的get和set操作
function propertyReponsive(obj, key) {let _value = obj[key]Object.defineProperty(obj, key, {get() {return _value},set(newValue) {_value = newValue}})
}
现在我们就需要在get中收集函数,在set中调用函数
依赖收集
在get中收集函数的这个环节,我们通常称之为依赖收集,即收集依赖该属性的函数
那么什么是依赖呢
依赖简单的来说就是函数在运行期间用到了哪些属性,就被称之为函数依赖于哪些属性
与依赖收集对应的操作叫做派发更新,意思也能简单,就是将收集到的函数重新再运行一遍就是派发更新
那么现在我们就有了一个新问题,这些依赖收集到哪呢
依赖队列
我们可以定义一个依赖队列,专门用来维护各个属性的依赖函数,这个依赖队列可以简单的就定义为一个数组,但为了日后的可维护和可扩展,我们将其定义为一个类,这个类的名字就命名为Dep
class Dep {constructor() {this.subs = new Set()}addSub(sub) {this.subs.add(sub)}
}
subs是一个set集合,专门用来存放依赖,之所以定义成set而不是数组是因为考虑到了依赖可能会重复的情况
我们现在虽然解决了如何存放依赖,那我们怎么才能找到依赖呢
寻找依赖
我们不妨转变一下思路,我们为什么无法寻找到依赖,因为函数的运行位置我们无法掌握,函数会通过各种各样的方式被调用运行,我们能不能规定每次调用函数时必须在某个特定的地方调用,这个地方可以是一个全局变量,可以是全局对象上的一个属性,在每次调用函数前函数必须要存放到这个指定的地方来调用,调用完之后再将函数移除留待其他函数调用
使用以上方案的话我们在Dep中寻找依赖就只需要监听特定变量/属性就能获得依赖
class Dep {constructor() {this.subs = new Set()}addSub(sub) {this.subs.add(sub)}depend() {if (window.target)this.addSub(window.target)}
}
depend方法用来在每次属性get操作被调用时收集当前依赖并存放到subs
我们先不去考虑如何在每次函数调用前将函数存放到特定的地方,只考虑依赖队列的话这么写无疑能获取依赖
在依赖收集后我们还需要在属性变更后及时派发更新
class Dep {constructor() {this.subs = new Set()}addSub(sub) {this.subs.add(sub)}depend() {if (window.target)this.addSub(window.target)}notify() {for (const sub of this.subs) {sub()}}
}
notify方法用于在属性set操作被调用时将sub里的依赖全部执行一遍
基于此我们就能实现依赖的收集了,最后我们再修改一下propertyResponse函数
function propertyReponsive(obj, key) {let _value = obj[key]let dep = new Dep()Object.defineProperty(obj, key, {get() {dep.depend()return _value},set(newValue) {_value = newValuedep.notify()}})
}
观察器
在之前的代码中我其实还遗留了一个问题,就是我们如何将函数放入window.target中,我们显然不能在每次函数调用前手动的将函数存放在window.target中,在函数运行结束后再将其移除
我们或许可以封装一个函数来协助我们做这件事
function watcher(fn) {window.target = fnfn()window.target = null
}
这么写虽然也能实现功能,但不利于日后的维护与扩展,我们还是将其写成一个类
class Watcher {constructor(fn, vm, ...args) {this.fn = fnthis.vm = vmthis.args = argswindow.target = thisfn.call(this.vm, this.args)window.target = null}
}
实例化一个Watcher对象需要传递三个参数,一个函数,一个当前函数对应的上下文,一个为函数运行时所需的参数
值得注意的是此时window.target存放的不再是函数,而是一个Watcher对象,为什么不直接存放函数呢,因为如果存放函数的话this和参数都有可能会发生错误,所以综合考虑才传递一个Watcher对象
当sub不再是一个函数时,这意味着在依赖队列里不能再通过简单粗暴的sub()来派发更新了,那该怎么解决呢
派发更新
我们或许可以在Watcher中定义一个方法,由这个方法来负责此函数的更新操作,在依赖队列中我们只需要调用这个方法就能完成派发更新
class Watcher {constructor(fn, vm, ...args) {this.fn = fnthis.vm = vmthis.args = argswindow.target = thisfn.call(this.vm, this.args)window.target = null}update() {this.fn.call(this.vm, this.args)}
}
update方法负责重新将函数执行一遍
Watcher改好了还需要修改Dep
class Dep {constructor() {this.subs = new Set()}addSub(sub) {this.subs.add(sub)}depend() {if (window.target)this.addSub(window.target)}notify() {for (const sub of this.subs) {sub.update()}}
}
Observer
现在,以上的代码已经能实现监测一个对象上的一个属性的数据响应式功能了,但如果我们需要监听一个对象的全部属性,乃至全部的子属性,我们就需要继续封装一个函数来解决
这里我们还是通过类的方式实现
class Observer {constructor(obj) {this.data = objif (!Array.isArray(this.data))this.walk()}walk() {for (const key in this.data) {propertyReponsive(this.data, key)}}
}
在Observer中因为Object.defineProperty只能监测对象,对于数组并不能监测,所以我们在执行walk之前需要对类型进行判断
我们接下来修改propertyResponse函数以支持递归监测
function propertyReponsive(obj, key) {let _value = obj[key]if (typeof _value === "object") new Observer(_value)let dep = new Dep()Object.defineProperty(obj, key, {get() {dep.depend()return _value},set(newValue) {_value = newValuedep.notify()}})
}
完整代码
到此为止我们就将整个数据响应式写完了,我们最后来看看效果
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><style>p {font-family: '幼圆';font-size: 20px;}</style>
</head><body><p class="firstName">姓:<span></span></p><p class="lastName">名:<span></span></p><p class="sex">性别:<span></span></p><input type="text" onchange="this.value===''? info.name='贝蒂小熊': info.name=this.value"><script>class Watcher {constructor(fn, vm, ...args) {this.fn = fnthis.vm = vmthis.args = argswindow.target = thisfn.call(this.vm, this.args)window.target = null}update() {this.fn.call(this.vm, this.args)}}class Dep {constructor() {this.subs = new Set()}addSub(sub) {this.subs.add(sub)}depend() {if (window.target)this.addSub(window.target)}notify() {for (const sub of this.subs) {sub.update()}}}class Observer {constructor(obj) {this.data = objif (!Array.isArray(this.data))this.walk()}walk() {for (const key in this.data) {propertyReponsive(this.data, key)}}}function propertyReponsive(obj, key) {let _value = obj[key]if (typeof _value === "object") new Observer(_value)let dep = new Dep()Object.defineProperty(obj, key, {get() {dep.depend()return _value},set(newValue) {_value = newValuedep.notify()}})}</script><script>const info = {name: "贝蒂小熊",sex: "男"}function renderFirstName() {const firstName = document.querySelector(".firstName>span")firstName.innerHTML = info.name.length > 3 ? info.name.slice(0, 2) : info.name[0]}function renderLastName() {const lastName = document.querySelector(".lastName>span")lastName.innerHTML = info.name.length > 3 ? info.name.slice(2) : info.name.slice(1)}function renderSex() {const sex = document.querySelector(".sex>span")sex.innerHTML = info.sex}new Observer(info)new Watcher(renderFirstName, window)new Watcher(renderLastName, window)new Watcher(renderSex, window)</script>
</body></html>

关于数据响应式
最后我们再来谈谈什么是数据响应式
粗犷的来说,当数据改变时页面会自动的根据数据的变化来变化,而这背后其实是当数据改变时,依赖此数据的函数会同步执行,数据响应式的本质就是依赖收集和派发更新,依赖收集即将数据与被监听的函数关联起来,派发更新即重运行依赖关系的函数,核心就是拦截getter和setter
关于Object.defineProperty的限制
因为Object.defintProperty只能监听单个属性的读取修改操作,当新增属性或者删除属性时无法监听
另外Object.defineProperty也无法监听数组的变化,所以以上两种情况都需要单独监听,而如果使用ES6中的Proxy和Reflect就能很好的处理以上的情况了
相关文章:
js通过Object.defineProperty实现数据响应式
目录 数据响应式属性描述符propertyResponsive 依赖收集依赖队列寻找依赖 观察器 派发更新Observer完整代码关于数据响应式关于Object.defineProperty的限制 数据响应式 假设我们现在有这么一个页面 <!DOCTYPE html> <html lang"en"><head><m…...
docker最简单教程(使用dockerfile构建环境)
一 手里有的东西 安装好的docker+dockerfile 二 操作 只需要在你的dockerfile文件下执行命令 docker build -t="xianhu/centos:gitdir" . 将用户名、操作系统和tag进行修改就可以了,这就相当于在你本地安装了一个docker环境,然后执行 docker run -it xianhu/ce…...
Vue2 —— 学习(三)
目录 一、绑定 class 样式 (一)字符串写法 1.流程介绍 2.代码实现 (二)数组写法 1.流程介绍 2.代码实现 (三)对象写法 1.流程介绍 2.代码实现 二、绑定 style 样式(了解ÿ…...
Qt Creator 12.0.2 debug 无法查看变量的值 Expression too Complex
鼠标放在局部变量上提示“expression too complex”。 在调试窗口也看不到局部变量的值。 这应该是qt的一个bug,https://bugreports.qt.io/browse/QTCREATORBUG-24180 暂时解决方法: 如下图,需要右键项目然后执行"Clean"和&quo…...
LeetCode-Java:303、304区域检索(前缀和)
文章目录 题目303、区域和检索(数组不可变)304、二维区域和检索(矩阵不可变) 解①303,一维前缀和②304,二维前缀和 算法前缀和一维前缀和二维前缀和 题目 303、区域和检索(数组不可变ÿ…...
出海业务的网络安全挑战
出海业务的扩展带来了巨大的市场机遇,同时也带来了不少网络安全挑战: 数据泄露与隐私保护:跨境数据传输增加了数据被截获和泄露的风险。地理位置限制和审查:某些地区的网络审查和地理位置限制可能阻碍企业正常开展业务。网络攻击…...
蓝桥杯考前准备— — c/c++
蓝桥杯考前准备— — c/c 对于输入输出函数 如果题目中有要求规定输入数据的格式与输出数据的格式,最好使用scanf()和prinrf()函数。 例如:输入的数据是 2020-02-18,则使用scanf("%d-%d-%d",&year,&mouth,&day)即可…...
【MATLAB源码-第4期】基于MATLAB的1024QAM误码率曲线,以及星座图展示。
1、算法描述 正交幅度调制(QAM,Quadrature Amplitude Modulation)是一种在两个正交载波上进行幅度调制的调制方式。这两个载波通常是相位差为90度(π/2)的正弦波,因此被称作正交载波。这种调制方式因此而得…...
数据结构-----枚举、泛型进阶(通配符?)
文章目录 枚举1 背景及定义2 使用3 枚举优点缺点4 枚举和反射4.1 枚举是否可以通过反射,拿到实例对象呢? 5 总结 泛型进阶1 通配符 ?1.1 通配符解决什么问题1.2 通配符上界1.3 通配符下界 枚举 1 背景及定义 枚举是在JDK1.5以后引入的。主要用途是&am…...
线上问题监控 Sentry 接入全过程
背景: 线上偶发问题出现后 ,测试人员仅通过接口信息无法复现错误场景;并且线上环境的监控,对于提高系统的稳定性 (降低脱发率) 至关重要;现在线上监控工具这个多,为什么选择Sentry?…...
【数据库(MySQL)基础】以MySQL为例的数据库基础
文章目录 0. 本文用到的emp表,dept表,salgrade表1. MySQL入门2. 简单查询3. 字段计算4. 条件查询4.1 and4.2 null4.3 or4.4 and和or的优先级4.4 in 和 not in4.5 模糊查询 5. 排序5.1 简单排序5.2 两个字段排序5.3 综合排序 6. 一些常用函数6.1 大小写转换6.2 substr子字符串6.…...
权限修饰符,代码块,抽象类,接口.Java
1,权限修饰符 权限修饰符:用来控制一个成员能够被访问的范围可以修饰成员变量,方法,构造方法,内部类 👻👗👑权限修饰符的分类 🧣四种作用范围由小到大(private<空着…...
CSS设置文本
目录 概述: text-aling: text-decoration: text-transform: text-indent: line-height: letter-spacing: word-spacing: text-shadow: vertical-align: white-space: direction: 概述: 在CSS中我们可以设置文本的属性,就像Word文…...
【svg】—— java提取svg中的颜色
需要针对svg元素进行解析,并提取其中的颜色,首先需要知道svg中的颜色。针对svg中颜色的格式大致可以一般有纯色和渐变两种形式。对于渐变有分为:线性渐变和放射性渐变针对svg中的颜色支持16进制的格式,又可以支持RGB的格式&#x…...
论文分享 | FAST'23 阿里云提出的针对SMR优化的存储引擎SMRSTORE
今天分享的一篇最近阅读的论文是FAST23的SMRstore: A Storage Engine for Cloud Object Storage on HM-SMR Drives。 https://www.usenix.org/conference/fast23/presentation/zhou 这篇文章是由阿里巴巴公司完成的,在这篇文章中,团队针对SMR的特性提出了…...
题目:建造房屋 (蓝桥OJ3362)
问题描述: 代码: #include<bits/stdc.h> using namespace std; int n, m, k, ans, mod 1e9 7; long long dp[55][2605]; /*dp[i][j]:第i个街道上建j个房屋的总方案数枚举所有的转移,累加到dp[n][k]即总方案数 */ int main() {cin >> n &…...
智能合约平台开发指南
随着区块链技术的普及,智能合约平台已经成为了这个领域的一个重要趋势。智能合约可以自动化执行合同条款,大大减少了执行和监督合同条款所需的成本和时间。那么,如何开发一个智能合约平台呢?以下是一些关键步骤。 一、选择合适的区…...
数学建模-最优包衣厚度终点判别法(主成分分析)
💞💞 前言 hello hello~ ,这里是viperrrrrrr~💖💖 ,欢迎大家点赞🥳🥳关注💥💥收藏🌹🌹🌹 💥个人主页ÿ…...
Mysql内存表及使用场景(12/16)
内存表(Memory引擎) InnoDB引擎使用B树作为主键索引,数据按照索引顺序存储,称为索引组织表(Index Organized Table)。 Memory引擎的数据和索引分开存储,数据以数组形式存放,主键索…...
Django交易商场
Hello,我是小恒不会java 最近学习django,写了一个demo,学到了不少东西。 我在GitHub上开源了,提示‘自行查看代码,维护,运行’。 最近有事,先发布代码了,我就随缘维护更新吧 介绍: 定…...
Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以?
Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以? 在 Golang 的面试中,map 类型的使用是一个常见的考点,其中对 key 类型的合法性 是一道常被提及的基础却很容易被忽视的问题。本文将带你深入理解 Golang 中…...
Spring Boot+Neo4j知识图谱实战:3步搭建智能关系网络!
一、引言 在数据驱动的背景下,知识图谱凭借其高效的信息组织能力,正逐步成为各行业应用的关键技术。本文聚焦 Spring Boot与Neo4j图数据库的技术结合,探讨知识图谱开发的实现细节,帮助读者掌握该技术栈在实际项目中的落地方法。 …...
(转)什么是DockerCompose?它有什么作用?
一、什么是DockerCompose? DockerCompose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器。 Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行。 DockerCompose就是把DockerFile转换成指令去运行。 …...
Hive 存储格式深度解析:从 TextFile 到 ORC,如何选对数据存储方案?
在大数据处理领域,Hive 作为 Hadoop 生态中重要的数据仓库工具,其存储格式的选择直接影响数据存储成本、查询效率和计算资源消耗。面对 TextFile、SequenceFile、Parquet、RCFile、ORC 等多种存储格式,很多开发者常常陷入选择困境。本文将从底…...
基于TurtleBot3在Gazebo地图实现机器人远程控制
1. TurtleBot3环境配置 # 下载TurtleBot3核心包 mkdir -p ~/catkin_ws/src cd ~/catkin_ws/src git clone -b noetic-devel https://github.com/ROBOTIS-GIT/turtlebot3.git git clone -b noetic https://github.com/ROBOTIS-GIT/turtlebot3_msgs.git git clone -b noetic-dev…...
推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材)
推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材) 这个项目能干嘛? 使用 gemini 2.0 的 api 和 google 其他的 api 来做衍生处理 简化和优化了文生图和图生图的行为(我的最主要) 并且有一些目标检测和切割(我用不到) 视频和 imagefx 因为没 a…...
QT3D学习笔记——圆台、圆锥
类名作用Qt3DWindow3D渲染窗口容器QEntity场景中的实体(对象或容器)QCamera控制观察视角QPointLight点光源QConeMesh圆锥几何网格QTransform控制实体的位置/旋转/缩放QPhongMaterialPhong光照材质(定义颜色、反光等)QFirstPersonC…...
【 java 虚拟机知识 第一篇 】
目录 1.内存模型 1.1.JVM内存模型的介绍 1.2.堆和栈的区别 1.3.栈的存储细节 1.4.堆的部分 1.5.程序计数器的作用 1.6.方法区的内容 1.7.字符串池 1.8.引用类型 1.9.内存泄漏与内存溢出 1.10.会出现内存溢出的结构 1.内存模型 1.1.JVM内存模型的介绍 内存模型主要分…...
论文阅读:Matting by Generation
今天介绍一篇关于 matting 抠图的文章,抠图也算是计算机视觉里面非常经典的一个任务了。从早期的经典算法到如今的深度学习算法,已经有很多的工作和这个任务相关。这两年 diffusion 模型很火,大家又开始用 diffusion 模型做各种 CV 任务了&am…...
智能职业发展系统:AI驱动的职业规划平台技术解析
智能职业发展系统:AI驱动的职业规划平台技术解析 引言:数字时代的职业革命 在当今瞬息万变的就业市场中,传统的职业规划方法已无法满足个人和企业的需求。据统计,全球每年有超过2亿人面临职业转型困境,而企业也因此遭…...
