Vue Object.defineProperty发布订阅模式架构详解

本文主要是介绍Vue Object.defineProperty发布订阅模式架构详解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Object.defineProperty发布订阅模式架构详解

1、 Object.defineProperty定义
Object.defineProperty(Object,prop,descriptor)

参数:
obj:必需,目标对象
prop:必需,目标对象 需获取或者修改的属性的名字
descriptor:必需,目标对象 属性所拥有的特性

简单的实例:

let data = {}// 采用中间变量去改变值_name = 1Object.defineProperty(data,'name',{get(){console.log("你访问了name属性");return _name},set(row){console.log("你设置修改了name属性,最新值为"+row);_name = row}})

输出
在这里插入图片描述

Object.defineProperty 属性详解:
  • writable:是否可以被重写,true可以重写,false不能重写,默认为false。
  • enumerable:是否可以被枚举(使用for…in或Object.keys( ))。设置为true可以被枚举;设置为false,不能被枚举。默认为false。
  • value:值可以使任意类型的值,默认为undefined
  • configurable:是否可以删除目标属性或是否可以再次修改属性的特性(writable, configurable, enumerable)。设置为true可以被删除或可以重新设置特性;设置为false,不能被可以被删除或不可以重新设置特性。默认为false。

一个简单的实例

let data= {}; 
Object.defineProperty(data, 'name', writable: true || false,configurable: true || false,enumerable: true || false,value:'gjf'
});

当然也还有我们上面存取器描述(set和get)

注意:当使用了getter或setter方法,不允许使用writable和value这两个属性
如果一起使用将会报错


Object.defineProperty更加通用的劫持方案
 // vue2 中的 Object.defineProperty 劫持的属性  vue3中的 porxy 劫持的是一个对象整体+惰性处理// 更加通用的劫持方案使用// 在vue开发中,都是提前把数据放到data配置中,// data(){//     return{//         name:"cp",//         height:120//     }// }let data={name:'cp',height:100,age:20}// 一般情况下,响应式数据很多,并且对象的属性很多,不止一个,// 对象的遍历方法Object.keys,遍历出来的对象是一个数组,所以我们可以使用foreachObject.keys(data).forEach(item=>{console.log(data[item]);defineReactive(data,item,data[item])// item代表data的每一个对象的属性// data[item]代表每一个对象属性的value// data是一个原对象// 处理每一个对象的key转变成响应式})// 把对象的属性改成get和set的形式// 1、函数定义形参相当于在内部申明了和形参名字对应的变量,并且初始值为undefined// 2、函数调用传入的实参的时候,相当于给内部申明好的变量做了赋值操作(首次遍历举例)// 3、defineReactive函数调用完毕,本应该内部遍历都会被回收,但是如果内部有其他函数使用了当前变量形成了闭包,不会被收回// 4、内部由于有其他方法引用了val属性,所以defineReactive函数的执行并不会导致val变量的销毁,会一直常驻内存// 5、由于闭包的特性,每一个传入下来的val都会常驻内存,相当于一个中间变量,目的是为了set和get的联动function defineReactive(data,item,val) {// 进行转换操作Object.defineProperty(data,item,{get(){console.log("你访问了属性"+item);return val},set(row){console.log("你修改了属性"+item+",最新值为"+row);val = row},})}// 形成了独立的作用域,互不干扰
实现 M->V的命令式操作
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>object.defineProperty</title>
</head><body><div id="app"><p v-text="name"></p></div>
</body>
<script>// 1、准备数据let data = {name: "1"}document.querySelector("#app p").innerHTML = data.name// 2、将数据转换为响应式(数据发生改变之后操作我们的视图)Object.keys(data).forEach(item => {defineReactive(data, item, data[item])})function defineReactive(data, item, val) {Object.defineProperty(data, item, {get() {console.log("你访问了属性" + item);return val},set(row) {if(row  === val){return}console.log("你修改了属性" + item + ",最新值为" + row);val = row// 这里我们把最新的值反应到视图中 关键位置// 核心就是操作DOM 把最新的值设置上去document.querySelector("#app p").innerHTML = val},})}
</script>
</html>

命令式操作就是操作了DOM

实现 M->V的 声明式的指令版本 (v-text)
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>object.defineProperty</title>
</head><body><div id="app"><p v-text="name"></p></div>
</body>
<script>let data = {name: 'a',}// 实现指令的核心:不管是指令也好,还是差值表达式也好,他们都是数据和视图之间建立关联的“标识”,所以本质就是通过一定的手段找到标识的Dom元素,// 然后把数据放上去,每当数据发生变化,就更新执行一遍放置数据的操作function compile() {let app = document.querySelector('#app')// 拿到app下的所有节点const childNodes = app.childNodes //所以的节点,包括文本节点,标签节点等// nodetype是节点标识 我们通过nodetype来筛选,标签节点为1 文本节点为3childNodes.forEach(item =>{if(item.nodeType === 1){// 拿到标签属性const attrs = item.attributesArray.from(attrs).forEach(arrts =>{const nodeName = arrts.nodeNameconst nodeValue = arrts.nodeValue// console.log(nodeName,nodeValue);// nodeName -> v-text 就是我们需要找到的标识// nodeValue -> name  就我我们数据中的Keyif(nodeName === "v-text"){item.innerText = data[nodeValue]}})}})}Object.keys(data).forEach(item => {defineReactive(data, item, data[item])})function defineReactive(data, item, val) {Object.defineProperty(data, item, {get() {console.log("你访问了属性" + item);return val},set(row) {if(row  === val){return}console.log("你修改了属性" + item + ",最新值为" + row);val = rowcompile()},})}compile()
</script>
</html>
实现V->M(视图反应到数据)

我们在上面的代码基础上添加了(v-model)标签

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><div id="app"><p v-text="name"></p><input type="text" v-model="name"></div>
</body>
<!-- V->M  v-model() M->V  V->M-->
<script>let data = {name: 'a',}function compile() {let app = document.querySelector('#app')const childNodes = app.childNodes childNodes.forEach(item =>{if(item.nodeType === 1){const attrs = item.attributesArray.from(attrs).forEach(arrts =>{const nodeName = arrts.nodeNameconst nodeValue = arrts.nodeValueif(nodeName === "v-text"){item.innerText = data[nodeValue]}if(nodeName ==="v-model"){item.value = data[nodeValue]// 监听input事件,在数据回调中拿到最新的数值,赋值给绑定的属性item.addEventListener('input',(e)=>{let newValue = e.target.value// 反向赋值data[nodeValue] = newValue})}})}})}Object.keys(data).forEach(item => {defineReactive(data, item, data[item])})function defineReactive(data, item, val) {Object.defineProperty(data, item, {get() {console.log("你访问了属性" + item);return val},set(row) {if(row  === val){return}console.log("你修改了属性" + item + ",最新值为" + row);val = rowcompile()},})}compile()
</script>
</html>

这样就实现了双向绑定的效果
不过这样的架构有一点问题,就是我们修改一个数据的时候,没有更改值的属性也会进行更改,如下我们多添加一个值

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body><div id="app"><p v-text="name"></p><div v-text="name"></div><span v-text="age"></span><input type="text" v-model="age"></div>
</body>
<script>let data = {name: 'a',age: 25}function compile() {let app = document.querySelector('#app')const childNodes = app.childNodeschildNodes.forEach(item => {if (item.nodeType === 1) {const attrs = item.attributesArray.from(attrs).forEach(arrts => {const nodeName = arrts.nodeNameconst nodeValue = arrts.nodeValueif (nodeName === "v-text") {console.log('修改了'+nodeValue);item.innerText = data[nodeValue]}if (nodeName === "v-model") {item.value = data[nodeValue]item.addEventListener('input', (e) => {let newValue = e.target.valuedata[nodeValue] = newValue})}})}})}Object.keys(data).forEach(item => {defineReactive(data, item, data[item])})function defineReactive(data, item, val) {Object.defineProperty(data, item, {get() {console.log("你访问了属性" + item);return val},set(row) {if (row === val) {return}console.log("你修改了属性" + item + ",最新值为" + row);val = rowcompile()},})}compile()
</script></html>

在这里我只在输入框中修改了age属性
在这里插入图片描述
但是他的name属性打印出了修改,所以我们要进行优化代码,利用订阅模式进行针对性的修改

利用订阅模式优化架构

需要理解订阅模式可以访问这个博客:链接: 发布订阅模式与观察者模式.
我们先了解一下浏览器的一对一和一对多的事件

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>发布订阅模式</title>
</head><body><button id="btn">click me!</button>
</body>
<script>const btn = document.querySelector("#btn")// 一对一// btn.onclick = function () {//     console.log("点击了按钮");// }// btn.onclick = function () {//     console.log("今天好热")// }// 一对多   实际上就是使用了发布订阅模式// btn.addEventListener("click",()=>{//     console.log("点击了按钮");// })// btn.addEventListener("click",()=>{//     console.log("今天好热");// })// 浏览器的实现过程分析:// 1、浏览器实现了一个方法 叫做:addEventlistener// 2、这个方法接收2个参数 参数1代表 事件类型 参数2代表回调函数// 3、为了实现一对多架构// {//     click:['回调函数1','回调函数2',...]// }// 4、当鼠标点击的时候,通过事件类型click去数据结构中找到存放了所有相关回调函数的数组,然后都遍历,都执行一遍,从而实现一对多/*实现一个自己的自定义事件收集和触发架构1、定义一个方法,接收两个参数,参数1为事件名称 参数2为回调函数只要方法一执行 就收集回调函数到对应的位置上去*/const map = {}function collect(eventName, fn) {//    如果当前map中已经初始化好了, click:[]//     就直接往里面push 如果没有初始化首次添加  就先进行初始化if (!map[eventName]) {map[eventName] = []}map[eventName].push(fn)}collect('cp',()=>{console.log("成功收集了cp");})collect('cp',()=>{console.log("再一次成功收集了cp");})console.log(map);// 以上完成了事件的收集工作,完成了一对多的存储架构// 事件触发函数// 模拟鼠标点击 主动通过程序去触发收集起来的事件// 需要通过事件名称 找到对应的回调函数数组 然后遍历执行function trigger (eventName){map[eventName].forEach(fn =>fn()    );}trigger('cp')
</script></html>

以上的js代码我们可以优化一下 把函数放到对象里面去

let Dep = {map: {},collect(eventName, fn) {if (!this.map[eventName]) { //this的指向为Depthis.map[eventName] = []}this.map[eventName].push(fn)},trigger(eventName) {this.map[eventName].forEach(fn => fn());}}

然后我们全部结合起来

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body><div id="app"><p v-text="name"></p><div v-text="name"></div><span v-text="age"></span></div>
</body>
<script>let Dep = {map: {},collect(eventName, fn) {if (!this.map[eventName]) {this.map[eventName] = []}this.map[eventName].push(fn)},trigger(eventName) {this.map[eventName].forEach(fn => fn());}}let data = {name: 'a',age: 25}function compile() {let app = document.querySelector('#app')const childNodes = app.childNodeschildNodes.forEach(item => {if (item.nodeType === 1) {const attrs = item.attributesArray.from(attrs).forEach(arrts => {const nodeName = arrts.nodeNameconst nodeValue = arrts.nodeValueif (nodeName === "v-text") {console.log('修改了' + nodeValue);item.innerText = data[nodeValue]// 收集更新函数Dep.collect(nodeValue, () => {item.innerText = data[nodeValue]})}if (nodeName === "v-model") {item.value = data[nodeValue]item.addEventListener('input', (e) => {let newValue = e.target.valuedata[nodeValue] = newValueDep.collect(nodeValue, () => {item.innerText = data[nodeValue]})})}})}})}Object.keys(data).forEach(item => {defineReactive(data, item, data[item])})function defineReactive(data, item, val) {Object.defineProperty(data, item, {get() {console.log("你访问了属性" + item);return val},set(row) {if (row === val) {return}console.log("你修改了属性" + item + ",最新值为" + row);val = row// compile()// 在这里进行精准更新 ->找到data对应的属性名找到对应的更新函数依次执行Dep.trigger(item)},})}compile()console.log(Dep);
</script></html>

这样我们就完成了 看效果:
在这里插入图片描述

这篇关于Vue Object.defineProperty发布订阅模式架构详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/891593

相关文章

Java利用@SneakyThrows注解提升异常处理效率详解

《Java利用@SneakyThrows注解提升异常处理效率详解》这篇文章将深度剖析@SneakyThrows的原理,用法,适用场景以及隐藏的陷阱,看看它如何让Java异常处理效率飙升50%,感兴趣的... 目录前言一、检查型异常的“诅咒”:为什么Java开发者讨厌它1.1 检查型异常的痛点1.2 为什么说

MySQL的配置文件详解及实例代码

《MySQL的配置文件详解及实例代码》MySQL的配置文件是服务器运行的重要组成部分,用于设置服务器操作的各种参数,下面:本文主要介绍MySQL配置文件的相关资料,文中通过代码介绍的非常详细,需要... 目录前言一、配置文件结构1.[mysqld]2.[client]3.[mysql]4.[mysqldum

springboot2.1.3 hystrix集成及hystrix-dashboard监控详解

《springboot2.1.3hystrix集成及hystrix-dashboard监控详解》Hystrix是Netflix开源的微服务容错工具,通过线程池隔离和熔断机制防止服务崩溃,支持降级、监... 目录Hystrix是Netflix开源技术www.chinasem.cn栈中的又一员猛将Hystrix熔

Java调用Python脚本实现HelloWorld的示例详解

《Java调用Python脚本实现HelloWorld的示例详解》作为程序员,我们经常会遇到需要在Java项目中调用Python脚本的场景,下面我们来看看如何从基础到进阶,一步步实现Java与Pyth... 目录一、环境准备二、基础调用:使用 Runtime.exec()2.1 实现步骤2.2 代码解析三、

python之uv使用详解

《python之uv使用详解》文章介绍uv在Ubuntu上用于Python项目管理,涵盖安装、初始化、依赖管理、运行调试及Docker应用,强调CI中使用--locked确保依赖一致性... 目录安装与更新standalonepip 安装创建php以及初始化项目依赖管理uv run直接在命令行运行pytho

Springboot项目构建时各种依赖详细介绍与依赖关系说明详解

《Springboot项目构建时各种依赖详细介绍与依赖关系说明详解》SpringBoot通过spring-boot-dependencies统一依赖版本管理,spring-boot-starter-w... 目录一、spring-boot-dependencies1.简介2. 内容概览3.核心内容结构4.

MySQL中REPLACE函数与语句举例详解

《MySQL中REPLACE函数与语句举例详解》在MySQL中REPLACE函数是一个用于处理字符串的强大工具,它的主要功能是替换字符串中的某些子字符串,:本文主要介绍MySQL中REPLACE函... 目录一、REPLACE()函数语法:参数说明:功能说明:示例:二、REPLACE INTO语句语法:参数

redis数据结构之String详解

《redis数据结构之String详解》Redis以String为基础类型,因C字符串效率低、非二进制安全等问题,采用SDS动态字符串实现高效存储,通过RedisObject封装,支持多种编码方式(如... 目录一、为什么Redis选String作为基础类型?二、SDS底层数据结构三、RedisObject

springboot整合mqtt的步骤示例详解

《springboot整合mqtt的步骤示例详解》MQTT(MessageQueuingTelemetryTransport)是一种轻量级的消息传输协议,适用于物联网设备之间的通信,本文介绍Sprin... 目录1、引入依赖包2、yml配置3、创建配置4、自定义注解6、使用示例使用场景:mqtt可用于消息发

Python Flask实现定时任务的不同方法详解

《PythonFlask实现定时任务的不同方法详解》在Flask中实现定时任务,最常用的方法是使用APScheduler库,本文将提供一个完整的解决方案,有需要的小伙伴可以跟随小编一起学习一下... 目录完js整实现方案代码解释1. 依赖安装2. 核心组件3. 任务类型4. 任务管理5. 持久化存储生产环境