vue进阶一~数据响应式,数据响应到视图层,手写v-model,订阅发布者模式,
1,数据响应式
当数据发生改变的时候,我们立即知道数据发生改变,并做出相关的操作:发送请求,打印文字,操作DOM等。
1.1,vue实现数据响应的原理
vue中使用了两种模式来实现数据响应式,分别是vue2(Object.defineProperty ()),vue3(Proxy)。
1.2,vue2实现数据响应:对象属性拦截Object.defineProperty ()
首先我们先要知道如何多一个对象进行赋值:两种方式,字面量赋值和Object.defineProperty ()
A:字面量赋值
我们定义一个数据的时候,通过字面值的数据进行定义:
<script>
let obj={
name:"小智"
}
obj.name="小红"
let app=document.getElementById('app')
console.log(obj);
app.innerText=obj.name
</script>
这时我们在控制台上看到好像是可以知道obj属性的变化的,我们再在控制台上打印:
obj的name属性是小红,我们再修改name属性值。
B:Object.defineProperty () 方法拦截对象属性
JavaScript提供了Object.defineProperty () 方法拦截对象属性。
let obj={}
let _name="小红"
Object.defineProperty(obj,'name',{
// 当访问对象的属性的时候,get方法被调用
get(value){
return _name
},
// 当修改对象属性的时候被调用
set(newValue){
_name=newValue
}
})
_name属性是一个中间变量,用于解决get/set联动问题。
c:强化属性劫持方案
一般情况下,我们需要将一个个变量都放到一个对象中,这时候实现Object.defineProperty ()赋值的时候,中间变量会变得有点多,这时候我们需要对其进行优化处理:
let data={
name:"小智",
age:12,
sex:"男"
}
Object.keys(data).forEach(key=>{
Observer(data,key,data[key])
console.log(key+data[key]);
})
function Observer(obj,key,value){
Object.defineProperty(obj,key,{
get(){
return value
},
set(Newvalue){
value=Newvalue
}
})
}
总结:响应式是指拦截数据的访问和设置,并对其做一些操作。
2,数据响应式反应到视图
我们已经可以实现数据响应式,拦截在数据设置和访问的状态了,例如:
let data={
name:"小吴",
age:16
}
Object.keys(data).forEach(key=>{
webserver(data,key,data[key])
})
function webserver(obj,key,value){
Object.defineProperty(obj,key,{
get(){
return value+'vip'
},
set(newValue){
value=newValue+'vip'
}
})
}
我们对对象属性设置和访问都进行拦截,并假设“vip”字符串,在控制台打印一下会发现:
我们只进行初始化没有进行任何后续的赋值都会在name和age后字段加‘vip’字符串,这个就是属性响应式的原理。
问题:这里发现一个有趣的问题,我本来就喜欢将功能写在一起,但是这里就导致了一个问题,问题代码如下:
let data={
name:"小吴",
age:16
}
Object.keys(data).forEach(key=>{
Object.defineProperty(data,key,{
get(){
return data[key]
},
set(newValue){
data[key]=newValue
}
})
})
在控制台打印。属性发生以下错误:
开始不知道为什么,后来才知道,这是作用域的问题,响应式是一个异步的过程,而forEach是一个同步的过程,在循环结束之后,才执行Object.defineProperty内的代码,解决方法有两个,一个是定义外部状态,形成闭包,代码如下:
let data={
name:"小吴",
age:16
}
Object.keys(data).forEach(key=>{
// webserver(data,key,data[key])
let obj=data
let item=key
let value=data[key]
Object.defineProperty(obj,item,{
get(){
return value
},
set(newValue){
value=newValue
}
})
})
另一种就是封装到一个方法里面,我们知道,方法的形参是可以提升的,以上封装方法的方式提示相当一下代码:
let data={
name:"小吴",
age:16
}
Object.keys(data).forEach(key=>{
webserver(data,key,data[key])
})
function webserver(obj,key,value){
let obj
let key
let value
Object.defineProperty(obj,key,{
get(){
return value+'vip'
},
set(newValue){
value=newValue+'vip'
}
})
}
这样在Object.defineProperty内部形成一个闭包。
2.1.数据反映到视图层---编程式响应
之前我们已经实现类数据响应式,那如何将数据响应到视图呢?说过一句话,响应式是用来拦截数据的设置和读取的,也就是说,在设置之前(set方法里面),我们是可以拿到数据的,只要将拿到的数据添加到DOM节点上就可以了:
let data={
name:"小吴",
age:16
}
let app=document.getElementById('app')
app.innerText=data.name
Object.keys(data).forEach(key=>{
webserver(data,key,data[key])
})
function webserver(obj,key,value){
Object.defineProperty(obj,key,{
get(){
return value
},
set(newValue){
if(newValue===value){//如果设置的值和原来的值相等,不执行赋值代码
return
}
app.innerText=newValue
value=newValue
}
})
}
这个是单数据响应,多数据响应处理起来还是比较麻烦,处理思路为:(等有空再说,会补坑的)
A:双向数据绑定
思路:给input绑定一个内容发生变化就会触发的一个函数,通过该函数改变变量实现双向数据绑定。
<body>
<div id="app"></div>
<!-- <div id="app1"></div> -->
<input type="text" oninput="change()" id="text">
<script>
let text=document.getElementById('text')
let app=document.getElementById('app')
let data="小美"
app.innerText=data
text.value=data
function change(data){
data=text.value
app.innerText=data
}
</script>
</body>
2.2.数据反映到视图层(数据双向绑定)
将数据响应到视图层的方式,在vue2使用的是Object.defineProperty()来实现,需要劫持到数据的变化,在数据变化的时候将其渲染到视图上:
第一步:数据层到视图层的响应
A,首先需要遍历data的数据,获取到每个data的属性
B,其次,获取到data的每个属性之后,对属性进行拦截。
C,在拦截时,需要将其值反映到对应的视图标签上。
第C步中又可以分为:
a,获取到视图中每个节点的DOM对象,
b,遍历DOM对象,获取到每个DOM对象对应的node对象(节点对象)。
c,遍历node对象,获取到node对象中含有v-model属性的节点
d,将对应的data的属性值赋值给node节点的value值上
代码实现为:
<body>
<div id="app">
名字:<input type="text" v-model="name" value="12"><br>
年龄:<input type="text" v-model="age" value="2323">
<div class="sjkx">
<h1 class="name">模拟v-model</h1>
</div>
</div>
<script>
let data = {
name: '',
age: '',
}
let view = document.getElementById('app')
// 数据反映到视图方法
function getVmodelNode(view, data) {
// a,获取到视图中每个节点的DOM对象,
let allDom = view.getElementsByTagName("*")
let allDomArray = Array.from(allDom)//将伪数组转化成数组
let attributeArray = []
// b,遍历DOM对象,获取到每个DOM对象对应的node对象。
allDomArray.forEach(nodeItem => {
//c, 遍历node对象,过滤获取到node对象中含有v-model的节点
Array.from(nodeItem.attributes).forEach(item => {
if (item.nodeName === 'v-model') {
//d,将对应的data的属性值赋值给node节点的value值上
nodeItem.value = data[item.nodeValue]
}
})
})
}
// A,首先需要遍历data的数据,获取到每个data的属性
Object.keys(data).forEach(key => {
// B,其次,获取到data的每个属性之后,对属性进行拦截。
debugger
WebServer(data,key,data[key])
})
function WebServer(data, key, value) {
Object.defineProperty(data, key, {
get() {
return value
},
set(newValue) {
value= newValue
// C,在拦截时,需要将其值反映到对应的视图标签上。
getVmodelNode(view,data)
}
})
}
</script>
</body>
第二步:视图层到数据层的响应。
视图层响应到数据层的思路:当输入框的数据发生变化的时候,调用函数,将输入框的值赋值给data对应的属性上。
实际上就是在第一步的第C步多加了一步:为对应的node对象绑定一个input事件,并将node的value赋值给data对应的属性上。
<body>
<div id="app">
名字:<input type="text" v-model="name" value="12"><br>
年龄:<input type="text" v-model="age" value="2323">
<div class="sjkx">
<h1 class="name">模拟v-model</h1>
</div>
</div>
<script>
let data = {
name: '',
age: '',
}
let view = document.getElementById('app')
// 数据反映到视图方法
function getVmodelNode(view, data) {
// a,获取到视图中每个节点的DOM对象,
let allDom = view.getElementsByTagName("*")
let allDomArray = Array.from(allDom)//将伪数组转化成数组
let attributeArray = []
// b,遍历DOM对象,获取到每个DOM对象对应的node对象。
allDomArray.forEach(nodeItem => {
//c, 遍历node对象,过滤获取到node对象中含有v-model的节点
Array.from(nodeItem.attributes).forEach(item => {
if (item.nodeName === 'v-model') {
//d,将对应的data的属性值赋值给node节点的value值上
nodeItem.value = data[item.nodeValue]
//视图层反映到数据层上,
nodeItem.addEventListener('input',(e)=>{
data[item.nodeValue]=e.target.value
})
}
})
})
}
// A,首先需要遍历data的数据,获取到每个data的属性
Object.keys(data).forEach(key => {
// B,其次,获取到data的每个属性之后,对属性进行拦截。
WebServer(data,key,data[key])
})
function WebServer(data, key, value) {
Object.defineProperty(data, key, {
get() {
return value
},
set(newValue) {
value= newValue
// C,在拦截时,需要将其值反映到对应的视图标签上。
getVmodelNode(view,data)
}
})
}
//视图响应到数据层
getVmodelNode(view, data)
</script>
</body>
这就是完整的模拟v-model的代码了。我们还可以通过这个代码实现v-text,只需要加个判断就可以了
<body>
<div id="app">
名字:<input type="text" v-model="name" value="12"><br>
年龄:<input type="text" v-model="age" value="2323">
<div class="sjkx">
<h1 class="name" >模拟v-model</h1>
<p v-text="text"> </p>
</div>
</div>
<script>
let data = {
name: '',
age: '',
text:'hHHHHHHH'
}
let view = document.getElementById('app')
// 数据反映到视图方法
function getVmodelNode(view, data) {
// a,获取到视图中每个节点的DOM对象,
let allDom = view.getElementsByTagName("*")
let allDomArray = Array.from(allDom)//将伪数组转化成数组
let attributeArray = []
// b,遍历DOM对象,获取到每个DOM对象对应的node对象。
allDomArray.forEach(nodeItem => {
//c, 遍历node对象,过滤获取到node对象中含有v-model的节点
Array.from(nodeItem.attributes).forEach(item => {
// 实现v-text核心代码
if (item.nodeName === 'v-text') {
console.log("haha",data[item.nodeValue]);
//d,将对应的data的属性值赋值给node节点的value值上
nodeItem.innerText = data[item.nodeValue]
}
//实现v-model核心代码
console.log(item.nodeName);
if (item.nodeName === 'v-model') {
//d,将对应的data的属性值赋值给node节点的value值上
nodeItem.value = data[item.nodeValue]
nodeItem.addEventListener('input',(e)=>{
let nodeValue=e.target.value
data[item.nodeValue]=nodeValue
})
}
})
})
}
// A,首先需要遍历data的数据,获取到每个data的属性
Object.keys(data).forEach(key => {
// B,其次,获取到data的每个属性之后,对属性进行拦截。
WebServer(data,key,data[key])
})
function WebServer(data, key, value) {
Object.defineProperty(data, key, {
get() {
return value
},
set(newValue) {
console.log("被调用了");
value= newValue
// C,在拦截时,需要将其值反映到对应的视图标签上。
getVmodelNode(view,data)
}
})
}
//视图响应到数据层
getVmodelNode(view, data)
</script>
</body>
现存问题:这里现存一个问题,就是在执行v-model核心代码的时候,我们只是输入一个值,但是v-model的核心代码块执行了两次,这不是我们想要的,而是应该我只输入一个值得时候,另一个值得v-model代码块不应该被执行
这时候需要引入一个优化点——精确更新
因为我们在执行的时候,执行到
if (item.nodeName === 'v-model') {
//d,将对应的data的属性值赋值给node节点的value值上
nodeItem.value = data[item.nodeValue]
nodeItem.addEventListener('input',(e)=>{
let nodeValue=e.target.value
data[item.nodeValue]=nodeValue
})
}
这个代码块的时候,我们都需要操作nodeItem和item.nodeValue 而nodeItem 是我们需要操作的DOM节点,item.nodeValue 是我们需要赋值的属性值,每次都操作同一个的时候,每次执行都会被创建,这会导致资源过多被使用。
因此,优化的思路为: 将nodeItem和item.nodeValue 存储到浏览器内存内,每次调用就从浏览器内存取出而不是创建。
存贮到浏览器内存中的方式:闭包 ,代码如下
()=>{nodeItem.value = data[item.nodeValue]}
上述还有一个问题,由于我们响应式数据绑定到的dom节点可能有多个,所以node 节点可能存在多个一旦响应式属性(name) 发生变化与name属性相关的所有的dom节点都需要进行一轮更新, 所以属性和更新函数之间是一个对多的关系
{
nodeValue:[
()=>{nodeItem1.value = data[item.nodeValue]},//name标签的
()=>{nodeItem1.value = data[item.nodeValue]},//age标签的
......
]
}
而要实现精确更新的这个过程,也被称之为订阅发布者模式
3,订阅发布者模式
我们先要知道定义发布者模式,在浏览器中,我们不能为同一个标签绑定相同的事件名称,此时我们可以使用addEventListener为多个DOM节点绑定相同的事件名,这种模式就是订阅发布者模式,
我们可以自定义一个定义发布者模式事件:
第一步:定义一个map,key为事件名,value为事件回调函数集合
let map = {}
第二步:收集事件名和回调函数存贮到map中
function collect(eventName, fn) {
if (!map[eventName]) {
map[eventName] = []
}
map[eventName].push(fn)
}
第三步,执行回调函数
function trigger(eventName) {
map[eventName].forEach(fn => fn())
}
为了代码美观,我们把全部代码放到一个对象内。
let addDeep = {
map: {},
collect: function (eventName, fn) {
if (!this.map[eventName]) {
this.map[eventName] = []
}
this.map[eventName].push(fn)
},
trigger:function (eventName) {
this.map[eventName].forEach(fn => fn())
}
}
测试代码:
addDeep.collect('lick', () => {
console.log("加一");
})
addDeep.collect('lick', () => {
console.log("加二");
})
addDeep.trigger('lick')
此时一个简单的订阅发布者模式就做好了。
3.1.使用订阅发布者模式实现精确更新
思路:我们在实现v-model和v-text的时候,更新的代码分别为 data[item.nodeValue]=nodeValue和nodeItem.innerText = data[item.nodeValue],我们需要更新的是data的属性值,因此我们需要收集到相关属性,之前我们说需要把nodeItem和item.nodeValue 形成一个闭包,保存到内存中。因此回调函数的代码就是
nodeItem.value = data[item.nodeValue]
也就是将更新操作放到回调函数中。
在核心代码块处:收集相关数据
if (item.nodeName === 'v-model') {
// 收集数据
addDeep.collect(item.nodeValue,()=>{
nodeItem.value = data[item.nodeValue]
})
nodeItem.addEventListener('input',e=>{
let dataValue=e.target.value
data[item.nodeValue]=dataValue
console.log("11",data);
})
}
实现精确更新
set(newValue) {
value= newValue
// 精确更新
addDeep.trigger(key)
}
完整代码(v-model)
<body>
<div id="app">
名字:<input type="text" v-model="name" value="12"><br>
年龄:<input type="text" v-model="age" value="2323">
<div class="sjkx">
<h1 class="name">模拟v-model</h1>
</div>
</div>
<script>
let data = {
name: '',
age: '',
}
let addDeep = {
map: {},
collect: function (eventName, fn) {
if (!this.map[eventName]) {
this.map[eventName] = []
}
this.map[eventName].push(fn)
},
trigger:function (eventName) {
this.map[eventName].forEach(fn => fn())
}
}
let view = document.getElementById('app')
// 数据反映到视图方法
function getVmodelNode(view, data) {
// a,获取到视图中每个节点的DOM对象,
let allDom = view.getElementsByTagName("*")
let allDomArray = Array.from(allDom)//将伪数组转化成数组
let attributeArray = []
// b,遍历DOM对象,获取到每个DOM对象对应的node对象。
allDomArray.forEach(nodeItem => {
//c, 遍历node对象,过滤获取到node对象中含有v-model的节点
Array.from(nodeItem.attributes).forEach(item => {
if (item.nodeName === 'v-model') {
//d,将对应的data的属性值赋值给node节点的value值上
// nodeItem.value = data[item.nodeValue]
console.log("22",data);
addDeep.collect(item.nodeValue,()=>{
nodeItem.value = data[item.nodeValue]
})
//视图层反映到数据层上,
nodeItem.addEventListener('input',e=>{
let dataValue=e.target.value
data[item.nodeValue]=dataValue
console.log("11",data);
})
}
})
})
}
// console.log(addDeep.map);
// A,首先需要遍历data的数据,获取到每个data的属性
Object.keys(data).forEach(key => {
// B,其次,获取到data的每个属性之后,对属性进行拦截。
WebServer(data,key,data[key])
})
function WebServer(data, key, value) {
Object.defineProperty(data, key, {
get() {
return value
},
set(newValue) {
value= newValue
// C,在拦截时,需要将其值反映到对应的视图标签上。
// getVmodelNode(view,data)
addDeep.trigger(key)
}
})
}
getVmodelNode(view, data)
// console.log(data);
</script>
</body>
在v-model代码核心处打印:
if (item.nodeName === 'v-model') {
//d,将对应的data的属性值赋值给node节点的value值上
// nodeItem.value = data[item.nodeValue]
console.log("22",data);
addDeep.collect(item.nodeValue,()=>{
nodeItem.value = data[item.nodeValue]
})
//视图层反映到数据层上,
nodeItem.addEventListener('input',e=>{
let dataValue=e.target.value
data[item.nodeValue]=dataValue
console.log("11",data);
})
}
初始化时:调用两次
输入新值:
值调用一次更新函数。这就实现了优化比较好的v-model源码了。
结束了,看都看到这了,点赞收藏吧~~~~~算我球球你啦!!!
vue进阶一~数据响应式,数据响应到视图层,手写v-model,订阅发布者模式,的更多相关文章
- angular,vue,react的基本语法—插值表达式,渲染数据,响应式数据
基本语法: 1.插值表达式: vue:{{}} react:{} angular:{{}} 2.渲染数据 vue js: export default{ data(){ return{ msg:&qu ...
- Vue实现双向绑定的原理以及响应式数据
一.vue中的响应式属性 Vue中的数据实现响应式绑定 1.对象实现响应式: 是在初始化的时候利用definePrototype的定义set和get过滤器,在进行组件模板编译时实现water的监听搜集 ...
- vue源码之响应式数据
分析vue是如何实现数据响应的. 前记 现在回顾一下看数据响应的原因. 之前看了vuex和vue-i18n的源码, 他们都有自己内部的vm, 也就是vue实例. 使用的都是vue的响应式数据特性及$w ...
- vue响应式数据变化
vue响应式数据变化 话不多说,先上代码: //拷贝一份数组原型,防止修改所有数组类型变量的原型方法 let arrayProto = Array.prototype;// 数组原型上的方法 let ...
- vue基础响应式数据
1.vue 采用 v……vm……m,模式,v---->el,vm---->new Vue(实例),m---->data 数据,让前端从操作大量的dom元素中解放出来. 2.vue响应 ...
- 仿VUE创建响应式数据
VUE对于前端开发人员都非常熟悉了,其工作原理估计也都能说的清个大概,具体代码的实现估计看的人不会太多,这里对vue响应式数据做个简单的实现. 先简单介绍一下VUE数据响应原理,VUE响应数据分为对象 ...
- Vue2手写源码---响应式数据的变化
响应式数据变化 数据发生变化后,我们可以监听到这个数据的变化 (每一步后面的括号是表示在那个模块进行的操作) 手写简单的响应式数据的实现(对象属性劫持.深度属性劫持.数组函数劫持).模板转成 ast ...
- Vue 进阶系列(一)之响应式原理及实现
Vue 进阶系列(一)之响应式原理及实现:https://juejin.im/post/5bce6a26e51d4579e9711f1d Vue 进阶系列(二)之插件原理及实现:https://jue ...
- 【Vue原理模拟】模拟Vue实现响应式数据
1. 预期效果 当数据变动时,触发自定义的回调函数. 2. 思路 对对象 object 的 setter 进行设置,使 setter 在赋值之后执行回调函数 callback(). 3.细节 3.1 ...
- Vue 响应式数据说明
值得注意的是只有当实例被创建时 data 中存在的属性才是响应式的.也就是说如果你添加一个新的属性,比如: vm.b = 'hi' 那么对 b 的改动将不会触发任何视图的更新. 这里唯一的例外是使用 ...
随机推荐
- mysql,左连接 ,查询右表为null的写法,删除,带join条件的写法
select * from sale_guest sg left join sale_bill sbon sg.bill_id=sb.id where sg.gmt_create>'2023-1 ...
- golang nsq 同一个topic有多个channel,同时项目又互相引用时出现的问题
p.p1 { margin: 0; font: 12px ".PingFang SC" } span.s1 { font: 12px "Helvetica Neue&qu ...
- .NET Core 项目Linux环境下生成二维码
问题: 公司系统开发中,需要对企微授权链接进行二维码生成,然后向客户提供:当然,首当其冲想到的是使用ZXing.NET库进行实现,毕竟生成简单二维码也就那几句代码:然而,在本地环境中,一切都很正常,但 ...
- Jenkins自动化集成
gitlab连接Jenkins 创建token后 , 现在的网页上就会出现一个token: token只出现一次,注意保存 将这个token在Jenkins上配置,现在开始配置Jenkins Jenk ...
- ReplayKit2:声音回调时间戳问题
一.ReplayKit2 框架回调中 视频.micphone声音.系统声音三路回调 - (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffe ...
- Python 潮流周刊#54:ChatTTS 强大的文本生成语音模型
本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章.教程.开源项目.软件工具.播客和视频.热门话题等内容.愿景:帮助所有读者精进 Python 技术,并增长职 ...
- HDFS 常用操作命令
HDFS 文件操作命令 注,其实常用命令不用网上搜,和linux下的命令很类似,触类旁通,直接在linux 上 hadoop fs 看一下就行了,不需要刻意去记 我把 linux 上的 help 列举 ...
- 如何解决系统报错:nf_conntrack: table full, dropping packets
问题 在系统日志中(/var/log/messages),有时会看到大面积的下面的报错: nf_conntrack: table full, dropping packet 这说明系统接到了大量的连接 ...
- star 最多的 Go 语言本地化库|GitHub 2.8K
如果你是一位 Go 用户,可以在我开源的学习仓库中,找到针对各种往期归档文章,及学习资料. B站:白泽talk,公众号[白泽talk],回复"电子书",即可获得包含<100个 ...
- 父类和子类对象的获取值的方式验证,通过父类属性的方式获取不到值,需要使用get方法
父类和子类对象的获取值的方式验证,通过父类属性的方式获取不到值,需要使用get方法 静态属性通过类.属性的方式获取,对象获取使用get方法获取 package com.example.core.myd ...