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上开源了,提示‘自行查看代码,维护,运行’。 最近有事,先发布代码了,我就随缘维护更新吧 介绍: 定…...
Python爬虫实战:研究MechanicalSoup库相关技术
一、MechanicalSoup 库概述 1.1 库简介 MechanicalSoup 是一个 Python 库,专为自动化交互网站而设计。它结合了 requests 的 HTTP 请求能力和 BeautifulSoup 的 HTML 解析能力,提供了直观的 API,让我们可以像人类用户一样浏览网页、填写表单和提交请求。 1.2 主要功能特点…...
挑战杯推荐项目
“人工智能”创意赛 - 智能艺术创作助手:借助大模型技术,开发能根据用户输入的主题、风格等要求,生成绘画、音乐、文学作品等多种形式艺术创作灵感或初稿的应用,帮助艺术家和创意爱好者激发创意、提高创作效率。 - 个性化梦境…...

【kafka】Golang实现分布式Masscan任务调度系统
要求: 输出两个程序,一个命令行程序(命令行参数用flag)和一个服务端程序。 命令行程序支持通过命令行参数配置下发IP或IP段、端口、扫描带宽,然后将消息推送到kafka里面。 服务端程序: 从kafka消费者接收…...
五年级数学知识边界总结思考-下册
目录 一、背景二、过程1.观察物体小学五年级下册“观察物体”知识点详解:由来、作用与意义**一、知识点核心内容****二、知识点的由来:从生活实践到数学抽象****三、知识的作用:解决实际问题的工具****四、学习的意义:培养核心素养…...

如何在看板中有效管理突发紧急任务
在看板中有效管理突发紧急任务需要:设立专门的紧急任务通道、重新调整任务优先级、保持适度的WIP(Work-in-Progress)弹性、优化任务处理流程、提高团队应对突发情况的敏捷性。其中,设立专门的紧急任务通道尤为重要,这能…...
WEB3全栈开发——面试专业技能点P2智能合约开发(Solidity)
一、Solidity合约开发 下面是 Solidity 合约开发 的概念、代码示例及讲解,适合用作学习或写简历项目背景说明。 🧠 一、概念简介:Solidity 合约开发 Solidity 是一种专门为 以太坊(Ethereum)平台编写智能合约的高级编…...

NFT模式:数字资产确权与链游经济系统构建
NFT模式:数字资产确权与链游经济系统构建 ——从技术架构到可持续生态的范式革命 一、确权技术革新:构建可信数字资产基石 1. 区块链底层架构的进化 跨链互操作协议:基于LayerZero协议实现以太坊、Solana等公链资产互通,通过零知…...

什么是Ansible Jinja2
理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具,可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板,允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板,并通…...
JavaScript基础-API 和 Web API
在学习JavaScript的过程中,理解API(应用程序接口)和Web API的概念及其应用是非常重要的。这些工具极大地扩展了JavaScript的功能,使得开发者能够创建出功能丰富、交互性强的Web应用程序。本文将深入探讨JavaScript中的API与Web AP…...
MySQL JOIN 表过多的优化思路
当 MySQL 查询涉及大量表 JOIN 时,性能会显著下降。以下是优化思路和简易实现方法: 一、核心优化思路 减少 JOIN 数量 数据冗余:添加必要的冗余字段(如订单表直接存储用户名)合并表:将频繁关联的小表合并成…...