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使用ANTLR4对Lua脚本语法校验详解

《Java使用ANTLR4对Lua脚本语法校验详解》ANTLR是一个强大的解析器生成器,用于读取、处理、执行或翻译结构化文本或二进制文件,下面就跟随小编一起看看Java如何使用ANTLR4对Lua脚本... 目录什么是ANTLR?第一个例子ANTLR4 的工作流程Lua脚本语法校验准备一个Lua Gramm

Vue3组件中getCurrentInstance()获取App实例,但是返回null的解决方案

《Vue3组件中getCurrentInstance()获取App实例,但是返回null的解决方案》:本文主要介绍Vue3组件中getCurrentInstance()获取App实例,但是返回nu... 目录vue3组件中getCurrentInstajavascriptnce()获取App实例,但是返回n

一文详解如何在Python中从字符串中提取部分内容

《一文详解如何在Python中从字符串中提取部分内容》:本文主要介绍如何在Python中从字符串中提取部分内容的相关资料,包括使用正则表达式、Pyparsing库、AST(抽象语法树)、字符串操作... 目录前言解决方案方法一:使用正则表达式方法二:使用 Pyparsing方法三:使用 AST方法四:使用字

Python列表去重的4种核心方法与实战指南详解

《Python列表去重的4种核心方法与实战指南详解》在Python开发中,处理列表数据时经常需要去除重复元素,本文将详细介绍4种最实用的列表去重方法,有需要的小伙伴可以根据自己的需要进行选择... 目录方法1:集合(set)去重法(最快速)方法2:顺序遍历法(保持顺序)方法3:副本删除法(原地修改)方法4:

使用Python构建一个Hexo博客发布工具

《使用Python构建一个Hexo博客发布工具》虽然Hexo的命令行工具非常强大,但对于日常的博客撰写和发布过程,我总觉得缺少一个直观的图形界面来简化操作,下面我们就来看看如何使用Python构建一个... 目录引言Hexo博客系统简介设计需求技术选择代码实现主框架界面设计核心功能实现1. 发布文章2. 加

python logging模块详解及其日志定时清理方式

《pythonlogging模块详解及其日志定时清理方式》:本文主要介绍pythonlogging模块详解及其日志定时清理方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地... 目录python logging模块及日志定时清理1.创建logger对象2.logging.basicCo

售价599元起! 华为路由器X1/Pro发布 配置与区别一览

《售价599元起!华为路由器X1/Pro发布配置与区别一览》华为路由器X1/Pro发布,有朋友留言问华为路由X1和X1Pro怎么选择,关于这个问题,本期图文将对这二款路由器做了期参数对比,大家看... 华为路由 X1 系列已经正式发布并开启预售,将在 4 月 25 日 10:08 正式开售,两款产品分别为华

JS+HTML实现在线图片水印添加工具

《JS+HTML实现在线图片水印添加工具》在社交媒体和内容创作日益频繁的今天,如何保护原创内容、展示品牌身份成了一个不得不面对的问题,本文将实现一个完全基于HTML+CSS构建的现代化图片水印在线工具... 目录概述功能亮点使用方法技术解析延伸思考运行效果项目源码下载总结概述在社交媒体和内容创作日益频繁的

前端CSS Grid 布局示例详解

《前端CSSGrid布局示例详解》CSSGrid是一种二维布局系统,可以同时控制行和列,相比Flex(一维布局),更适合用在整体页面布局或复杂模块结构中,:本文主要介绍前端CSSGri... 目录css Grid 布局详解(通俗易懂版)一、概述二、基础概念三、创建 Grid 容器四、定义网格行和列五、设置行

Node.js 数据库 CRUD 项目示例详解(完美解决方案)

《Node.js数据库CRUD项目示例详解(完美解决方案)》:本文主要介绍Node.js数据库CRUD项目示例详解(完美解决方案),本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考... 目录项目结构1. 初始化项目2. 配置数据库连接 (config/db.js)3. 创建模型 (models/