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上开源了,提示‘自行查看代码,维护,运行’。 最近有事,先发布代码了,我就随缘维护更新吧 介绍: 定…...
华为校园公开课走入上海交大,鸿蒙成为专业核心课程
4月12日,华为校园公开课在中国上海交通大学成功举办,吸引了来自计算机等相关专业的150余名学生参加。据了解,由吴帆、陈贵海、过敏意、吴晨涛、刘生钟等教授在中国上海交通大学面向计算机系本科生开设的《操作系统》课程,是该系学…...
【会员单位】泰州玉安环境工程有限公司
中华环保联合会理事单位 水环境治理专业委员会副主任委员单位 我会为会员单位提供服务: 1、企业宣传与技术项目对接; 2、企业标准、行业标准制定; 3、院士专家指导与人才培训 4、国际与国内会议交流 5、专精特新、小巨人等申报认证 6…...
Google视觉机器人超级汇总:从RT、RT-2到AutoRT/SARA-RT/RT-Trajectory、RT-H
前言 随着对视觉语言机器人研究的深入,发现Google的工作很值得深挖,比如RT-2 想到很多工作都是站在Google的肩上做产品和应用,Google真是科技进步的核心推动力,做了大量大模型的基础设施,服(推荐重点关注下Googl…...
LeetCode-1143. 最长公共子序列【字符串 动态规划】
LeetCode-1143. 最长公共子序列【字符串 动态规划】 题目描述:解题思路一:动规五部曲解题思路二:1维DP解题思路三:0 题目描述: 给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。…...
从0开始创建单链表
前言 这次我来为大家讲解链表,首先我们来理解一下什么是单链表,我们可以将单链表想象成火车 每一节车厢装着货物和连接下一个车厢的链子,单链表也是如此,它是将一个又一个的数据封装到节点上,节点里不仅包含着数据&…...
STC89C52学习笔记(十)
STC89C52学习笔记(十) 综述:本文介绍了DS18B20和单总线协议,以及讲述了如何使用DS18B20测量温度。 一、单总线协议 1.只有一根通讯线:DQ (常见的运用单总线的两种设备:DS18B20和DHT11&#…...
初识二叉树和二叉树的基本操作
目录 一、树 1.什么是树 2. 与树相关的概念 二、二叉树 1.什么是二叉树 2.二叉树特点 3.满二叉树与完全二叉树 4.二叉树性质 相关题目: 5.二叉树的存储 6.二叉树的遍历和基本操作 二叉树的遍历 二叉树的基本操作 一、树 1.什么是树 子树是不相交的;…...
如何开辟动态二维数组(C语言)
1. 开辟动态二维数组 C语言标准库中并没有可以直接开辟动态二维数组的函数,但我们可以通过动态一维数组来模拟动态二维数组。 二维数组其实可以看作是一个存着"DataType []"类型数据的一维数组,也就是存放着一维数组地址的一维数组。 所以&…...
【MATLAB第104期】基于MATLAB的xgboost的敏感性分析/特征值排序计算(针对多输入单输出回归预测模型)
【MATLAB第104期】基于MATLAB的xgboost的敏感性分析/特征值排序计算(针对多输入单输出回归预测模型) 因matlab的xgboost训练模型不含敏感性分析算法,本文通过使用single算法,即单特征因素对输出影响进行分析,得出不同…...
C语言程序与设计——工程项目开发
之前我们已经了解了C语言的基础知识部分,掌握这些之后,基本就可以开发一些小程序了。在开发时,就会出现合作的情况,C语言是如何协作开发的呢,将在这一篇文章进行演示。 工程项目开发 在开发过程中,你接到…...
html5动态效果的网站是怎么做的/windows优化大师卸载
java 向下转型与instanceof关键字-总结 文章目录java 向下转型与instanceof关键字-总结1. 向下转型和instanceof关系1.1. 为什么要有向下转型1.2. 向下转型和instanceof有什么关系2. 向下转型和instanceof示例Demo2.1. 父类 Person2.2. 子类 Man2.3.子类Woman2.4. 多态向下转型…...
做往外批发的网站吗/百度贴吧官网入口
再来一发水体,是为了照应上一发水题。 再次也特别说明一下,白书上的中国剩余定理的模板不靠谱。 老子刚刚用柏树上的模板交上去,简直wa出翔啊。 下面隆重推荐安叔版同余方程组的求解方法。 反正这个版本十分强大,适用于各种情况。…...
南昌公司网站建设/百度推广电话客服
2019独角兽企业重金招聘Python工程师标准>>> java当中,类的加载顺序是:类静态块-类静态属性-类内部属性-类构造方法,经过测:类静态块与类静态属性,执行的先后顺序与其在类中的顺序有关 package com.qimh.classloadshux…...
自己做的网站怎么被搜索出来/关键词排名优化系统
利用表之间的关系创建Query 我们经常需要根据表之间的关系用代码创建query,SysQuery这个类提供了一个方法queryFromTableRelation,当然这个方法的代码跟我们平常根据表之间的关系构造query的过程是完全一样的,不过它做成了通用的方法,直接调用它就不用自己每次都重复劳动了,另…...
文登市城乡建设局网站/谷歌关键词工具
原创作品,出自 “深蓝的blog” 博客,欢迎转载,转载时请务必注明以下出处,否则追究版权法律责任。 深蓝的blog:http://blog.csdn.net/huangyanlong/article/details/43938953 开始实验: (1&…...
商丘电子商务网站建设/飓风seo刷排名软件
创建协程的方式主要有:launch()、async()、coroutineScope()、runBlocking() 。 launch() 创建一个异步协程(非阻塞),返回一个不带返回值的 job。 fun main(){GlobalScope.launch{// 默认 CorutineDispatcher 为 Dispatchers.Defaultdelay(1000) // 仅可用于协程,而非线…...