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上开源了,提示‘自行查看代码,维护,运行’。 最近有事,先发布代码了,我就随缘维护更新吧 介绍: 定…...
Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)
文章目录 1.什么是Redis?2.为什么要使用redis作为mysql的缓存?3.什么是缓存雪崩、缓存穿透、缓存击穿?3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...
大数据零基础学习day1之环境准备和大数据初步理解
学习大数据会使用到多台Linux服务器。 一、环境准备 1、VMware 基于VMware构建Linux虚拟机 是大数据从业者或者IT从业者的必备技能之一也是成本低廉的方案 所以VMware虚拟机方案是必须要学习的。 (1)设置网关 打开VMware虚拟机,点击编辑…...
Objective-C常用命名规范总结
【OC】常用命名规范总结 文章目录 【OC】常用命名规范总结1.类名(Class Name)2.协议名(Protocol Name)3.方法名(Method Name)4.属性名(Property Name)5.局部变量/实例变量(Local / Instance Variables&…...
定时器任务——若依源码分析
分析util包下面的工具类schedule utils: ScheduleUtils 是若依中用于与 Quartz 框架交互的工具类,封装了定时任务的 创建、更新、暂停、删除等核心逻辑。 createScheduleJob createScheduleJob 用于将任务注册到 Quartz,先构建任务的 JobD…...
高危文件识别的常用算法:原理、应用与企业场景
高危文件识别的常用算法:原理、应用与企业场景 高危文件识别旨在检测可能导致安全威胁的文件,如包含恶意代码、敏感数据或欺诈内容的文档,在企业协同办公环境中(如Teams、Google Workspace)尤为重要。结合大模型技术&…...
Rust 异步编程
Rust 异步编程 引言 Rust 是一种系统编程语言,以其高性能、安全性以及零成本抽象而著称。在多核处理器成为主流的今天,异步编程成为了一种提高应用性能、优化资源利用的有效手段。本文将深入探讨 Rust 异步编程的核心概念、常用库以及最佳实践。 异步编程基础 什么是异步…...
基于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…...
mac 安装homebrew (nvm 及git)
mac 安装nvm 及git 万恶之源 mac 安装这些东西离不开Xcode。及homebrew 一、先说安装git步骤 通用: 方法一:使用 Homebrew 安装 Git(推荐) 步骤如下:打开终端(Terminal.app) 1.安装 Homebrew…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...
基于Java+VUE+MariaDB实现(Web)仿小米商城
仿小米商城 环境安装 nodejs maven JDK11 运行 mvn clean install -DskipTestscd adminmvn spring-boot:runcd ../webmvn spring-boot:runcd ../xiaomi-store-admin-vuenpm installnpm run servecd ../xiaomi-store-vuenpm installnpm run serve 注意:运行前…...
