Vue.js 数据双向绑定的实现

news2024/11/25 6:40:53

前言

在我们使用vue的时候,当数据发生了改变,界面也会跟着更新,但这并不是理所当然的,我们修改数据的时候vue是如何监听数据的改变以及当数据发生改变的时候vue如何让界面刷新的?

当我们修改数据的时候vue是通过es5中的Object.defineProperty方法来监听数据的改变的,当数据发生了改变通过发布订阅模式统计订阅者界面发生了刷新,这是一种设计模式

下图,从new Vue开始创建Vue实例,会传入el和data,data会被传入一个观察者对象,利用Object.definproperty将data里数据转化成getter/setter进行数据劫持,data里的每个属性都会创建一个Dep实例用来保存watcher实例

而el则传入compile,在compile里进行指令的解析,当解析到el中使用到data里的数据会触发我们的getter,从而将我们的watcher添加到依赖当中。当数据发生改变的时候会触发我们的setter发出依赖通知,通知watcher,watcher接受到通知后去向view发出通知,让view去更新

数据劫持

html部分创建一个id为app的div标签,里面有span和input标签,span标签使用了插值表达式,input标签使用了v-model

<div class="container" id="app"><span>内容:{{content}}</span><input type="text" v-model="content">
</div> 

js部分引入了一个vue.js文件,实现数据双向绑定的代码就写在这里面,然后创建Vue实例vm,把数据挂载到div标签上

const vm=new Vue({el:'#app',data:{content:'请输入开机密码'}
}) 

new了一个Vue实例很明显需要用到构造函数,在vue的源码里定义类是使用了function来定义的,这里我使用ES6的class来创建这个Vue实例

然后设置constructor,形参设为obj_instance,作为new一个Vue实例的时候传入的对象,并把传进来的对象里的data赋值给实例里的$data属性

在javascript里对象的属性发生了变化,需要告诉我们,我们就可以把更改后的属性更新到dom节点里,因此初始化实例的时候定义一个监听函数作为调用,调用的时候传入需要监听的数据

class Vue{//创建Vue实例constructor(obj_instance){this.$data=obj_instance.dataObserver(this.$data)}
}
function Observer(data_instance){//监听函数} 

打印一下这个实例vm

实例已经创建出来了但是还需要为$data里的每一个属性进行监听,要实现数据监听就用到了Object.definePropertyObject.defineProperty可以修改对象的现有属性,语法格式为Object.defineProperty(obj, prop, descriptor)

  • obj:要定义属性的对象
  • prop:要定义或修改的属性的名称
  • descriptor:要定义或修改的属性描述符

监听对象里的每一个属性,我们使用Object.keys和foreach遍历对象里的每一个属性并且对每一个属性使用Object.defineProperty进行数据监听

function Observer(data_instance){Object.keys(data_instance).forEach(key=>{Object.defineProperty(data_instance,key,{enumerable:true,//设置为true表示属性可以枚举configurable:true,//设置为true表示属性描述符可以被改变get(){},//访问该属性的时候触发,get和set函数就是数据监听的核心set(){},//修改该属性的时候触发})})
} 

Object.defineProperty前需要将属性对应的值存起来然后在get函数里面返回出来,不然到了get函数以后属性的值已经没了,返回给属性的值就变成了undefined

let value=data_instance[key]
Object.defineProperty(data_instance,key,{enumerable:true,configurable:true,get(){console.log(key,value);return value},set(){}
}) 

点击一下$data里的属性名就会触发get函数

然后设置set函数,为set设置形参,这个形参表示新传进来的属性值,然后将这个新的属性值赋值给变量value,不需要return返回什么,只做修改,返回是在访问get的时候返回的,修改之后get也会访问最新的value变量值

set(newValue){console.log(key,value,newValue);value = newValue
} 

但是当前只为$data的第一层属性设置了get和set,如果还有更深的一层如

obj:{a:'a',b:'b'
} 

这种的并没有设置get和set,我们需要一层一层的往属性里面进行数据劫持,因此使用递归再次监听自己,并在遍历之前进行条件判断,没有子属性了或者没有检测到对象就终止递归

function Observer(data_instance){//递归出口if(!data_instance || typeof data_instance != 'object') returnObject.keys(data_instance).forEach(key=>{let value=data_instance[key]Observer(value)//递归-子属性的劫持Object.defineProperty(data_instance,key,{enumerable:true,configurable:true,get(){console.log(key,value);return value},set(newValue){console.log(key,value,newValue);value = newValue}})})
} 

还有一个细节,如果我们将$data的content属性从字符串改写成一个对象,这个新的对象并没有get和set

因为我们在修改的时候根本没有设置get和set,因此在set里要调用监听函数

set(newValue){console.log(key,value,newValue);value = newValueObserver(newValue)
} 

模板解析

劫持数据后就要把Vue实例里的数据应用带页面上,得要加一个临时内存区域,将所有数据都更新后再渲染页面以此减少dom操作

创建一个解析函数,设置2个参数,一个是Vue实例里挂载的元素,另一个是Vue实例,在函数里获取获取元素保存在实例了的$el里,获取元素后放入临时内存里,需要用到[createDocumentFragment]创建一个新的空白的文档片段

然后把$el的子节点一个一个加到fragment变量里,页面已经没有内容了,内容都被临时存在fragment里了

class Vue{constructor(obj_instance){this.$data=obj_instance.dataObserver(this.$data)Compile(obj_instance.el,this)}
}
function Compile(ele,vm){vm.$el=document.querySelector(ele)const fragment=document.createDocumentFragment()let child;while (child=vm.$el.firstChild){fragment.append(child)}console.log(fragment);console.log(fragment.childNodes);
} 

现在直接把需要修改的内容应用到文档碎片里面,应用后重新渲染,只需修改了fragment的childNodes子节点的文本节点,文本节点的类型是3,可以创建一个函数并调用来修改fragment里的内容

节点里面可能还会有节点,因此判定节点类型是否为3,不是就递归调用这个解析函数

节点类型为3就进行修改操作,但也不行把整个节点的文本都修改,只需修改插值表达式的内容,因此要使用正则表达式匹配,将匹配的结果保存到变量里,匹配的结果是一个数组,而索引为1的元素才是我们需要提取出来的元素,这个元素就是去除了{{}}和空格得到的字符串,然后就可以直接用Vue实例来访问对应属性的值,修改完后return出去结束递归

function Compile(ele,vm){vm.$el=document.querySelector(ele) //获取元素保存在实例了的$el里const fragment=document.createDocumentFragment() //创建文档碎片let child;while (child=vm.$el.firstChild){//循环将子节点添加到文档碎片里fragment.append(child)}fragment_compile(fragment)function fragment_compile(node){ //修改文本节点内容const pattern = /\{\{\s*(\S*)\s*\}\}/ //检索字符串中正则表达式的匹配,用于匹配插值表达式if(node.nodeType===3){const result = pattern.exec(node.nodeValue)if(result){console.log('result[1]')const value=result[1].split('.').reduce(//split将对象里的属性分布在数组里,链式地进行排列;reduce进行累加,层层递进获取$data的值(total,current)=>total[current],vm.$data)node.nodeValue=node.nodeValue.replace(pattern,value) //replace函数将插值表达式替换成$data里的属性的值}return }node.childNodes.forEach(child=>fragment_compile(child))}vm.$el.appendChild(fragment) //将文档碎片应用到对应的dom元素里面
} 

页面的内容又出来了,插值表达式替换成了vm实例里的数据

订阅发布者模式

虽然进行了数据劫持,和将数据应用到页面上,但是数据发生变动还不能及时更新,还需要实现订阅发布者模式

首先创建一个类用来收集和通知订阅者,生成实例的时候需要有一个数组存放订阅者的信息,一个将订阅者添加到这个数组里的方法和一个通知订阅者的方法,调用这个方法就回去遍历订阅者的数组,让订阅者调用自身的update方法进行更新

class Dependency{constructor(){this.subscribers=[] //存放订阅者的信息}addSub(sub){this.subscribers.push(sub) //将订阅者添加到这个数组里}notify(){this.subscribers.forEach(sub=>sub.update()) //遍历订阅者的数组,调用自身的update函数进行更新}
} 

设置订阅者类,需要用到Vue实例上的属性,需要Vue实例和Vue实例对应的属性和一个回调函数作为参数,将参数都赋值给实例

然后就可以创建订阅者的update函数,在函数里调用传进来的回调函数

class Watcher{constructor(vm,key,callback){//将参数都赋值给Watcher实例this.vm=vmthis.key=keythis.callback=callback}update(){this.callback() }
} 

替换文档碎片内容的时候需要告诉订阅者如何更新,所以订阅者实例在模板解析把节点值替换内容的时候创建,传入vm实例,exec匹配成功后的索引值1和回调函数,将替换文本的执行语句复制到回调函数里,通知订阅者更新的时候就调用这个回调函数

回调函数里的nodeValue要提前保存,不然替换的内容就不是插值表达式而是替换过的内容

然后就要想办法将订阅者存储到Dependency实例的数组里,我们可以在构造Watcher实例的时候保存实例到订阅者数组里

Dependency.temp=this //设置一个临时属性temp 

将新的订阅者添加到订阅者数组里且还要将所有的订阅者都进行同样的操作,那么就可以在触发get的时候将订阅者添加到订阅者数组里,为了正确触发对应的属性get,需要用reduce方法对key进行同样的操作

可以看到控制台打印出了Wathcer实例,每个实例都不同,都对应不同的属性值

Dependency类还没创建实例,里面的订阅者数组是不存在的,所以要先创建实例再将订阅者添加到订阅者数组里

修改数据的时候通知订阅者来进行更新,在set里调用dependency的通知方法,通知方法就会去遍数组,订阅者执行自己的update方法进行数据更新

但是update调用回调函数缺少设定形参,依旧使用split和reduce方法获取属性值

update(){const value =this.key.split('.').reduce((total,current)=>total[current],this.vm.$data)this.callback(value)
} 

在控制台修改属性值都修改成功了,页面也自动更新了

完成了文本的绑定就可以绑定输入框了,在vue里通过v-model进行绑定,因此要判断哪个节点有v-model,元素节点的类型是1,可以使用nodeName来匹配input元素,直接在判断文本节点下面进行新的判断

if(node.nodeType===1&&node.nodeName==='INPUT'){const attr=Array.from(node.attributes)console.log(attr);
} 

节点名字nodeName为v-model,nodeValue为name,就是数据里的属性名

因此对这个数组进行遍历,匹配到了v-model根据nodeValue找到对应的属性值,把属性值赋值到节点上,同时为了在数据更新后订阅者知道更新自己,也要在INPUT节点里新增Watcher实例

attr.forEach(i=>{if(i.nodeName==='v-model'){const value=i.nodeValue.split('.').reduce((total,current)=>total[current],vm.$data)node.value=valuenew Watcher(vm,i.nodeValue,newValue=>{node.value=newValue})}
}) 

修改属性值,页面也作出修改

最后剩下用视图改变数据,在v-model的节点上使用addEventListener增加input监听事件就行了

node.addEventListener('input',e=>{const arr1=i.nodeValue.split('.')const arr2=arr1.slice(0,arr1.length - 1)const final=arr2.reduce((total,current)=>total[current],vm.$data)final[arr1[arr1.length - 1]]=e.target.value
}) 

最后

整理了75个JS高频面试题,并给出了答案和解析,基本上可以保证你能应付面试官关于JS的提问。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

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

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

相关文章

DataGear 制作基于Vue2、Element UI弹窗效果的数据可视化看板

DataGear 在4.4.0版本新增了dg-chart-manual-render特性&#xff0c;用于手动控制看板内图表的渲染&#xff0c;而非在页面加载时自动渲染。利用这一特性&#xff0c;可以很方便制作具有弹窗效果的数据可视化看板。 本文以Vue2、Element UI 前端框架为例&#xff0c;介绍如何制…

【C语言】通讯录—静态版

前言: 刚学完结构体和枚举&#xff0c;在网上找了个通讯录的项目&#xff0c;用来练手 目标:该通讯录可存储千人个人信息 包括:姓名&#xff0c;年龄&#xff0c;性别&#xff0c;电话&#xff0c;地址 实现功能: 1.添加联系人 2.删除联系人 …

Web 安全漏洞之 XSS 攻击

什么是 XSS 攻击 XSS&#xff08;Cross-Site Scripting&#xff09;又称跨站脚本&#xff0c;XSS的重点不在于跨站点&#xff0c;而是在于脚本的执行。XSS是一种经常出现在 Web 应用程序中的计算机安全漏洞&#xff0c;是由于 Web 应用程序对用户的输入过滤不足而产生的。 常…

MyBatis -- 多表查询

MyBatis -- 多表查询一、准备工作二、多表查询属性名与字段名不匹配一、准备工作 博客系统场景。 创建库、表&#xff1a; -- 创建数据库 drop database if exists mycnblog; create database mycnblog DEFAULT CHARACTER SET utf8mb4;-- 使用数据数据 use mycnblog;-- 创建…

小程序样式和组件基础

小程序样式和组件基础小程序样式样式wxss简介响应式单位rpx其它css单位支持的单位内置组件简介image组件-基础组件介绍常用属性示例image组件-懒加载与基础库版本用法swiper组件组件介绍常用属性示例navigator组件-基础组件介绍常用属性示例navigator组件-页面栈页面栈返回按钮…

常用windows自带程序对应的命令

敲winr键&#xff0c;调出 运行 框&#xff1b; 输入命令回车&#xff0c;打开对应程序&#xff1b; 输入cmd&#xff0c;调出命令行窗口&#xff1b; gpedit&#xff0c;组策略编辑器&#xff0c; 有的电脑没有组策略编辑器&#xff1b; 资源管理器&#xff1b; dvdplay&…

【BP靶场portswigger-客户端15】基于DOM的漏洞-7个实验(全)

前言&#xff1a; 介绍&#xff1a; 博主&#xff1a;网络安全领域狂热爱好者&#xff08;承诺在CSDN永久无偿分享文章&#xff09;。 殊荣&#xff1a;CSDN网络安全领域优质创作者&#xff0c;2022年双十一业务安全保卫战-某厂第一名&#xff0c;某厂特邀数字业务安全研究员&…

使用PyTorch构建GAN生成对抗网络源码(详细步骤讲解+注释版)02 人脸识别 上

文章目录1 数据集描述2 GPU设置3 设置Dataset类4 设置辨别器类5 辅助函数与辅助类1 数据集描述 此项目使用的是著名的celebA&#xff08;CelebFaces Attribute&#xff09;数据集。其包含10,177个名人身份的202,599张人脸图片&#xff0c;每张图片都做好了特征标记&#xff0c…

【数据结构】顺序表的原理及实现

1.什么是顺序表 顺序表是用一段物理地址连续的存储单元进行存储元素的线性结构&#xff0c;通常是以数组进行存储。通过数据元素物理存储的相邻关系来反映数据元素之间逻辑上的相邻关系。 2.顺序表的实现 判断顺序表是否为空表public boolean isEmpty()判断顺序表是否满publi…

复旦MBA海外短期课程 | 善用ESG金融,共创可持续未来

2022年&#xff0c;世界在颠簸中向前迈进&#xff1a;全球气候变化、能源危机、大国博弈……在这样的背景下&#xff0c;近年来备受瞩目的ESG价值、“双碳”目标、可持续发展、责任投资等话题愈发成为焦点。今年复旦MBA培养体系全面升级之际&#xff0c;在新增的“未来发展模块…

Pandas CSV 文件

Pandas CSV 文件CSV&#xff08;Comma-Separated Values&#xff0c;逗号分隔值&#xff0c;有时也称为字符分隔值&#xff0c;因为分隔字符也可以不是逗号&#xff09;&#xff0c;其文件以纯文本形式存储表格数据&#xff08;数字和文本&#xff09;。CSV 是一种通用的、相对…

【沐风老师】3dMax一键生成中央空调排风口插件使用教程

3dMax一键创建中央空调排风口插件&#xff0c;快捷生成矩形或菱形两种形状的排风口。 【版本要求】 不确定。3dmax2020环境测试可用&#xff0c;其他版本自行测试&#xff0c;欢迎反馈测试结果。 【安装方法】 方法一&#xff1a;拖动插件文件到3dMax窗口打开。 方法二&#x…

ESP32相关知识点

1.vs code代码回退到上一步: 方法1:在Windows中可以使用快捷键“Alt←”实现 方法2:利用vs code界面操作&#xff0c;Go-Back 2.设置vs code下ESP-IDF Monitor Device的波特率。 步骤Manage-New Code update available------Command Palette 弹出对话框 搜索或选择:ESP-IDF:SD…

基于Androidstudio的打车拼车app

需求信息&#xff1a; 客户端&#xff1a; 1&#xff1a;注册和登录:用户信息录入与登录。 2&#xff1a;修改密码:用户忘记密码可在此处找回。 3&#xff1a;地图功能:提供城市地图&#xff0c;能被自由移动、放大和缩小。 4&#xff1a;自身定位:获取用户位置信息&#xff0c…

STM-PWM采集

输入捕获分为两种方式进行捕获 1、pwm输入捕获:精度高&#xff0c;每个定时器只能采集一个pwm&#xff0c;且只能使用通道1、通道2。 2、通用输入捕获&#xff1a;相对比较精确&#xff0c;每个定时器可以采集多个pwm&#xff0c; 1、pwm输入捕获使用教程如下&#xff1a; 参…

Vue 快速入门(一)

1、介绍 Vue(读音/vju/&#xff0c;类似view)&#xff0c;是中国的大神尤雨溪开发的&#xff0c;为数不多的国人开发的世界顶级开源软件。是一套用于构建用户界面的渐进式框架&#xff0c;Vue 被设计为可以自底向上逐层应用。MVVM响应式编程模型&#xff0c;避免直接操作DOM&am…

虹科分享 | HPC调度解决方案:HK-Adaptive在数字卫星图像领域的应用

2011年3月11日&#xff0c;日本海岸附近发生了9.0级地震。这次地震引发了强大的海啸&#xff0c;并向内陆传播了6英里&#xff0c;不仅使地球的轴心偏移了大约10到25厘米&#xff0c;还导致福岛核电站发生核紧急情况。 为了减少这场灾害的损失&#xff0c;不同国家的不同组织以…

浅谈虚拟电厂与企业微电网数字化建设

安科瑞 华楠摘要&#xff1a;2023年1月8日&#xff0c;微信公众号鱼眼看电改(作者俞庆)发表了文章《虚拟电厂与负荷侧数字化》&#xff0c;原文如下&#xff1a;“虚拟电厂是电力数字化的一个应用方向&#xff0c;准确的说&#xff0c;是负荷侧数字化的发展方向。所以负荷侧数字…

NetSuite Classifications的注意事项

Classifications&#xff0c;我们将之称为“分析维度”吧&#xff0c;这样更能体现其真正的用途。在NetSuite中标准的分析维度是“LDC&#xff08;Location、Department、Class&#xff09;”&#xff0c;这是NetSuite的特色。有些注意事项我们今天分享下。 1. LDC的用途区别 …

Django REST framework--Swagger API文档生成器

Django REST framework--Swagger API文档生成器swagger在线接口文档drf-yasg安装与配置安装drf-yasg配置drf-yasg互动模式文档模式定制化用法&#xff08;viewset模式&#xff09;修饰视图装饰器api_view修饰视图集swagger在线接口文档 目前为止&#xff0c;接口开发到了一定的…