js通过Object.defineProperty实现数据响应式

news2025/1/23 22:27:06

目录

  • 数据响应式
    • 属性描述符
    • 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 = "牢大"

界面却并没有及时的同步显示
结果

我们可以说解决这个问题十分简单,直接调用renderFirstNamerenderLastName函数就行了

info.name = "牢大"
renderFirstName()
renderLastName()

结果

可是为什么更改了name我们就需要调用renderFirstNamerenderLastName这两个函数?
我们可以从逻辑上说name的改变会让一个人的姓和名也跟着变更,而一个人的性别却并不和姓名相关,所以不用调用renderSex函数,那如果我们将renderSex的函数修改成以下这样呢

function renderSex() {
    const sex = document.querySelector(".sex>span")
    text = info.name === "贝蒂小熊" ? "赛马娘" : "肘击王"
    sex.innerHTML = info.sex + " - " + text
}

此时的sex依旧是,没有改变,sexname在逻辑上也没有强相关的联系,那么此时应该要调用renderSex函数吗
结果
似乎有哪里不对,可见除了从逻辑层面解释在哪些属性被修改时应该调用哪些函数之外还可以通过其他方面解释
我们再来看下面这个例子

const obj = {
    a: "value",
    b: 1,
    c: new Symbol(),
    d: {
        key: "key"
    }
}
function e() {
    //相关操作......
}
function f() {
    //相关操作......
}
function g() {
    //相关操作......
}
function h() {
    //相关操作......
}

此时无论是obj还是相关的四个函数全是无意义的脏数据,在逻辑上没有任何关联,但每个函数都调用了obj里的某一个属性,我们并不知道哪些函数调用了哪些属性,那么我们该怎么确定在obj里的属性被改变时该调用哪些函数

答案其实很简单,当某一个函数访问了某一个属性,那么这个属性被改变时这个函数就需要同步重新运行,无论这个属性与函数在逻辑上是否相关联,一个函数可以访问多个属性,一个属性可以被多个函数访问,函数在运行期间可能会修改多个属性,多个属性被修改会带动更多的函数运行…

这种解决方案我们通常称之为响应式编程,也被称之为数据响应式

那么新的问题又出来了,我们如何记录哪些属性被哪些函数访问了

属性描述符

我们在学习属性描述符的时候我们学过两个存取属性描述符,分别是setgetset会在属性被设置时调用get会在属性被读取时调用,我们能不能在这两个描述符上完成函数收集函数运行的操作呢?

propertyResponsive

我们定义一个函数用来重写属性的setget描述符

function propertyReponsive(obj, key) {

}

这个函数需要传递两个参数,obj为需要监控的对象,key为具体监控的属性
我们首先需要获得原属性的值

function propertyReponsive(obj, key) {
    let _value = obj[key]
}

然后我们需要拦截原本的getset操作

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 = newValue
            dep.notify()
        }
    })
}

观察器

在之前的代码中我其实还遗留了一个问题,就是我们如何将函数放入window.target中,我们显然不能在每次函数调用前手动的将函数存放在window.target中,在函数运行结束后再将其移除
我们或许可以封装一个函数来协助我们做这件事

function watcher(fn) {
    window.target = fn
    fn()
    window.target = null
}

这么写虽然也能实现功能,但不利于日后的维护与扩展,我们还是将其写成一个

class Watcher {
    constructor(fn, vm, ...args) {
        this.fn = fn
        this.vm = vm
        this.args = args
        window.target = this
        fn.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 = fn
        this.vm = vm
        this.args = args
        window.target = this
        fn.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 = obj
        if (!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 = newValue
            dep.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 = fn
                this.vm = vm
                this.args = args
                window.target = this
                fn.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 = obj
                if (!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 = newValue
                    dep.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就能很好的处理以上的情况了

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

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

相关文章

Android Studio学习15——多页面情况下再看Activity生命周期

按返回键退出APP时&#xff1a; 走正常页面的退出流程&#xff1a;onPause–>onStop–>onDestroy(会Destroy,因为它从任务栈中退出了) 再点击图标回来时&#xff1a; 走正常页面的创建流程&#xff1a;onCreate–>onStart–>onResume 按Home键退出App时&#xff1a…

Leetcode 581. 最短无序连续子数组

心路历程&#xff1a; 本以为这道题要用动态规划求解&#xff0c;因为题目中这几个关键字与动态规划太匹配了&#xff0c;结果想了半天也没发现dp(i)和dp(i-1)的递推关系。 这道题本意考察双指针的做法&#xff0c;也可以用排序后做比较的方式来做。 注意的点&#xff1a; 1…

Advanced RAG 02:揭开 PDF 文档解析的神秘面纱

编者按&#xff1a; 自 2023 年以来&#xff0c;RAG 已成为基于 LLM 的人工智能系统中应用最为广泛的架构之一。由于诸多产品的关键功能&#xff08;如&#xff1a;领域智能问答、知识库构建等&#xff09;严重依赖RAG&#xff0c;优化其性能、提高检索效率和准确性迫在眉睫&am…

为什么函数式编程应该成为软件开发的未来

它很难学&#xff0c;但是你的代码会产生更少的意外 你可能认为软件产品生命周期中最长最昂贵的阶段是系统的初始开发阶段&#xff0c;因为所有那些伟大的功能都是在最初的想象中创建的。事实上&#xff0c;最困难的部分是之后的维护阶段。这是程序员为他们在开发过程中走捷径付…

探索实践昏暗光线低光照场景下目标检测,基于YOLOv7【tiny/l/x】模型开发构建昏暗光线低光照场景下的目标检测识别系统

昏暗光线低光照条件下的目标检测问题&#xff0c;是机器视觉领域一个长期存在且持续受到关注的挑战。这个问题的背景主要源自现代社会对机器视觉技术的广泛需求&#xff0c;特别是在光线条件不佳的环境下&#xff0c;如夜间监控、自动驾驶、安防系统等场景。在昏暗光线或低光照…

C++:红黑树封装实现map、set

一、map、set的底层结构 前面对map、set等树形结构的关联式容器进行了简单的介绍&#xff0c;了解到map、set都是由红黑树封装实现的。红黑树是一种由二叉搜索树进行平衡处理后的平衡树&#xff0c;其查找、插入、删除等操作的时间复杂度为O(logn)&#xff0c;详情请参考数据结…

neo4j图数据库下载安装配置

neo4j下载地址Index of /doc/neo4j/3.5.8/ 1.说明&#xff1a;jdk 1.8 版本对应的 neo4j 数据库版本 推荐安装3.X版本 2.配置系统环境变量 3.启动 neo4j.bat console 4.访问

【Lavavel框架】——各目录作用的介绍

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;开发者-曼亿点 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 曼亿点 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a…

SQLite 4.9的虚拟表机制(十四)

返回&#xff1a;SQLite—系列文章目录 上一篇:SQLite 4.9的 OS 接口或“VFS”&#xff08;十三&#xff09; 下一篇&#xff1a;SQLite—系列文章目录 1. 引言 虚拟表是向打开的 SQLite 数据库连接注册的对象。从SQL语句的角度来看&#xff0c; 虚拟表对象与任何其他…

【c语言】strncpy函数模拟实现

strncpy函数模拟实现 strncpy函数在cplusplus网站中的定义 模拟实现源码 //模拟实现 #include <stdio.h> #include <string.h> #include <assert.h>char* my_strncpy(char* destination, const char* source, size_t num) {assert(destination && so…

C++ | Leetcode C++题解之第16题最接近的三数之和

题目&#xff1a; 题解&#xff1a; class Solution { public:int threeSumClosest(vector<int>& nums, int target) {sort(nums.begin(), nums.end());int n nums.size();int best 1e7;// 根据差值的绝对值来更新答案auto update [&](int cur) {if (abs(cur…

蓝桥杯-单片机组基础14——定时计数器与延时函数2方法实现长短按功能

蓝桥杯单片机组备赛指南请查看 &#xff1a;本专栏第1篇文章 本文章针对蓝桥杯-单片机组比赛开发板所写&#xff0c;代码可直接在比赛开发板上使用。 型号&#xff1a;国信天长4T开发板&#xff08;绿板&#xff09;&#xff0c;芯片&#xff1a;IAP15F2K61S2 &#xff08;使…

Python+Vuecil笔记

Nginx 进入目录: C:\nginx-1.20.2\nginx-1.20.2 start nginx 开始 nginx -s stop 停止 nginx -s quit 退出CSS 通过标签去写css 循环展示数据 JS 点击时执行事件 Django 配置media 在seetings里面修改 STATIC_URL /static/ MEDIA_URL /upload/ MEDIA_ROOT os.pat…

力扣 583. 两个字符串的删除操作

题目来源&#xff1a;https://leetcode.cn/problems/delete-operation-for-two-strings/description/ C题解1&#xff1a;动态规划 寻找word1和word2拥有的公共最长子序列&#xff0c;之后分别对word1和word2进行删除操作&#xff0c;即可使word1和word2相等。 寻找公共最长子…

linux 任务管理器(top, ps),面试考点与面试技巧

图2-1 top命令 RES 表示内存占用。 SHR 表示 share 内存占用. 该视图会自动刷新&#xff0c;按"P"(大写要用 shift) 按照cpu使用率倒排 按 "M"按照内存占用倒排。 图 2-2 top命令开头的几行 top命令开头的几行见图2-2. load average后的三个小数分别表…

Embedding:跨越离散与连续边界——离散数据的连续向量表示及其在深度学习与自然语言处理中的关键角色

Embedding嵌入技术是一种在深度学习、自然语言处理&#xff08;NLP&#xff09;、计算机视觉等领域广泛应用的技术&#xff0c;它主要用于将高维、复杂且离散的原始数据&#xff08;如文本中的词汇、图像中的像素等&#xff09;映射到一个低维、连续且稠密的向量空间中。这些低…

Ubuntu 22.04安装新硬盘并启动时自动挂载

方法一 要在Ubuntu 22.04系统中安装一个新硬盘、对其进行格式化并实现启动时自动挂载&#xff0c;需要按以下步骤操作&#xff1a; 1. 安装硬盘 - 确保你的硬盘正确连接到计算机上&#xff08;涉及硬件安装&#xff09;。 2. 发现新硬盘 - 在系统启动后&#xff0c;打开终端…

机器视觉学习(十二)—— 绘制图形

目录 一、绘制函数参数说明 1.1 cv2.line(&#xff09;绘制直线 1.2 cv2.rectangle&#xff08;&#xff09;绘制矩形 1.3 cv2.circle&#xff08;&#xff09; 绘制圆形 1.4 cv2.ellipse&#xff08;&#xff09;绘制椭圆 1.5 cv2.polylines&#xff08;&#xff09;绘制…

【电路笔记】-逻辑非门

逻辑非门 文章目录 逻辑非门1、概述2、晶体管逻辑非门3、六角施密特反相器逻辑非门是所有逻辑门中最基本的,通常称为反相缓冲器或简称为反相器。 1、概述 反相非门是单输入器件,其输出电平通常为逻辑电平“1”,当其单个输入为逻辑电平“1”时,输出电平变为“低”至逻辑电平…

目标检测——RCNN系列学习(二)Faster RCNN

接着上一篇文章&#xff1a;目标检测——RCNN系列学习(一&#xff09;-CSDN博客 主要内容包含&#xff1a;Faster RCNN 废话不多说。 Faster RCNN [1506.01497] Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks (arxiv.org)https://arxiv.…