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

相关文章

MySQL数据库双机热备的配置方法详解

《MySQL数据库双机热备的配置方法详解》在企业级应用中,数据库的高可用性和数据的安全性是至关重要的,MySQL作为最流行的开源关系型数据库管理系统之一,提供了多种方式来实现高可用性,其中双机热备(M... 目录1. 环境准备1.1 安装mysql1.2 配置MySQL1.2.1 主服务器配置1.2.2 从

Linux kill正在执行的后台任务 kill进程组使用详解

《Linuxkill正在执行的后台任务kill进程组使用详解》文章介绍了两个脚本的功能和区别,以及执行这些脚本时遇到的进程管理问题,通过查看进程树、使用`kill`命令和`lsof`命令,分析了子... 目录零. 用到的命令一. 待执行的脚本二. 执行含子进程的脚本,并kill2.1 进程查看2.2 遇到的

MyBatis常用XML语法详解

《MyBatis常用XML语法详解》文章介绍了MyBatis常用XML语法,包括结果映射、查询语句、插入语句、更新语句、删除语句、动态SQL标签以及ehcache.xml文件的使用,感兴趣的朋友跟随小... 目录1、定义结果映射2、查询语句3、插入语句4、更新语句5、删除语句6、动态 SQL 标签7、ehc

详解SpringBoot+Ehcache使用示例

《详解SpringBoot+Ehcache使用示例》本文介绍了SpringBoot中配置Ehcache、自定义get/set方式,并实际使用缓存的过程,文中通过示例代码介绍的非常详细,对大家的学习或者... 目录摘要概念内存与磁盘持久化存储:配置灵活性:编码示例引入依赖:配置ehcache.XML文件:配置

从基础到高级详解Go语言中错误处理的实践指南

《从基础到高级详解Go语言中错误处理的实践指南》Go语言采用了一种独特而明确的错误处理哲学,与其他主流编程语言形成鲜明对比,本文将为大家详细介绍Go语言中错误处理详细方法,希望对大家有所帮助... 目录1 Go 错误处理哲学与核心机制1.1 错误接口设计1.2 错误与异常的区别2 错误创建与检查2.1 基础

k8s按需创建PV和使用PVC详解

《k8s按需创建PV和使用PVC详解》Kubernetes中,PV和PVC用于管理持久存储,StorageClass实现动态PV分配,PVC声明存储需求并绑定PV,通过kubectl验证状态,注意回收... 目录1.按需创建 PV(使用 StorageClass)创建 StorageClass2.创建 PV

vite搭建vue3项目的搭建步骤

《vite搭建vue3项目的搭建步骤》本文主要介绍了vite搭建vue3项目的搭建步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学... 目录1.确保Nodejs环境2.使用vite-cli工具3.进入项目安装依赖1.确保Nodejs环境

Python版本信息获取方法详解与实战

《Python版本信息获取方法详解与实战》在Python开发中,获取Python版本号是调试、兼容性检查和版本控制的重要基础操作,本文详细介绍了如何使用sys和platform模块获取Python的主... 目录1. python版本号获取基础2. 使用sys模块获取版本信息2.1 sys模块概述2.1.1

Nginx搭建前端本地预览环境的完整步骤教学

《Nginx搭建前端本地预览环境的完整步骤教学》这篇文章主要为大家详细介绍了Nginx搭建前端本地预览环境的完整步骤教学,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录项目目录结构核心配置文件:nginx.conf脚本化操作:nginx.shnpm 脚本集成总结:对前端的意义很多

一文详解Python如何开发游戏

《一文详解Python如何开发游戏》Python是一种非常流行的编程语言,也可以用来开发游戏模组,:本文主要介绍Python如何开发游戏的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录一、python简介二、Python 开发 2D 游戏的优劣势优势缺点三、Python 开发 3D