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 = "牢大"

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

我们可以说解决这个问题十分简单,直接调用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就能很好的处理以上的情况了

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ics-05-攻防世界

题目 点了半天只有设备维护中心能进去 御剑扫一下 找到一个css 没什么用 再点击云平台设备维护中心url发生了变化 设备维护中心http://61.147.171.105:65103/index.php?pageindex试一下php伪协议 php://filter/readconvert.base64-encode/resourceindex.php base64解一下…

python 指数搜索(Exponential Search)

该搜索算法的名称可能会产生误导&#xff0c;因为它的工作时间为 O(Log n)。该名称来自于它搜索元素的方式。 给定一个已排序的数组和要 搜索的元素 x&#xff0c;找到 x 在数组中的位置。 输入&#xff1a;arr[] {10, 20, 40, 45, 55} x 45 输出&#xff1a;在索…

打包与发布iOS应用的完整指南

摘要 本文旨在指导开发者如何准备工作、打包和发布iOS应用。详细介绍了生成请求证书文件、生成APP开发证书及发布证书、生成APP ID、添加调试设备、生成描述文件等步骤。同时&#xff0c;结合案例演示和实际操作&#xff0c;帮助读者更好地理解和应用这些步骤。通过本文&#…

34470A是德科技34470A数字万用表

181/2461/8938产品概述&#xff1a; Truevolt数字万用表&#xff08;34460A、34461A、34465A、34470A&#xff09;利用是德科技的新专利技术&#xff0c;使您能够快速获得见解、测量低功耗设备并保持校准的测量结果。Truevolt提供全方位的测量能力&#xff0c;具有更高的精度、…

基于51单片机教室灯光全自动控制设计( proteus仿真+程序+设计报告+原理图+讲解视频)

基于51单片机教室灯光全自动控制设计( proteus仿真程序设计报告原理图讲解视频&#xff09; 基于51单片机教室灯光全自动控制设计 1. 主要功能&#xff1a;2. 讲解视频&#xff1a;3. 仿真设计4. 程序代码5. 设计报告6. 原理图7. 设计资料内容清单&&下载链接 仿真图pro…

Python | NCL风格 | EOF | 相关 | 回归

这里在linux系统上使用geocat实现NCL风格的图片绘制 Linux上安装 geocat conda update conda conda create -n geocat -c conda-forge geocat-viz conda activate geocat conda update geocat-vizDataset - NOAA Optimum Interpolation (OI) SST V2 # 海温月平均数据 - lsmas…

dubbo知识点

为什么要用 Dubbo&#xff1f; 随着服务化的进一步发展&#xff0c;服务越来越多&#xff0c;服务之间的调用和依赖关系也越来越复杂&#xff0c;诞生了面向服务的架构体系(SOA)&#xff0c;也因此衍生出了一系列相应的技术&#xff0c;如对服务提供、服务调用、连接处理、通信…

React - 你使用过高阶组件吗

难度级别:初级及以上 提问概率:55% 高阶组件并不能单纯的说它是一个函数,或是一个组件,在React中,函数也可以做为一种组件。而高阶组件就是将一个组件做为入参,被传入一个函数或者组件中,经过一定的加工处理,最终再返回一个组件的组合…

海外仓的痛点和需求都有哪些?位像素海外仓系统能解决什么问题?

在全球化贸易的时代&#xff0c;越来越多人将目光聚焦在海外仓上&#xff0c;下场想要分一杯羹。然而&#xff0c;海外仓管理过程中也存在着许多痛点和挑战。为此&#xff0c;海外仓都会使用海外仓系统来协助管理海外仓。来探讨一下海外仓的痛点、需求以及海外仓系统能够解决的…

TiDB 慢查询日志分析

导读 TiDB 中的慢查询日志是一项 关键的性能监控工具&#xff0c;其主要作用在于协助数据库管理员追踪执行时间较长的 SQL 查询语句。 通过记录那些超过设定阈值的查询&#xff0c;慢查询日志为性能优化提供了关键的线索&#xff0c;有助于发现潜在的性能瓶颈&#xff0c;优化…

XML HTTP传输 小结

what’s XML XML 指可扩展标记语言&#xff08;eXtensible Markup Language&#xff09;。 XML 被设计用来传输和存储数据&#xff0c;不用于表现和展示数据&#xff0c;HTML 则用来表现数据。 XML 是独立于软件和硬件的信息传输工具。 应该掌握的基础知识 HTMLJavaScript…

跨越网络边界:借助C++编写的下载器程序,轻松获取Amazon商品信息

背景介绍 在数字化时代&#xff0c;数据是新的石油。企业和开发者都在寻找高效的方法来收集和分析网络上的信息。亚马逊&#xff0c;作为全球最大的电子商务平台之一&#xff0c;拥有丰富的商品信息&#xff0c;这对于市场分析和竞争情报来说是一个宝贵的资源。 问题陈述 然…

相机标定——四个坐标系介绍

世界坐标系(Xw,Yw,Zw) 世界坐标系是一个用于描述和定位三维空间中物体位置的坐标系&#xff0c;通常反映真实世界下物体的位置和方向。它是一个惯性坐标系&#xff0c;被用作整个场景或系统的参考框架。在很多情况下&#xff0c;世界坐标系被认为是固定不变的&#xff0c;即它…

Windows系统配置Docker的国内镜像

1.打开docker的设置&#xff0c;点击Docker Engine 2.添加国内的镜像源&#xff0c;将下面的内容加进去 "registry-mirrors": ["https://docker.mirrors.ustc.edu.cn","https://registry.docker-cn.com","http://hub-mirror.c.163.com&quo…

电动汽车电池管理系统(BMS)

1 动力电池 目前几乎所有电动汽车都使用锂离子电池作为动力电池&#xff0c;根据极性材料的选择不同&#xff0c;动力电池可分为3种&#xff1a;镍钴锰三元电池NMC&#xff0c;镍钴铝三元电池NCA和磷酸铁锂电池LFP 1.1 NMC 镍钴锰三元电池&#xff0c;简称 NCM&#xff0c;是取…

基于wsl的Ubuntu20.04上安装桌面环境

在子系统Ubuntu20.04上安装桌面环境 1. 更换软件源 由于Ubuntu默认的软件源在国外&#xff0c;有时候后可能会造成下载软件卡顿&#xff0c;这里我们更换为国内的阿里云源&#xff0c;其他国内源亦可。 双击打开Ubuntu20.04 LTS图标&#xff0c;在命令行中输入 # 备份原来的软…

LeetCode-74. 搜索二维矩阵【数组 二分查找 矩阵】

LeetCode-74. 搜索二维矩阵【数组 二分查找 矩阵】 题目描述&#xff1a;解题思路一&#xff1a;先二分查找行&#xff0c;再二分查找列。解题思路二&#xff1a;暴力遍历&#xff0c;也能过。解题思路三&#xff1a;用python的in。 题目描述&#xff1a; 给你一个满足下述两条…

HarmonyOS NEXT应用开发之Tab组件实现增删Tab标签

介绍 本示例介绍使用了Tab组件实现自定义增删Tab页签的功能。该场景多用于浏览器等场景。 效果图预览 使用说明&#xff1a; 点击新增按钮&#xff0c;新增Tab页面。点击删除按钮&#xff0c;删除Tab页面。 实现思路 设置Tab组件的barHeight为0&#xff0c;隐藏组件自带的…

实践笔记-03 docker buildx 使用

docker buildx 使用 1.启用docker buildx2.启用 binfmt_misc3.从默认的构建器切换到多平台构建器3.1创建buildkitd.toml文件&#xff08;私有仓库是http没有证书的情况下&#xff0c;需要配置&#xff09;3.2创建构建器并使用新创建的构建器 4.构建多架构镜像并推送至harbor仓库…

5分钟学会Rust语言如何操作JSON

JSON(JavaScript Object Notation)在Web开发中被广泛应用于数据交换。作为一种数据格式&#xff0c;JSON相较于XML来说&#xff0c;更易于阅读和写入&#xff0c;且数据解析性能强。Rust作为一门系统级编程语言&#xff0c;其与JSON的交互操作密切。本文将详细地描述在Rust中如…